##// END OF EJS Templates
hgweb: run with "served" filter...
Pierre-Yves David -
r18429:e9ea0f0f default
parent child Browse files
Show More
@@ -1,332 +1,334 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 import os
9 import os
10 from mercurial import ui, hg, hook, error, encoding, templater, util
10 from mercurial import ui, hg, hook, error, encoding, templater, util
11 from common import get_stat, ErrorResponse, permhooks, caching
11 from common import get_stat, ErrorResponse, permhooks, caching
12 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
12 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
13 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 from request import wsgirequest
14 from request import wsgirequest
15 import webcommands, protocol, webutil
15 import webcommands, protocol, webutil
16
16
17 perms = {
17 perms = {
18 'changegroup': 'pull',
18 'changegroup': 'pull',
19 'changegroupsubset': 'pull',
19 'changegroupsubset': 'pull',
20 'getbundle': 'pull',
20 'getbundle': 'pull',
21 'stream_out': 'pull',
21 'stream_out': 'pull',
22 'listkeys': 'pull',
22 'listkeys': 'pull',
23 'unbundle': 'push',
23 'unbundle': 'push',
24 'pushkey': 'push',
24 'pushkey': 'push',
25 }
25 }
26
26
27 def makebreadcrumb(url):
27 def makebreadcrumb(url):
28 '''Return a 'URL breadcrumb' list
28 '''Return a 'URL breadcrumb' list
29
29
30 A 'URL breadcrumb' is a list of URL-name pairs,
30 A 'URL breadcrumb' is a list of URL-name pairs,
31 corresponding to each of the path items on a URL.
31 corresponding to each of the path items on a URL.
32 This can be used to create path navigation entries.
32 This can be used to create path navigation entries.
33 '''
33 '''
34 if url.endswith('/'):
34 if url.endswith('/'):
35 url = url[:-1]
35 url = url[:-1]
36 relpath = url
36 relpath = url
37 if relpath.startswith('/'):
37 if relpath.startswith('/'):
38 relpath = relpath[1:]
38 relpath = relpath[1:]
39
39
40 breadcrumb = []
40 breadcrumb = []
41 urlel = url
41 urlel = url
42 pathitems = [''] + relpath.split('/')
42 pathitems = [''] + relpath.split('/')
43 for pathel in reversed(pathitems):
43 for pathel in reversed(pathitems):
44 if not pathel or not urlel:
44 if not pathel or not urlel:
45 break
45 break
46 breadcrumb.append({'url': urlel, 'name': pathel})
46 breadcrumb.append({'url': urlel, 'name': pathel})
47 urlel = os.path.dirname(urlel)
47 urlel = os.path.dirname(urlel)
48 return reversed(breadcrumb)
48 return reversed(breadcrumb)
49
49
50
50
51 class hgweb(object):
51 class hgweb(object):
52 def __init__(self, repo, name=None, baseui=None):
52 def __init__(self, repo, name=None, baseui=None):
53 if isinstance(repo, str):
53 if isinstance(repo, str):
54 if baseui:
54 if baseui:
55 u = baseui.copy()
55 u = baseui.copy()
56 else:
56 else:
57 u = ui.ui()
57 u = ui.ui()
58 self.repo = hg.repository(u, repo)
58 self.repo = hg.repository(u, repo)
59 else:
59 else:
60 self.repo = repo
60 self.repo = repo
61
61
62 self.repo = self.repo.filtered('served')
62 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
63 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
63 self.repo.ui.setconfig('ui', 'nontty', 'true')
64 self.repo.ui.setconfig('ui', 'nontty', 'true')
64 hook.redirect(True)
65 hook.redirect(True)
65 self.mtime = -1
66 self.mtime = -1
66 self.size = -1
67 self.size = -1
67 self.reponame = name
68 self.reponame = name
68 self.archives = 'zip', 'gz', 'bz2'
69 self.archives = 'zip', 'gz', 'bz2'
69 self.stripecount = 1
70 self.stripecount = 1
70 # a repo owner may set web.templates in .hg/hgrc to get any file
71 # a repo owner may set web.templates in .hg/hgrc to get any file
71 # readable by the user running the CGI script
72 # readable by the user running the CGI script
72 self.templatepath = self.config('web', 'templates')
73 self.templatepath = self.config('web', 'templates')
73
74
74 # The CGI scripts are often run by a user different from the repo owner.
75 # The CGI scripts are often run by a user different from the repo owner.
75 # Trust the settings from the .hg/hgrc files by default.
76 # Trust the settings from the .hg/hgrc files by default.
76 def config(self, section, name, default=None, untrusted=True):
77 def config(self, section, name, default=None, untrusted=True):
77 return self.repo.ui.config(section, name, default,
78 return self.repo.ui.config(section, name, default,
78 untrusted=untrusted)
79 untrusted=untrusted)
79
80
80 def configbool(self, section, name, default=False, untrusted=True):
81 def configbool(self, section, name, default=False, untrusted=True):
81 return self.repo.ui.configbool(section, name, default,
82 return self.repo.ui.configbool(section, name, default,
82 untrusted=untrusted)
83 untrusted=untrusted)
83
84
84 def configlist(self, section, name, default=None, untrusted=True):
85 def configlist(self, section, name, default=None, untrusted=True):
85 return self.repo.ui.configlist(section, name, default,
86 return self.repo.ui.configlist(section, name, default,
86 untrusted=untrusted)
87 untrusted=untrusted)
87
88
88 def refresh(self, request=None):
89 def refresh(self, request=None):
89 if request:
90 if request:
90 self.repo.ui.environ = request.env
91 self.repo.ui.environ = request.env
91 st = get_stat(self.repo.spath)
92 st = get_stat(self.repo.spath)
92 # compare changelog size in addition to mtime to catch
93 # compare changelog size in addition to mtime to catch
93 # rollbacks made less than a second ago
94 # rollbacks made less than a second ago
94 if st.st_mtime != self.mtime or st.st_size != self.size:
95 if st.st_mtime != self.mtime or st.st_size != self.size:
95 self.mtime = st.st_mtime
96 self.mtime = st.st_mtime
96 self.size = st.st_size
97 self.size = st.st_size
97 self.repo = hg.repository(self.repo.ui, self.repo.root)
98 self.repo = hg.repository(self.repo.ui, self.repo.root)
99 self.repo = self.repo.filtered('served')
98 self.maxchanges = int(self.config("web", "maxchanges", 10))
100 self.maxchanges = int(self.config("web", "maxchanges", 10))
99 self.stripecount = int(self.config("web", "stripes", 1))
101 self.stripecount = int(self.config("web", "stripes", 1))
100 self.maxshortchanges = int(self.config("web", "maxshortchanges",
102 self.maxshortchanges = int(self.config("web", "maxshortchanges",
101 60))
103 60))
102 self.maxfiles = int(self.config("web", "maxfiles", 10))
104 self.maxfiles = int(self.config("web", "maxfiles", 10))
103 self.allowpull = self.configbool("web", "allowpull", True)
105 self.allowpull = self.configbool("web", "allowpull", True)
104 encoding.encoding = self.config("web", "encoding",
106 encoding.encoding = self.config("web", "encoding",
105 encoding.encoding)
107 encoding.encoding)
106
108
107 def run(self):
109 def run(self):
108 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
110 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
109 raise RuntimeError("This function is only intended to be "
111 raise RuntimeError("This function is only intended to be "
110 "called while running as a CGI script.")
112 "called while running as a CGI script.")
111 import mercurial.hgweb.wsgicgi as wsgicgi
113 import mercurial.hgweb.wsgicgi as wsgicgi
112 wsgicgi.launch(self)
114 wsgicgi.launch(self)
113
115
114 def __call__(self, env, respond):
116 def __call__(self, env, respond):
115 req = wsgirequest(env, respond)
117 req = wsgirequest(env, respond)
116 return self.run_wsgi(req)
118 return self.run_wsgi(req)
117
119
118 def run_wsgi(self, req):
120 def run_wsgi(self, req):
119
121
120 self.refresh(req)
122 self.refresh(req)
121
123
122 # work with CGI variables to create coherent structure
124 # work with CGI variables to create coherent structure
123 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
125 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
124
126
125 req.url = req.env['SCRIPT_NAME']
127 req.url = req.env['SCRIPT_NAME']
126 if not req.url.endswith('/'):
128 if not req.url.endswith('/'):
127 req.url += '/'
129 req.url += '/'
128 if 'REPO_NAME' in req.env:
130 if 'REPO_NAME' in req.env:
129 req.url += req.env['REPO_NAME'] + '/'
131 req.url += req.env['REPO_NAME'] + '/'
130
132
131 if 'PATH_INFO' in req.env:
133 if 'PATH_INFO' in req.env:
132 parts = req.env['PATH_INFO'].strip('/').split('/')
134 parts = req.env['PATH_INFO'].strip('/').split('/')
133 repo_parts = req.env.get('REPO_NAME', '').split('/')
135 repo_parts = req.env.get('REPO_NAME', '').split('/')
134 if parts[:len(repo_parts)] == repo_parts:
136 if parts[:len(repo_parts)] == repo_parts:
135 parts = parts[len(repo_parts):]
137 parts = parts[len(repo_parts):]
136 query = '/'.join(parts)
138 query = '/'.join(parts)
137 else:
139 else:
138 query = req.env['QUERY_STRING'].split('&', 1)[0]
140 query = req.env['QUERY_STRING'].split('&', 1)[0]
139 query = query.split(';', 1)[0]
141 query = query.split(';', 1)[0]
140
142
141 # process this if it's a protocol request
143 # process this if it's a protocol request
142 # protocol bits don't need to create any URLs
144 # protocol bits don't need to create any URLs
143 # and the clients always use the old URL structure
145 # and the clients always use the old URL structure
144
146
145 cmd = req.form.get('cmd', [''])[0]
147 cmd = req.form.get('cmd', [''])[0]
146 if protocol.iscmd(cmd):
148 if protocol.iscmd(cmd):
147 try:
149 try:
148 if query:
150 if query:
149 raise ErrorResponse(HTTP_NOT_FOUND)
151 raise ErrorResponse(HTTP_NOT_FOUND)
150 if cmd in perms:
152 if cmd in perms:
151 self.check_perm(req, perms[cmd])
153 self.check_perm(req, perms[cmd])
152 return protocol.call(self.repo, req, cmd)
154 return protocol.call(self.repo, req, cmd)
153 except ErrorResponse, inst:
155 except ErrorResponse, inst:
154 # A client that sends unbundle without 100-continue will
156 # A client that sends unbundle without 100-continue will
155 # break if we respond early.
157 # break if we respond early.
156 if (cmd == 'unbundle' and
158 if (cmd == 'unbundle' and
157 (req.env.get('HTTP_EXPECT',
159 (req.env.get('HTTP_EXPECT',
158 '').lower() != '100-continue') or
160 '').lower() != '100-continue') or
159 req.env.get('X-HgHttp2', '')):
161 req.env.get('X-HgHttp2', '')):
160 req.drain()
162 req.drain()
161 req.respond(inst, protocol.HGTYPE,
163 req.respond(inst, protocol.HGTYPE,
162 body='0\n%s\n' % inst.message)
164 body='0\n%s\n' % inst.message)
163 return ''
165 return ''
164
166
165 # translate user-visible url structure to internal structure
167 # translate user-visible url structure to internal structure
166
168
167 args = query.split('/', 2)
169 args = query.split('/', 2)
168 if 'cmd' not in req.form and args and args[0]:
170 if 'cmd' not in req.form and args and args[0]:
169
171
170 cmd = args.pop(0)
172 cmd = args.pop(0)
171 style = cmd.rfind('-')
173 style = cmd.rfind('-')
172 if style != -1:
174 if style != -1:
173 req.form['style'] = [cmd[:style]]
175 req.form['style'] = [cmd[:style]]
174 cmd = cmd[style + 1:]
176 cmd = cmd[style + 1:]
175
177
176 # avoid accepting e.g. style parameter as command
178 # avoid accepting e.g. style parameter as command
177 if util.safehasattr(webcommands, cmd):
179 if util.safehasattr(webcommands, cmd):
178 req.form['cmd'] = [cmd]
180 req.form['cmd'] = [cmd]
179 else:
181 else:
180 cmd = ''
182 cmd = ''
181
183
182 if cmd == 'static':
184 if cmd == 'static':
183 req.form['file'] = ['/'.join(args)]
185 req.form['file'] = ['/'.join(args)]
184 else:
186 else:
185 if args and args[0]:
187 if args and args[0]:
186 node = args.pop(0)
188 node = args.pop(0)
187 req.form['node'] = [node]
189 req.form['node'] = [node]
188 if args:
190 if args:
189 req.form['file'] = args
191 req.form['file'] = args
190
192
191 ua = req.env.get('HTTP_USER_AGENT', '')
193 ua = req.env.get('HTTP_USER_AGENT', '')
192 if cmd == 'rev' and 'mercurial' in ua:
194 if cmd == 'rev' and 'mercurial' in ua:
193 req.form['style'] = ['raw']
195 req.form['style'] = ['raw']
194
196
195 if cmd == 'archive':
197 if cmd == 'archive':
196 fn = req.form['node'][0]
198 fn = req.form['node'][0]
197 for type_, spec in self.archive_specs.iteritems():
199 for type_, spec in self.archive_specs.iteritems():
198 ext = spec[2]
200 ext = spec[2]
199 if fn.endswith(ext):
201 if fn.endswith(ext):
200 req.form['node'] = [fn[:-len(ext)]]
202 req.form['node'] = [fn[:-len(ext)]]
201 req.form['type'] = [type_]
203 req.form['type'] = [type_]
202
204
203 # process the web interface request
205 # process the web interface request
204
206
205 try:
207 try:
206 tmpl = self.templater(req)
208 tmpl = self.templater(req)
207 ctype = tmpl('mimetype', encoding=encoding.encoding)
209 ctype = tmpl('mimetype', encoding=encoding.encoding)
208 ctype = templater.stringify(ctype)
210 ctype = templater.stringify(ctype)
209
211
210 # check read permissions non-static content
212 # check read permissions non-static content
211 if cmd != 'static':
213 if cmd != 'static':
212 self.check_perm(req, None)
214 self.check_perm(req, None)
213
215
214 if cmd == '':
216 if cmd == '':
215 req.form['cmd'] = [tmpl.cache['default']]
217 req.form['cmd'] = [tmpl.cache['default']]
216 cmd = req.form['cmd'][0]
218 cmd = req.form['cmd'][0]
217
219
218 if self.configbool('web', 'cache', True):
220 if self.configbool('web', 'cache', True):
219 caching(self, req) # sets ETag header or raises NOT_MODIFIED
221 caching(self, req) # sets ETag header or raises NOT_MODIFIED
220 if cmd not in webcommands.__all__:
222 if cmd not in webcommands.__all__:
221 msg = 'no such method: %s' % cmd
223 msg = 'no such method: %s' % cmd
222 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
224 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
223 elif cmd == 'file' and 'raw' in req.form.get('style', []):
225 elif cmd == 'file' and 'raw' in req.form.get('style', []):
224 self.ctype = ctype
226 self.ctype = ctype
225 content = webcommands.rawfile(self, req, tmpl)
227 content = webcommands.rawfile(self, req, tmpl)
226 else:
228 else:
227 content = getattr(webcommands, cmd)(self, req, tmpl)
229 content = getattr(webcommands, cmd)(self, req, tmpl)
228 req.respond(HTTP_OK, ctype)
230 req.respond(HTTP_OK, ctype)
229
231
230 return content
232 return content
231
233
232 except error.LookupError, err:
234 except error.LookupError, err:
233 req.respond(HTTP_NOT_FOUND, ctype)
235 req.respond(HTTP_NOT_FOUND, ctype)
234 msg = str(err)
236 msg = str(err)
235 if 'manifest' not in msg:
237 if 'manifest' not in msg:
236 msg = 'revision not found: %s' % err.name
238 msg = 'revision not found: %s' % err.name
237 return tmpl('error', error=msg)
239 return tmpl('error', error=msg)
238 except (error.RepoError, error.RevlogError), inst:
240 except (error.RepoError, error.RevlogError), inst:
239 req.respond(HTTP_SERVER_ERROR, ctype)
241 req.respond(HTTP_SERVER_ERROR, ctype)
240 return tmpl('error', error=str(inst))
242 return tmpl('error', error=str(inst))
241 except ErrorResponse, inst:
243 except ErrorResponse, inst:
242 req.respond(inst, ctype)
244 req.respond(inst, ctype)
243 if inst.code == HTTP_NOT_MODIFIED:
245 if inst.code == HTTP_NOT_MODIFIED:
244 # Not allowed to return a body on a 304
246 # Not allowed to return a body on a 304
245 return ['']
247 return ['']
246 return tmpl('error', error=inst.message)
248 return tmpl('error', error=inst.message)
247
249
248 def templater(self, req):
250 def templater(self, req):
249
251
250 # determine scheme, port and server name
252 # determine scheme, port and server name
251 # this is needed to create absolute urls
253 # this is needed to create absolute urls
252
254
253 proto = req.env.get('wsgi.url_scheme')
255 proto = req.env.get('wsgi.url_scheme')
254 if proto == 'https':
256 if proto == 'https':
255 proto = 'https'
257 proto = 'https'
256 default_port = "443"
258 default_port = "443"
257 else:
259 else:
258 proto = 'http'
260 proto = 'http'
259 default_port = "80"
261 default_port = "80"
260
262
261 port = req.env["SERVER_PORT"]
263 port = req.env["SERVER_PORT"]
262 port = port != default_port and (":" + port) or ""
264 port = port != default_port and (":" + port) or ""
263 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
265 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
264 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
266 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
265 logoimg = self.config("web", "logoimg", "hglogo.png")
267 logoimg = self.config("web", "logoimg", "hglogo.png")
266 staticurl = self.config("web", "staticurl") or req.url + 'static/'
268 staticurl = self.config("web", "staticurl") or req.url + 'static/'
267 if not staticurl.endswith('/'):
269 if not staticurl.endswith('/'):
268 staticurl += '/'
270 staticurl += '/'
269
271
270 # some functions for the templater
272 # some functions for the templater
271
273
272 def header(**map):
274 def header(**map):
273 yield tmpl('header', encoding=encoding.encoding, **map)
275 yield tmpl('header', encoding=encoding.encoding, **map)
274
276
275 def footer(**map):
277 def footer(**map):
276 yield tmpl("footer", **map)
278 yield tmpl("footer", **map)
277
279
278 def motd(**map):
280 def motd(**map):
279 yield self.config("web", "motd", "")
281 yield self.config("web", "motd", "")
280
282
281 # figure out which style to use
283 # figure out which style to use
282
284
283 vars = {}
285 vars = {}
284 styles = (
286 styles = (
285 req.form.get('style', [None])[0],
287 req.form.get('style', [None])[0],
286 self.config('web', 'style'),
288 self.config('web', 'style'),
287 'paper',
289 'paper',
288 )
290 )
289 style, mapfile = templater.stylemap(styles, self.templatepath)
291 style, mapfile = templater.stylemap(styles, self.templatepath)
290 if style == styles[0]:
292 if style == styles[0]:
291 vars['style'] = style
293 vars['style'] = style
292
294
293 start = req.url[-1] == '?' and '&' or '?'
295 start = req.url[-1] == '?' and '&' or '?'
294 sessionvars = webutil.sessionvars(vars, start)
296 sessionvars = webutil.sessionvars(vars, start)
295
297
296 if not self.reponame:
298 if not self.reponame:
297 self.reponame = (self.config("web", "name")
299 self.reponame = (self.config("web", "name")
298 or req.env.get('REPO_NAME')
300 or req.env.get('REPO_NAME')
299 or req.url.strip('/') or self.repo.root)
301 or req.url.strip('/') or self.repo.root)
300
302
301 # create the templater
303 # create the templater
302
304
303 tmpl = templater.templater(mapfile,
305 tmpl = templater.templater(mapfile,
304 defaults={"url": req.url,
306 defaults={"url": req.url,
305 "logourl": logourl,
307 "logourl": logourl,
306 "logoimg": logoimg,
308 "logoimg": logoimg,
307 "staticurl": staticurl,
309 "staticurl": staticurl,
308 "urlbase": urlbase,
310 "urlbase": urlbase,
309 "repo": self.reponame,
311 "repo": self.reponame,
310 "header": header,
312 "header": header,
311 "footer": footer,
313 "footer": footer,
312 "motd": motd,
314 "motd": motd,
313 "sessionvars": sessionvars,
315 "sessionvars": sessionvars,
314 "pathdef": makebreadcrumb(req.url),
316 "pathdef": makebreadcrumb(req.url),
315 })
317 })
316 return tmpl
318 return tmpl
317
319
318 def archivelist(self, nodeid):
320 def archivelist(self, nodeid):
319 allowed = self.configlist("web", "allow_archive")
321 allowed = self.configlist("web", "allow_archive")
320 for i, spec in self.archive_specs.iteritems():
322 for i, spec in self.archive_specs.iteritems():
321 if i in allowed or self.configbool("web", "allow" + i):
323 if i in allowed or self.configbool("web", "allow" + i):
322 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
324 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
323
325
324 archive_specs = {
326 archive_specs = {
325 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
327 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
326 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
328 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
327 'zip': ('application/zip', 'zip', '.zip', None),
329 'zip': ('application/zip', 'zip', '.zip', None),
328 }
330 }
329
331
330 def check_perm(self, req, op):
332 def check_perm(self, req, op):
331 for hook in permhooks:
333 for hook in permhooks:
332 hook(self, req, op)
334 hook(self, req, op)
General Comments 0
You need to be logged in to leave comments. Login now