##// END OF EJS Templates
hgweb: pass ErrorResponses directly into req.respond()
Dirkjan Ochtman -
r7740:176d3d68 default
parent child Browse files
Show More
@@ -1,313 +1,313 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
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os, mimetypes
9 import os, mimetypes
10 from mercurial.node import hex, nullid
10 from mercurial.node import hex, nullid
11 from mercurial import ui, hg, util, hook, error
11 from mercurial import ui, hg, util, hook, error
12 from mercurial import templater, templatefilters
12 from mercurial import templater, templatefilters
13 from common import get_mtime, style_map, ErrorResponse
13 from common import get_mtime, style_map, ErrorResponse
14 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
15 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
16 from request import wsgirequest
16 from request import wsgirequest
17 import webcommands, protocol, webutil
17 import webcommands, protocol, webutil
18
18
19 perms = {
19 perms = {
20 'changegroup': 'pull',
20 'changegroup': 'pull',
21 'changegroupsubset': 'pull',
21 'changegroupsubset': 'pull',
22 'unbundle': 'push',
22 'unbundle': 'push',
23 'stream_out': 'pull',
23 'stream_out': 'pull',
24 }
24 }
25
25
26 class hgweb(object):
26 class hgweb(object):
27 def __init__(self, repo, name=None):
27 def __init__(self, repo, name=None):
28 if isinstance(repo, str):
28 if isinstance(repo, str):
29 parentui = ui.ui(report_untrusted=False, interactive=False)
29 parentui = ui.ui(report_untrusted=False, interactive=False)
30 self.repo = hg.repository(parentui, repo)
30 self.repo = hg.repository(parentui, repo)
31 else:
31 else:
32 self.repo = repo
32 self.repo = repo
33
33
34 hook.redirect(True)
34 hook.redirect(True)
35 self.mtime = -1
35 self.mtime = -1
36 self.reponame = name
36 self.reponame = name
37 self.archives = 'zip', 'gz', 'bz2'
37 self.archives = 'zip', 'gz', 'bz2'
38 self.stripecount = 1
38 self.stripecount = 1
39 # a repo owner may set web.templates in .hg/hgrc to get any file
39 # a repo owner may set web.templates in .hg/hgrc to get any file
40 # readable by the user running the CGI script
40 # readable by the user running the CGI script
41 self.templatepath = self.config("web", "templates",
41 self.templatepath = self.config("web", "templates",
42 templater.templatepath(),
42 templater.templatepath(),
43 untrusted=False)
43 untrusted=False)
44
44
45 # The CGI scripts are often run by a user different from the repo owner.
45 # The CGI scripts are often run by a user different from the repo owner.
46 # Trust the settings from the .hg/hgrc files by default.
46 # Trust the settings from the .hg/hgrc files by default.
47 def config(self, section, name, default=None, untrusted=True):
47 def config(self, section, name, default=None, untrusted=True):
48 return self.repo.ui.config(section, name, default,
48 return self.repo.ui.config(section, name, default,
49 untrusted=untrusted)
49 untrusted=untrusted)
50
50
51 def configbool(self, section, name, default=False, untrusted=True):
51 def configbool(self, section, name, default=False, untrusted=True):
52 return self.repo.ui.configbool(section, name, default,
52 return self.repo.ui.configbool(section, name, default,
53 untrusted=untrusted)
53 untrusted=untrusted)
54
54
55 def configlist(self, section, name, default=None, untrusted=True):
55 def configlist(self, section, name, default=None, untrusted=True):
56 return self.repo.ui.configlist(section, name, default,
56 return self.repo.ui.configlist(section, name, default,
57 untrusted=untrusted)
57 untrusted=untrusted)
58
58
59 def refresh(self):
59 def refresh(self):
60 mtime = get_mtime(self.repo.root)
60 mtime = get_mtime(self.repo.root)
61 if mtime != self.mtime:
61 if mtime != self.mtime:
62 self.mtime = mtime
62 self.mtime = mtime
63 self.repo = hg.repository(self.repo.ui, self.repo.root)
63 self.repo = hg.repository(self.repo.ui, self.repo.root)
64 self.maxchanges = int(self.config("web", "maxchanges", 10))
64 self.maxchanges = int(self.config("web", "maxchanges", 10))
65 self.stripecount = int(self.config("web", "stripes", 1))
65 self.stripecount = int(self.config("web", "stripes", 1))
66 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
66 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
67 self.maxfiles = int(self.config("web", "maxfiles", 10))
67 self.maxfiles = int(self.config("web", "maxfiles", 10))
68 self.allowpull = self.configbool("web", "allowpull", True)
68 self.allowpull = self.configbool("web", "allowpull", True)
69 self.encoding = self.config("web", "encoding", util._encoding)
69 self.encoding = self.config("web", "encoding", util._encoding)
70
70
71 def run(self):
71 def run(self):
72 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
72 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
73 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
73 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
74 import mercurial.hgweb.wsgicgi as wsgicgi
74 import mercurial.hgweb.wsgicgi as wsgicgi
75 wsgicgi.launch(self)
75 wsgicgi.launch(self)
76
76
77 def __call__(self, env, respond):
77 def __call__(self, env, respond):
78 req = wsgirequest(env, respond)
78 req = wsgirequest(env, respond)
79 return self.run_wsgi(req)
79 return self.run_wsgi(req)
80
80
81 def run_wsgi(self, req):
81 def run_wsgi(self, req):
82
82
83 self.refresh()
83 self.refresh()
84
84
85 # process this if it's a protocol request
85 # process this if it's a protocol request
86 # protocol bits don't need to create any URLs
86 # protocol bits don't need to create any URLs
87 # and the clients always use the old URL structure
87 # and the clients always use the old URL structure
88
88
89 cmd = req.form.get('cmd', [''])[0]
89 cmd = req.form.get('cmd', [''])[0]
90 if cmd and cmd in protocol.__all__:
90 if cmd and cmd in protocol.__all__:
91 try:
91 try:
92 if cmd in perms:
92 if cmd in perms:
93 try:
93 try:
94 self.check_perm(req, perms[cmd])
94 self.check_perm(req, perms[cmd])
95 except ErrorResponse, inst:
95 except ErrorResponse, inst:
96 if cmd == 'unbundle':
96 if cmd == 'unbundle':
97 req.drain()
97 req.drain()
98 raise
98 raise
99 method = getattr(protocol, cmd)
99 method = getattr(protocol, cmd)
100 return method(self.repo, req)
100 return method(self.repo, req)
101 except ErrorResponse, inst:
101 except ErrorResponse, inst:
102 req.respond(inst.code, protocol.HGTYPE)
102 req.respond(inst, protocol.HGTYPE)
103 if not inst.message:
103 if not inst.message:
104 return []
104 return []
105 return '0\n%s\n' % inst.message,
105 return '0\n%s\n' % inst.message,
106
106
107 # work with CGI variables to create coherent structure
107 # work with CGI variables to create coherent structure
108 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
108 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
109
109
110 req.url = req.env['SCRIPT_NAME']
110 req.url = req.env['SCRIPT_NAME']
111 if not req.url.endswith('/'):
111 if not req.url.endswith('/'):
112 req.url += '/'
112 req.url += '/'
113 if 'REPO_NAME' in req.env:
113 if 'REPO_NAME' in req.env:
114 req.url += req.env['REPO_NAME'] + '/'
114 req.url += req.env['REPO_NAME'] + '/'
115
115
116 if 'PATH_INFO' in req.env:
116 if 'PATH_INFO' in req.env:
117 parts = req.env['PATH_INFO'].strip('/').split('/')
117 parts = req.env['PATH_INFO'].strip('/').split('/')
118 repo_parts = req.env.get('REPO_NAME', '').split('/')
118 repo_parts = req.env.get('REPO_NAME', '').split('/')
119 if parts[:len(repo_parts)] == repo_parts:
119 if parts[:len(repo_parts)] == repo_parts:
120 parts = parts[len(repo_parts):]
120 parts = parts[len(repo_parts):]
121 query = '/'.join(parts)
121 query = '/'.join(parts)
122 else:
122 else:
123 query = req.env['QUERY_STRING'].split('&', 1)[0]
123 query = req.env['QUERY_STRING'].split('&', 1)[0]
124 query = query.split(';', 1)[0]
124 query = query.split(';', 1)[0]
125
125
126 # translate user-visible url structure to internal structure
126 # translate user-visible url structure to internal structure
127
127
128 args = query.split('/', 2)
128 args = query.split('/', 2)
129 if 'cmd' not in req.form and args and args[0]:
129 if 'cmd' not in req.form and args and args[0]:
130
130
131 cmd = args.pop(0)
131 cmd = args.pop(0)
132 style = cmd.rfind('-')
132 style = cmd.rfind('-')
133 if style != -1:
133 if style != -1:
134 req.form['style'] = [cmd[:style]]
134 req.form['style'] = [cmd[:style]]
135 cmd = cmd[style+1:]
135 cmd = cmd[style+1:]
136
136
137 # avoid accepting e.g. style parameter as command
137 # avoid accepting e.g. style parameter as command
138 if hasattr(webcommands, cmd):
138 if hasattr(webcommands, cmd):
139 req.form['cmd'] = [cmd]
139 req.form['cmd'] = [cmd]
140 else:
140 else:
141 cmd = ''
141 cmd = ''
142
142
143 if cmd == 'static':
143 if cmd == 'static':
144 req.form['file'] = ['/'.join(args)]
144 req.form['file'] = ['/'.join(args)]
145 else:
145 else:
146 if args and args[0]:
146 if args and args[0]:
147 node = args.pop(0)
147 node = args.pop(0)
148 req.form['node'] = [node]
148 req.form['node'] = [node]
149 if args:
149 if args:
150 req.form['file'] = args
150 req.form['file'] = args
151
151
152 if cmd == 'archive':
152 if cmd == 'archive':
153 fn = req.form['node'][0]
153 fn = req.form['node'][0]
154 for type_, spec in self.archive_specs.iteritems():
154 for type_, spec in self.archive_specs.iteritems():
155 ext = spec[2]
155 ext = spec[2]
156 if fn.endswith(ext):
156 if fn.endswith(ext):
157 req.form['node'] = [fn[:-len(ext)]]
157 req.form['node'] = [fn[:-len(ext)]]
158 req.form['type'] = [type_]
158 req.form['type'] = [type_]
159
159
160 # process the web interface request
160 # process the web interface request
161
161
162 try:
162 try:
163 tmpl = self.templater(req)
163 tmpl = self.templater(req)
164 ctype = tmpl('mimetype', encoding=self.encoding)
164 ctype = tmpl('mimetype', encoding=self.encoding)
165 ctype = templater.stringify(ctype)
165 ctype = templater.stringify(ctype)
166
166
167 # check read permissions non-static content
167 # check read permissions non-static content
168 if cmd != 'static':
168 if cmd != 'static':
169 self.check_perm(req, None)
169 self.check_perm(req, None)
170
170
171 if cmd == '':
171 if cmd == '':
172 req.form['cmd'] = [tmpl.cache['default']]
172 req.form['cmd'] = [tmpl.cache['default']]
173 cmd = req.form['cmd'][0]
173 cmd = req.form['cmd'][0]
174
174
175 if cmd not in webcommands.__all__:
175 if cmd not in webcommands.__all__:
176 msg = 'no such method: %s' % cmd
176 msg = 'no such method: %s' % cmd
177 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
177 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
178 elif cmd == 'file' and 'raw' in req.form.get('style', []):
178 elif cmd == 'file' and 'raw' in req.form.get('style', []):
179 self.ctype = ctype
179 self.ctype = ctype
180 content = webcommands.rawfile(self, req, tmpl)
180 content = webcommands.rawfile(self, req, tmpl)
181 else:
181 else:
182 content = getattr(webcommands, cmd)(self, req, tmpl)
182 content = getattr(webcommands, cmd)(self, req, tmpl)
183 req.respond(HTTP_OK, ctype)
183 req.respond(HTTP_OK, ctype)
184
184
185 return content
185 return content
186
186
187 except error.LookupError, err:
187 except error.LookupError, err:
188 req.respond(HTTP_NOT_FOUND, ctype)
188 req.respond(HTTP_NOT_FOUND, ctype)
189 msg = str(err)
189 msg = str(err)
190 if 'manifest' not in msg:
190 if 'manifest' not in msg:
191 msg = 'revision not found: %s' % err.name
191 msg = 'revision not found: %s' % err.name
192 return tmpl('error', error=msg)
192 return tmpl('error', error=msg)
193 except (error.RepoError, error.RevlogError), inst:
193 except (error.RepoError, error.RevlogError), inst:
194 req.respond(HTTP_SERVER_ERROR, ctype)
194 req.respond(HTTP_SERVER_ERROR, ctype)
195 return tmpl('error', error=str(inst))
195 return tmpl('error', error=str(inst))
196 except ErrorResponse, inst:
196 except ErrorResponse, inst:
197 req.respond(inst.code, ctype)
197 req.respond(inst, ctype)
198 return tmpl('error', error=inst.message)
198 return tmpl('error', error=inst.message)
199
199
200 def templater(self, req):
200 def templater(self, req):
201
201
202 # determine scheme, port and server name
202 # determine scheme, port and server name
203 # this is needed to create absolute urls
203 # this is needed to create absolute urls
204
204
205 proto = req.env.get('wsgi.url_scheme')
205 proto = req.env.get('wsgi.url_scheme')
206 if proto == 'https':
206 if proto == 'https':
207 proto = 'https'
207 proto = 'https'
208 default_port = "443"
208 default_port = "443"
209 else:
209 else:
210 proto = 'http'
210 proto = 'http'
211 default_port = "80"
211 default_port = "80"
212
212
213 port = req.env["SERVER_PORT"]
213 port = req.env["SERVER_PORT"]
214 port = port != default_port and (":" + port) or ""
214 port = port != default_port and (":" + port) or ""
215 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
215 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
216 staticurl = self.config("web", "staticurl") or req.url + 'static/'
216 staticurl = self.config("web", "staticurl") or req.url + 'static/'
217 if not staticurl.endswith('/'):
217 if not staticurl.endswith('/'):
218 staticurl += '/'
218 staticurl += '/'
219
219
220 # some functions for the templater
220 # some functions for the templater
221
221
222 def header(**map):
222 def header(**map):
223 yield tmpl('header', encoding=self.encoding, **map)
223 yield tmpl('header', encoding=self.encoding, **map)
224
224
225 def footer(**map):
225 def footer(**map):
226 yield tmpl("footer", **map)
226 yield tmpl("footer", **map)
227
227
228 def motd(**map):
228 def motd(**map):
229 yield self.config("web", "motd", "")
229 yield self.config("web", "motd", "")
230
230
231 # figure out which style to use
231 # figure out which style to use
232
232
233 vars = {}
233 vars = {}
234 style = self.config("web", "style", "paper")
234 style = self.config("web", "style", "paper")
235 if 'style' in req.form:
235 if 'style' in req.form:
236 style = req.form['style'][0]
236 style = req.form['style'][0]
237 vars['style'] = style
237 vars['style'] = style
238
238
239 start = req.url[-1] == '?' and '&' or '?'
239 start = req.url[-1] == '?' and '&' or '?'
240 sessionvars = webutil.sessionvars(vars, start)
240 sessionvars = webutil.sessionvars(vars, start)
241 mapfile = style_map(self.templatepath, style)
241 mapfile = style_map(self.templatepath, style)
242
242
243 if not self.reponame:
243 if not self.reponame:
244 self.reponame = (self.config("web", "name")
244 self.reponame = (self.config("web", "name")
245 or req.env.get('REPO_NAME')
245 or req.env.get('REPO_NAME')
246 or req.url.strip('/') or self.repo.root)
246 or req.url.strip('/') or self.repo.root)
247
247
248 # create the templater
248 # create the templater
249
249
250 tmpl = templater.templater(mapfile, templatefilters.filters,
250 tmpl = templater.templater(mapfile, templatefilters.filters,
251 defaults={"url": req.url,
251 defaults={"url": req.url,
252 "staticurl": staticurl,
252 "staticurl": staticurl,
253 "urlbase": urlbase,
253 "urlbase": urlbase,
254 "repo": self.reponame,
254 "repo": self.reponame,
255 "header": header,
255 "header": header,
256 "footer": footer,
256 "footer": footer,
257 "motd": motd,
257 "motd": motd,
258 "sessionvars": sessionvars
258 "sessionvars": sessionvars
259 })
259 })
260 return tmpl
260 return tmpl
261
261
262 def archivelist(self, nodeid):
262 def archivelist(self, nodeid):
263 allowed = self.configlist("web", "allow_archive")
263 allowed = self.configlist("web", "allow_archive")
264 for i, spec in self.archive_specs.iteritems():
264 for i, spec in self.archive_specs.iteritems():
265 if i in allowed or self.configbool("web", "allow" + i):
265 if i in allowed or self.configbool("web", "allow" + i):
266 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
266 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
267
267
268 archive_specs = {
268 archive_specs = {
269 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
269 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
270 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
270 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
271 'zip': ('application/zip', 'zip', '.zip', None),
271 'zip': ('application/zip', 'zip', '.zip', None),
272 }
272 }
273
273
274 def check_perm(self, req, op):
274 def check_perm(self, req, op):
275 '''Check permission for operation based on request data (including
275 '''Check permission for operation based on request data (including
276 authentication info). Return if op allowed, else raise an ErrorResponse
276 authentication info). Return if op allowed, else raise an ErrorResponse
277 exception.'''
277 exception.'''
278
278
279 user = req.env.get('REMOTE_USER')
279 user = req.env.get('REMOTE_USER')
280
280
281 deny_read = self.configlist('web', 'deny_read')
281 deny_read = self.configlist('web', 'deny_read')
282 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
282 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
283 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
283 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
284
284
285 allow_read = self.configlist('web', 'allow_read')
285 allow_read = self.configlist('web', 'allow_read')
286 result = (not allow_read) or (allow_read == ['*'])
286 result = (not allow_read) or (allow_read == ['*'])
287 if not result or user in allow_read:
287 if not result or user in allow_read:
288 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
288 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
289
289
290 if op == 'pull' and not self.allowpull:
290 if op == 'pull' and not self.allowpull:
291 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
291 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
292 elif op == 'pull' or op is None: # op is None for interface requests
292 elif op == 'pull' or op is None: # op is None for interface requests
293 return
293 return
294
294
295 # enforce that you can only push using POST requests
295 # enforce that you can only push using POST requests
296 if req.env['REQUEST_METHOD'] != 'POST':
296 if req.env['REQUEST_METHOD'] != 'POST':
297 msg = 'push requires POST request'
297 msg = 'push requires POST request'
298 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
298 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
299
299
300 # require ssl by default for pushing, auth info cannot be sniffed
300 # require ssl by default for pushing, auth info cannot be sniffed
301 # and replayed
301 # and replayed
302 scheme = req.env.get('wsgi.url_scheme')
302 scheme = req.env.get('wsgi.url_scheme')
303 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
303 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
304 raise ErrorResponse(HTTP_OK, 'ssl required')
304 raise ErrorResponse(HTTP_OK, 'ssl required')
305
305
306 deny = self.configlist('web', 'deny_push')
306 deny = self.configlist('web', 'deny_push')
307 if deny and (not user or deny == ['*'] or user in deny):
307 if deny and (not user or deny == ['*'] or user in deny):
308 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
308 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
309
309
310 allow = self.configlist('web', 'allow_push')
310 allow = self.configlist('web', 'allow_push')
311 result = allow and (allow == ['*'] or user in allow)
311 result = allow and (allow == ['*'] or user in allow)
312 if not result:
312 if not result:
313 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
313 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
@@ -1,327 +1,327 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os
9 import os
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import ui, hg, util, templater, templatefilters, error
11 from mercurial import ui, hg, util, templater, templatefilters, error
12 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen,\
12 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen,\
13 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 from hgweb_mod import hgweb
14 from hgweb_mod import hgweb
15 from request import wsgirequest
15 from request import wsgirequest
16
16
17 # This is a stopgap
17 # This is a stopgap
18 class hgwebdir(object):
18 class hgwebdir(object):
19 def __init__(self, config, parentui=None):
19 def __init__(self, config, parentui=None):
20 def cleannames(items):
20 def cleannames(items):
21 return [(util.pconvert(name).strip('/'), path)
21 return [(util.pconvert(name).strip('/'), path)
22 for name, path in items]
22 for name, path in items]
23
23
24 self.parentui = parentui or ui.ui(report_untrusted=False,
24 self.parentui = parentui or ui.ui(report_untrusted=False,
25 interactive = False)
25 interactive = False)
26 self.motd = None
26 self.motd = None
27 self.style = 'paper'
27 self.style = 'paper'
28 self.stripecount = None
28 self.stripecount = None
29 self.repos_sorted = ('name', False)
29 self.repos_sorted = ('name', False)
30 self._baseurl = None
30 self._baseurl = None
31 if isinstance(config, (list, tuple)):
31 if isinstance(config, (list, tuple)):
32 self.repos = cleannames(config)
32 self.repos = cleannames(config)
33 self.repos_sorted = ('', False)
33 self.repos_sorted = ('', False)
34 elif isinstance(config, dict):
34 elif isinstance(config, dict):
35 self.repos = util.sort(cleannames(config.items()))
35 self.repos = util.sort(cleannames(config.items()))
36 else:
36 else:
37 if isinstance(config, util.configparser):
37 if isinstance(config, util.configparser):
38 cp = config
38 cp = config
39 else:
39 else:
40 cp = util.configparser()
40 cp = util.configparser()
41 cp.read(config)
41 cp.read(config)
42 self.repos = []
42 self.repos = []
43 if cp.has_section('web'):
43 if cp.has_section('web'):
44 if cp.has_option('web', 'motd'):
44 if cp.has_option('web', 'motd'):
45 self.motd = cp.get('web', 'motd')
45 self.motd = cp.get('web', 'motd')
46 if cp.has_option('web', 'style'):
46 if cp.has_option('web', 'style'):
47 self.style = cp.get('web', 'style')
47 self.style = cp.get('web', 'style')
48 if cp.has_option('web', 'stripes'):
48 if cp.has_option('web', 'stripes'):
49 self.stripecount = int(cp.get('web', 'stripes'))
49 self.stripecount = int(cp.get('web', 'stripes'))
50 if cp.has_option('web', 'baseurl'):
50 if cp.has_option('web', 'baseurl'):
51 self._baseurl = cp.get('web', 'baseurl')
51 self._baseurl = cp.get('web', 'baseurl')
52 if cp.has_section('paths'):
52 if cp.has_section('paths'):
53 paths = cleannames(cp.items('paths'))
53 paths = cleannames(cp.items('paths'))
54 for prefix, root in paths:
54 for prefix, root in paths:
55 roothead, roottail = os.path.split(root)
55 roothead, roottail = os.path.split(root)
56 # "foo = /bar/*" makes every subrepo of /bar/ to be
56 # "foo = /bar/*" makes every subrepo of /bar/ to be
57 # mounted as foo/subrepo
57 # mounted as foo/subrepo
58 # and "foo = /bar/**" does even recurse inside the
58 # and "foo = /bar/**" does even recurse inside the
59 # subdirectories, remember to use it without working dir.
59 # subdirectories, remember to use it without working dir.
60 try:
60 try:
61 recurse = {'*': False, '**': True}[roottail]
61 recurse = {'*': False, '**': True}[roottail]
62 except KeyError:
62 except KeyError:
63 self.repos.append((prefix, root))
63 self.repos.append((prefix, root))
64 continue
64 continue
65 roothead = os.path.normpath(roothead)
65 roothead = os.path.normpath(roothead)
66 for path in util.walkrepos(roothead, followsym=True,
66 for path in util.walkrepos(roothead, followsym=True,
67 recurse=recurse):
67 recurse=recurse):
68 path = os.path.normpath(path)
68 path = os.path.normpath(path)
69 name = util.pconvert(path[len(roothead):]).strip('/')
69 name = util.pconvert(path[len(roothead):]).strip('/')
70 if prefix:
70 if prefix:
71 name = prefix + '/' + name
71 name = prefix + '/' + name
72 self.repos.append((name, path))
72 self.repos.append((name, path))
73 if cp.has_section('collections'):
73 if cp.has_section('collections'):
74 for prefix, root in cp.items('collections'):
74 for prefix, root in cp.items('collections'):
75 for path in util.walkrepos(root, followsym=True):
75 for path in util.walkrepos(root, followsym=True):
76 repo = os.path.normpath(path)
76 repo = os.path.normpath(path)
77 name = repo
77 name = repo
78 if name.startswith(prefix):
78 if name.startswith(prefix):
79 name = name[len(prefix):]
79 name = name[len(prefix):]
80 self.repos.append((name.lstrip(os.sep), repo))
80 self.repos.append((name.lstrip(os.sep), repo))
81 self.repos.sort()
81 self.repos.sort()
82
82
83 def run(self):
83 def run(self):
84 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
84 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
85 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
85 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
86 import mercurial.hgweb.wsgicgi as wsgicgi
86 import mercurial.hgweb.wsgicgi as wsgicgi
87 wsgicgi.launch(self)
87 wsgicgi.launch(self)
88
88
89 def __call__(self, env, respond):
89 def __call__(self, env, respond):
90 req = wsgirequest(env, respond)
90 req = wsgirequest(env, respond)
91 return self.run_wsgi(req)
91 return self.run_wsgi(req)
92
92
93 def read_allowed(self, ui, req):
93 def read_allowed(self, ui, req):
94 """Check allow_read and deny_read config options of a repo's ui object
94 """Check allow_read and deny_read config options of a repo's ui object
95 to determine user permissions. By default, with neither option set (or
95 to determine user permissions. By default, with neither option set (or
96 both empty), allow all users to read the repo. There are two ways a
96 both empty), allow all users to read the repo. There are two ways a
97 user can be denied read access: (1) deny_read is not empty, and the
97 user can be denied read access: (1) deny_read is not empty, and the
98 user is unauthenticated or deny_read contains user (or *), and (2)
98 user is unauthenticated or deny_read contains user (or *), and (2)
99 allow_read is not empty and the user is not in allow_read. Return True
99 allow_read is not empty and the user is not in allow_read. Return True
100 if user is allowed to read the repo, else return False."""
100 if user is allowed to read the repo, else return False."""
101
101
102 user = req.env.get('REMOTE_USER')
102 user = req.env.get('REMOTE_USER')
103
103
104 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
104 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
105 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
105 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
106 return False
106 return False
107
107
108 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
108 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
109 # by default, allow reading if no allow_read option has been set
109 # by default, allow reading if no allow_read option has been set
110 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
110 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
111 return True
111 return True
112
112
113 return False
113 return False
114
114
115 def run_wsgi(self, req):
115 def run_wsgi(self, req):
116
116
117 try:
117 try:
118 try:
118 try:
119
119
120 virtual = req.env.get("PATH_INFO", "").strip('/')
120 virtual = req.env.get("PATH_INFO", "").strip('/')
121 tmpl = self.templater(req)
121 tmpl = self.templater(req)
122 ctype = tmpl('mimetype', encoding=util._encoding)
122 ctype = tmpl('mimetype', encoding=util._encoding)
123 ctype = templater.stringify(ctype)
123 ctype = templater.stringify(ctype)
124
124
125 # a static file
125 # a static file
126 if virtual.startswith('static/') or 'static' in req.form:
126 if virtual.startswith('static/') or 'static' in req.form:
127 if virtual.startswith('static/'):
127 if virtual.startswith('static/'):
128 fname = virtual[7:]
128 fname = virtual[7:]
129 else:
129 else:
130 fname = req.form['static'][0]
130 fname = req.form['static'][0]
131 static = templater.templatepath('static')
131 static = templater.templatepath('static')
132 return (staticfile(static, fname, req),)
132 return (staticfile(static, fname, req),)
133
133
134 # top-level index
134 # top-level index
135 elif not virtual:
135 elif not virtual:
136 req.respond(HTTP_OK, ctype)
136 req.respond(HTTP_OK, ctype)
137 return self.makeindex(req, tmpl)
137 return self.makeindex(req, tmpl)
138
138
139 # nested indexes and hgwebs
139 # nested indexes and hgwebs
140
140
141 repos = dict(self.repos)
141 repos = dict(self.repos)
142 while virtual:
142 while virtual:
143 real = repos.get(virtual)
143 real = repos.get(virtual)
144 if real:
144 if real:
145 req.env['REPO_NAME'] = virtual
145 req.env['REPO_NAME'] = virtual
146 try:
146 try:
147 repo = hg.repository(self.parentui, real)
147 repo = hg.repository(self.parentui, real)
148 return hgweb(repo).run_wsgi(req)
148 return hgweb(repo).run_wsgi(req)
149 except IOError, inst:
149 except IOError, inst:
150 msg = inst.strerror
150 msg = inst.strerror
151 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
151 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
152 except error.RepoError, inst:
152 except error.RepoError, inst:
153 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
153 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
154
154
155 # browse subdirectories
155 # browse subdirectories
156 subdir = virtual + '/'
156 subdir = virtual + '/'
157 if [r for r in repos if r.startswith(subdir)]:
157 if [r for r in repos if r.startswith(subdir)]:
158 req.respond(HTTP_OK, ctype)
158 req.respond(HTTP_OK, ctype)
159 return self.makeindex(req, tmpl, subdir)
159 return self.makeindex(req, tmpl, subdir)
160
160
161 up = virtual.rfind('/')
161 up = virtual.rfind('/')
162 if up < 0:
162 if up < 0:
163 break
163 break
164 virtual = virtual[:up]
164 virtual = virtual[:up]
165
165
166 # prefixes not found
166 # prefixes not found
167 req.respond(HTTP_NOT_FOUND, ctype)
167 req.respond(HTTP_NOT_FOUND, ctype)
168 return tmpl("notfound", repo=virtual)
168 return tmpl("notfound", repo=virtual)
169
169
170 except ErrorResponse, err:
170 except ErrorResponse, err:
171 req.respond(err.code, ctype)
171 req.respond(err, ctype)
172 return tmpl('error', error=err.message or '')
172 return tmpl('error', error=err.message or '')
173 finally:
173 finally:
174 tmpl = None
174 tmpl = None
175
175
176 def makeindex(self, req, tmpl, subdir=""):
176 def makeindex(self, req, tmpl, subdir=""):
177
177
178 def archivelist(ui, nodeid, url):
178 def archivelist(ui, nodeid, url):
179 allowed = ui.configlist("web", "allow_archive", untrusted=True)
179 allowed = ui.configlist("web", "allow_archive", untrusted=True)
180 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
180 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
181 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
181 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
182 untrusted=True):
182 untrusted=True):
183 yield {"type" : i[0], "extension": i[1],
183 yield {"type" : i[0], "extension": i[1],
184 "node": nodeid, "url": url}
184 "node": nodeid, "url": url}
185
185
186 def entries(sortcolumn="", descending=False, subdir="", **map):
186 def entries(sortcolumn="", descending=False, subdir="", **map):
187 def sessionvars(**map):
187 def sessionvars(**map):
188 fields = []
188 fields = []
189 if 'style' in req.form:
189 if 'style' in req.form:
190 style = req.form['style'][0]
190 style = req.form['style'][0]
191 if style != get('web', 'style', ''):
191 if style != get('web', 'style', ''):
192 fields.append(('style', style))
192 fields.append(('style', style))
193
193
194 separator = url[-1] == '?' and ';' or '?'
194 separator = url[-1] == '?' and ';' or '?'
195 for name, value in fields:
195 for name, value in fields:
196 yield dict(name=name, value=value, separator=separator)
196 yield dict(name=name, value=value, separator=separator)
197 separator = ';'
197 separator = ';'
198
198
199 rows = []
199 rows = []
200 parity = paritygen(self.stripecount)
200 parity = paritygen(self.stripecount)
201 for name, path in self.repos:
201 for name, path in self.repos:
202 if not name.startswith(subdir):
202 if not name.startswith(subdir):
203 continue
203 continue
204 name = name[len(subdir):]
204 name = name[len(subdir):]
205
205
206 u = ui.ui(parentui=self.parentui)
206 u = ui.ui(parentui=self.parentui)
207 try:
207 try:
208 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
208 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
209 except Exception, e:
209 except Exception, e:
210 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
210 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
211 continue
211 continue
212 def get(section, name, default=None):
212 def get(section, name, default=None):
213 return u.config(section, name, default, untrusted=True)
213 return u.config(section, name, default, untrusted=True)
214
214
215 if u.configbool("web", "hidden", untrusted=True):
215 if u.configbool("web", "hidden", untrusted=True):
216 continue
216 continue
217
217
218 if not self.read_allowed(u, req):
218 if not self.read_allowed(u, req):
219 continue
219 continue
220
220
221 parts = [name]
221 parts = [name]
222 if 'PATH_INFO' in req.env:
222 if 'PATH_INFO' in req.env:
223 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
223 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
224 if req.env['SCRIPT_NAME']:
224 if req.env['SCRIPT_NAME']:
225 parts.insert(0, req.env['SCRIPT_NAME'])
225 parts.insert(0, req.env['SCRIPT_NAME'])
226 url = ('/'.join(parts).replace("//", "/")) + '/'
226 url = ('/'.join(parts).replace("//", "/")) + '/'
227
227
228 # update time with local timezone
228 # update time with local timezone
229 try:
229 try:
230 d = (get_mtime(path), util.makedate()[1])
230 d = (get_mtime(path), util.makedate()[1])
231 except OSError:
231 except OSError:
232 continue
232 continue
233
233
234 contact = get_contact(get)
234 contact = get_contact(get)
235 description = get("web", "description", "")
235 description = get("web", "description", "")
236 name = get("web", "name", name)
236 name = get("web", "name", name)
237 row = dict(contact=contact or "unknown",
237 row = dict(contact=contact or "unknown",
238 contact_sort=contact.upper() or "unknown",
238 contact_sort=contact.upper() or "unknown",
239 name=name,
239 name=name,
240 name_sort=name,
240 name_sort=name,
241 url=url,
241 url=url,
242 description=description or "unknown",
242 description=description or "unknown",
243 description_sort=description.upper() or "unknown",
243 description_sort=description.upper() or "unknown",
244 lastchange=d,
244 lastchange=d,
245 lastchange_sort=d[1]-d[0],
245 lastchange_sort=d[1]-d[0],
246 sessionvars=sessionvars,
246 sessionvars=sessionvars,
247 archives=archivelist(u, "tip", url))
247 archives=archivelist(u, "tip", url))
248 if (not sortcolumn
248 if (not sortcolumn
249 or (sortcolumn, descending) == self.repos_sorted):
249 or (sortcolumn, descending) == self.repos_sorted):
250 # fast path for unsorted output
250 # fast path for unsorted output
251 row['parity'] = parity.next()
251 row['parity'] = parity.next()
252 yield row
252 yield row
253 else:
253 else:
254 rows.append((row["%s_sort" % sortcolumn], row))
254 rows.append((row["%s_sort" % sortcolumn], row))
255 if rows:
255 if rows:
256 rows.sort()
256 rows.sort()
257 if descending:
257 if descending:
258 rows.reverse()
258 rows.reverse()
259 for key, row in rows:
259 for key, row in rows:
260 row['parity'] = parity.next()
260 row['parity'] = parity.next()
261 yield row
261 yield row
262
262
263 sortable = ["name", "description", "contact", "lastchange"]
263 sortable = ["name", "description", "contact", "lastchange"]
264 sortcolumn, descending = self.repos_sorted
264 sortcolumn, descending = self.repos_sorted
265 if 'sort' in req.form:
265 if 'sort' in req.form:
266 sortcolumn = req.form['sort'][0]
266 sortcolumn = req.form['sort'][0]
267 descending = sortcolumn.startswith('-')
267 descending = sortcolumn.startswith('-')
268 if descending:
268 if descending:
269 sortcolumn = sortcolumn[1:]
269 sortcolumn = sortcolumn[1:]
270 if sortcolumn not in sortable:
270 if sortcolumn not in sortable:
271 sortcolumn = ""
271 sortcolumn = ""
272
272
273 sort = [("sort_%s" % column,
273 sort = [("sort_%s" % column,
274 "%s%s" % ((not descending and column == sortcolumn)
274 "%s%s" % ((not descending and column == sortcolumn)
275 and "-" or "", column))
275 and "-" or "", column))
276 for column in sortable]
276 for column in sortable]
277
277
278 if self._baseurl is not None:
278 if self._baseurl is not None:
279 req.env['SCRIPT_NAME'] = self._baseurl
279 req.env['SCRIPT_NAME'] = self._baseurl
280
280
281 return tmpl("index", entries=entries, subdir=subdir,
281 return tmpl("index", entries=entries, subdir=subdir,
282 sortcolumn=sortcolumn, descending=descending,
282 sortcolumn=sortcolumn, descending=descending,
283 **dict(sort))
283 **dict(sort))
284
284
285 def templater(self, req):
285 def templater(self, req):
286
286
287 def header(**map):
287 def header(**map):
288 yield tmpl('header', encoding=util._encoding, **map)
288 yield tmpl('header', encoding=util._encoding, **map)
289
289
290 def footer(**map):
290 def footer(**map):
291 yield tmpl("footer", **map)
291 yield tmpl("footer", **map)
292
292
293 def motd(**map):
293 def motd(**map):
294 if self.motd is not None:
294 if self.motd is not None:
295 yield self.motd
295 yield self.motd
296 else:
296 else:
297 yield config('web', 'motd', '')
297 yield config('web', 'motd', '')
298
298
299 def config(section, name, default=None, untrusted=True):
299 def config(section, name, default=None, untrusted=True):
300 return self.parentui.config(section, name, default, untrusted)
300 return self.parentui.config(section, name, default, untrusted)
301
301
302 if self._baseurl is not None:
302 if self._baseurl is not None:
303 req.env['SCRIPT_NAME'] = self._baseurl
303 req.env['SCRIPT_NAME'] = self._baseurl
304
304
305 url = req.env.get('SCRIPT_NAME', '')
305 url = req.env.get('SCRIPT_NAME', '')
306 if not url.endswith('/'):
306 if not url.endswith('/'):
307 url += '/'
307 url += '/'
308
308
309 staticurl = config('web', 'staticurl') or url + 'static/'
309 staticurl = config('web', 'staticurl') or url + 'static/'
310 if not staticurl.endswith('/'):
310 if not staticurl.endswith('/'):
311 staticurl += '/'
311 staticurl += '/'
312
312
313 style = self.style
313 style = self.style
314 if style is None:
314 if style is None:
315 style = config('web', 'style', '')
315 style = config('web', 'style', '')
316 if 'style' in req.form:
316 if 'style' in req.form:
317 style = req.form['style'][0]
317 style = req.form['style'][0]
318 if self.stripecount is None:
318 if self.stripecount is None:
319 self.stripecount = int(config('web', 'stripes', 1))
319 self.stripecount = int(config('web', 'stripes', 1))
320 mapfile = style_map(templater.templatepath(), style)
320 mapfile = style_map(templater.templatepath(), style)
321 tmpl = templater.templater(mapfile, templatefilters.filters,
321 tmpl = templater.templater(mapfile, templatefilters.filters,
322 defaults={"header": header,
322 defaults={"header": header,
323 "footer": footer,
323 "footer": footer,
324 "motd": motd,
324 "motd": motd,
325 "url": url,
325 "url": url,
326 "staticurl": staticurl})
326 "staticurl": staticurl})
327 return tmpl
327 return tmpl
General Comments 0
You need to be logged in to leave comments. Login now