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