##// END OF EJS Templates
hgweb: move shortcut expansion to request instantiation
Dirkjan Ochtman -
r6774:0dbb56e9 default
parent child Browse files
Show More
@@ -1,379 +1,354 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os, mimetypes
10 10 from mercurial.node import hex, nullid
11 11 from mercurial.repo import RepoError
12 12 from mercurial import mdiff, ui, hg, util, patch, hook
13 13 from mercurial import revlog, templater, templatefilters, changegroup
14 14 from common import get_mtime, style_map, paritygen, countgen, ErrorResponse
15 15 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 16 from request import wsgirequest
17 17 import webcommands, protocol, webutil
18 18
19 shortcuts = {
20 'cl': [('cmd', ['changelog']), ('rev', None)],
21 'sl': [('cmd', ['shortlog']), ('rev', None)],
22 'cs': [('cmd', ['changeset']), ('node', None)],
23 'f': [('cmd', ['file']), ('filenode', None)],
24 'fl': [('cmd', ['filelog']), ('filenode', None)],
25 'fd': [('cmd', ['filediff']), ('node', None)],
26 'fa': [('cmd', ['annotate']), ('filenode', None)],
27 'mf': [('cmd', ['manifest']), ('manifest', None)],
28 'ca': [('cmd', ['archive']), ('node', None)],
29 'tags': [('cmd', ['tags'])],
30 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
31 'static': [('cmd', ['static']), ('file', None)]
32 }
33
34 19 class hgweb(object):
35 20 def __init__(self, repo, name=None):
36 21 if isinstance(repo, str):
37 22 parentui = ui.ui(report_untrusted=False, interactive=False)
38 23 self.repo = hg.repository(parentui, repo)
39 24 else:
40 25 self.repo = repo
41 26
42 27 hook.redirect(True)
43 28 self.mtime = -1
44 29 self.reponame = name
45 30 self.archives = 'zip', 'gz', 'bz2'
46 31 self.stripecount = 1
47 32 self._capabilities = None
48 33 # a repo owner may set web.templates in .hg/hgrc to get any file
49 34 # readable by the user running the CGI script
50 35 self.templatepath = self.config("web", "templates",
51 36 templater.templatepath(),
52 37 untrusted=False)
53 38
54 39 # The CGI scripts are often run by a user different from the repo owner.
55 40 # Trust the settings from the .hg/hgrc files by default.
56 41 def config(self, section, name, default=None, untrusted=True):
57 42 return self.repo.ui.config(section, name, default,
58 43 untrusted=untrusted)
59 44
60 45 def configbool(self, section, name, default=False, untrusted=True):
61 46 return self.repo.ui.configbool(section, name, default,
62 47 untrusted=untrusted)
63 48
64 49 def configlist(self, section, name, default=None, untrusted=True):
65 50 return self.repo.ui.configlist(section, name, default,
66 51 untrusted=untrusted)
67 52
68 53 def refresh(self):
69 54 mtime = get_mtime(self.repo.root)
70 55 if mtime != self.mtime:
71 56 self.mtime = mtime
72 57 self.repo = hg.repository(self.repo.ui, self.repo.root)
73 58 self.maxchanges = int(self.config("web", "maxchanges", 10))
74 59 self.stripecount = int(self.config("web", "stripes", 1))
75 60 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
76 61 self.maxfiles = int(self.config("web", "maxfiles", 10))
77 62 self.allowpull = self.configbool("web", "allowpull", True)
78 63 self.encoding = self.config("web", "encoding", util._encoding)
79 64 self._capabilities = None
80 65
81 66 def capabilities(self):
82 67 if self._capabilities is not None:
83 68 return self._capabilities
84 69 caps = ['lookup', 'changegroupsubset']
85 70 if self.configbool('server', 'uncompressed'):
86 71 caps.append('stream=%d' % self.repo.changelog.version)
87 72 if changegroup.bundlepriority:
88 73 caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority))
89 74 self._capabilities = caps
90 75 return caps
91 76
92 77 def run(self):
93 78 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
94 79 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
95 80 import mercurial.hgweb.wsgicgi as wsgicgi
96 81 wsgicgi.launch(self)
97 82
98 83 def __call__(self, env, respond):
99 84 req = wsgirequest(env, respond)
100 85 self.run_wsgi(req)
101 86 return req
102 87
103 88 def run_wsgi(self, req):
104 89
105 90 self.refresh()
106 91
107 # expand form shortcuts
108
109 for k in shortcuts.iterkeys():
110 if k in req.form:
111 for name, value in shortcuts[k]:
112 if value is None:
113 value = req.form[k]
114 req.form[name] = value
115 del req.form[k]
116
117 92 # work with CGI variables to create coherent structure
118 93 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
119 94
120 95 req.url = req.env['SCRIPT_NAME']
121 96 if not req.url.endswith('/'):
122 97 req.url += '/'
123 98 if 'REPO_NAME' in req.env:
124 99 req.url += req.env['REPO_NAME'] + '/'
125 100
126 101 if 'PATH_INFO' in req.env:
127 102 parts = req.env['PATH_INFO'].strip('/').split('/')
128 103 repo_parts = req.env.get('REPO_NAME', '').split('/')
129 104 if parts[:len(repo_parts)] == repo_parts:
130 105 parts = parts[len(repo_parts):]
131 106 query = '/'.join(parts)
132 107 else:
133 108 query = req.env['QUERY_STRING'].split('&', 1)[0]
134 109 query = query.split(';', 1)[0]
135 110
136 111 # translate user-visible url structure to internal structure
137 112
138 113 args = query.split('/', 2)
139 114 if 'cmd' not in req.form and args and args[0]:
140 115
141 116 cmd = args.pop(0)
142 117 style = cmd.rfind('-')
143 118 if style != -1:
144 119 req.form['style'] = [cmd[:style]]
145 120 cmd = cmd[style+1:]
146 121
147 122 # avoid accepting e.g. style parameter as command
148 123 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
149 124 req.form['cmd'] = [cmd]
150 125
151 126 if args and args[0]:
152 127 node = args.pop(0)
153 128 req.form['node'] = [node]
154 129 if args:
155 130 req.form['file'] = args
156 131
157 132 if cmd == 'static':
158 133 req.form['file'] = req.form['node']
159 134 elif cmd == 'archive':
160 135 fn = req.form['node'][0]
161 136 for type_, spec in self.archive_specs.iteritems():
162 137 ext = spec[2]
163 138 if fn.endswith(ext):
164 139 req.form['node'] = [fn[:-len(ext)]]
165 140 req.form['type'] = [type_]
166 141
167 142 # process this if it's a protocol request
168 143
169 144 cmd = req.form.get('cmd', [''])[0]
170 145 if cmd in protocol.__all__:
171 146 method = getattr(protocol, cmd)
172 147 method(self, req)
173 148 return
174 149
175 150 # process the web interface request
176 151
177 152 try:
178 153
179 154 tmpl = self.templater(req)
180 155 ctype = tmpl('mimetype', encoding=self.encoding)
181 156 ctype = templater.stringify(ctype)
182 157
183 158 if cmd == '':
184 159 req.form['cmd'] = [tmpl.cache['default']]
185 160 cmd = req.form['cmd'][0]
186 161
187 162 if cmd not in webcommands.__all__:
188 163 msg = 'no such method: %s' % cmd
189 164 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
190 165 elif cmd == 'file' and 'raw' in req.form.get('style', []):
191 166 self.ctype = ctype
192 167 content = webcommands.rawfile(self, req, tmpl)
193 168 else:
194 169 content = getattr(webcommands, cmd)(self, req, tmpl)
195 170 req.respond(HTTP_OK, ctype)
196 171
197 172 req.write(content)
198 173 del tmpl
199 174
200 175 except revlog.LookupError, err:
201 176 req.respond(HTTP_NOT_FOUND, ctype)
202 177 msg = str(err)
203 178 if 'manifest' not in msg:
204 179 msg = 'revision not found: %s' % err.name
205 180 req.write(tmpl('error', error=msg))
206 181 except (RepoError, revlog.RevlogError), inst:
207 182 req.respond(HTTP_SERVER_ERROR, ctype)
208 183 req.write(tmpl('error', error=str(inst)))
209 184 except ErrorResponse, inst:
210 185 req.respond(inst.code, ctype)
211 186 req.write(tmpl('error', error=inst.message))
212 187
213 188 def templater(self, req):
214 189
215 190 # determine scheme, port and server name
216 191 # this is needed to create absolute urls
217 192
218 193 proto = req.env.get('wsgi.url_scheme')
219 194 if proto == 'https':
220 195 proto = 'https'
221 196 default_port = "443"
222 197 else:
223 198 proto = 'http'
224 199 default_port = "80"
225 200
226 201 port = req.env["SERVER_PORT"]
227 202 port = port != default_port and (":" + port) or ""
228 203 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
229 204 staticurl = self.config("web", "staticurl") or req.url + 'static/'
230 205 if not staticurl.endswith('/'):
231 206 staticurl += '/'
232 207
233 208 # some functions for the templater
234 209
235 210 def header(**map):
236 211 yield tmpl('header', encoding=self.encoding, **map)
237 212
238 213 def footer(**map):
239 214 yield tmpl("footer", **map)
240 215
241 216 def motd(**map):
242 217 yield self.config("web", "motd", "")
243 218
244 219 def sessionvars(**map):
245 220 fields = []
246 221 if 'style' in req.form:
247 222 style = req.form['style'][0]
248 223 if style != self.config('web', 'style', ''):
249 224 fields.append(('style', style))
250 225
251 226 separator = req.url[-1] == '?' and ';' or '?'
252 227 for name, value in fields:
253 228 yield dict(name=name, value=value, separator=separator)
254 229 separator = ';'
255 230
256 231 # figure out which style to use
257 232
258 233 style = self.config("web", "style", "")
259 234 if 'style' in req.form:
260 235 style = req.form['style'][0]
261 236 mapfile = style_map(self.templatepath, style)
262 237
263 238 if not self.reponame:
264 239 self.reponame = (self.config("web", "name")
265 240 or req.env.get('REPO_NAME')
266 241 or req.url.strip('/') or self.repo.root)
267 242
268 243 # create the templater
269 244
270 245 tmpl = templater.templater(mapfile, templatefilters.filters,
271 246 defaults={"url": req.url,
272 247 "staticurl": staticurl,
273 248 "urlbase": urlbase,
274 249 "repo": self.reponame,
275 250 "header": header,
276 251 "footer": footer,
277 252 "motd": motd,
278 253 "sessionvars": sessionvars
279 254 })
280 255 return tmpl
281 256
282 257 def archivelist(self, nodeid):
283 258 allowed = self.configlist("web", "allow_archive")
284 259 for i, spec in self.archive_specs.iteritems():
285 260 if i in allowed or self.configbool("web", "allow" + i):
286 261 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
287 262
288 263 def listfilediffs(self, tmpl, files, changeset):
289 264 for f in files[:self.maxfiles]:
290 265 yield tmpl("filedifflink", node=hex(changeset), file=f)
291 266 if len(files) > self.maxfiles:
292 267 yield tmpl("fileellipses")
293 268
294 269 def diff(self, tmpl, node1, node2, files):
295 270 def filterfiles(filters, files):
296 271 l = [x for x in files if x in filters]
297 272
298 273 for t in filters:
299 274 if t and t[-1] != os.sep:
300 275 t += os.sep
301 276 l += [x for x in files if x.startswith(t)]
302 277 return l
303 278
304 279 parity = paritygen(self.stripecount)
305 280 def diffblock(diff, f, fn):
306 281 yield tmpl("diffblock",
307 282 lines=prettyprintlines(diff),
308 283 parity=parity.next(),
309 284 file=f,
310 285 filenode=hex(fn or nullid))
311 286
312 287 blockcount = countgen()
313 288 def prettyprintlines(diff):
314 289 blockno = blockcount.next()
315 290 for lineno, l in enumerate(diff.splitlines(1)):
316 291 if blockno == 0:
317 292 lineno = lineno + 1
318 293 else:
319 294 lineno = "%d.%d" % (blockno, lineno + 1)
320 295 if l.startswith('+'):
321 296 ltype = "difflineplus"
322 297 elif l.startswith('-'):
323 298 ltype = "difflineminus"
324 299 elif l.startswith('@'):
325 300 ltype = "difflineat"
326 301 else:
327 302 ltype = "diffline"
328 303 yield tmpl(ltype,
329 304 line=l,
330 305 lineid="l%s" % lineno,
331 306 linenumber="% 8s" % lineno)
332 307
333 308 r = self.repo
334 309 c1 = r.changectx(node1)
335 310 c2 = r.changectx(node2)
336 311 date1 = util.datestr(c1.date())
337 312 date2 = util.datestr(c2.date())
338 313
339 314 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
340 315 if files:
341 316 modified, added, removed = map(lambda x: filterfiles(files, x),
342 317 (modified, added, removed))
343 318
344 319 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
345 320 for f in modified:
346 321 to = c1.filectx(f).data()
347 322 tn = c2.filectx(f).data()
348 323 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
349 324 opts=diffopts), f, tn)
350 325 for f in added:
351 326 to = None
352 327 tn = c2.filectx(f).data()
353 328 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
354 329 opts=diffopts), f, tn)
355 330 for f in removed:
356 331 to = c1.filectx(f).data()
357 332 tn = None
358 333 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
359 334 opts=diffopts), f, tn)
360 335
361 336 archive_specs = {
362 337 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
363 338 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
364 339 'zip': ('application/zip', 'zip', '.zip', None),
365 340 }
366 341
367 342 def check_perm(self, req, op, default):
368 343 '''check permission for operation based on user auth.
369 344 return true if op allowed, else false.
370 345 default is policy to use if no config given.'''
371 346
372 347 user = req.env.get('REMOTE_USER')
373 348
374 349 deny = self.configlist('web', 'deny_' + op)
375 350 if deny and (not user or deny == ['*'] or user in deny):
376 351 return False
377 352
378 353 allow = self.configlist('web', 'allow_' + op)
379 354 return (allow and (allow == ['*'] or user in allow)) or default
@@ -1,101 +1,126 b''
1 1 # hgweb/request.py - An http request from either CGI or the standalone server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import socket, cgi, errno
10 10 from common import ErrorResponse, statusmessage
11 11
12 shortcuts = {
13 'cl': [('cmd', ['changelog']), ('rev', None)],
14 'sl': [('cmd', ['shortlog']), ('rev', None)],
15 'cs': [('cmd', ['changeset']), ('node', None)],
16 'f': [('cmd', ['file']), ('filenode', None)],
17 'fl': [('cmd', ['filelog']), ('filenode', None)],
18 'fd': [('cmd', ['filediff']), ('node', None)],
19 'fa': [('cmd', ['annotate']), ('filenode', None)],
20 'mf': [('cmd', ['manifest']), ('manifest', None)],
21 'ca': [('cmd', ['archive']), ('node', None)],
22 'tags': [('cmd', ['tags'])],
23 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
24 'static': [('cmd', ['static']), ('file', None)]
25 }
26
27 def expand(form):
28 for k in shortcuts.iterkeys():
29 if k in form:
30 for name, value in shortcuts[k]:
31 if value is None:
32 value = form[k]
33 form[name] = value
34 del form[k]
35 return form
36
12 37 class wsgirequest(object):
13 38 def __init__(self, wsgienv, start_response):
14 39 version = wsgienv['wsgi.version']
15 40 if (version < (1, 0)) or (version >= (2, 0)):
16 41 raise RuntimeError("Unknown and unsupported WSGI version %d.%d"
17 42 % version)
18 43 self.inp = wsgienv['wsgi.input']
19 44 self.err = wsgienv['wsgi.errors']
20 45 self.threaded = wsgienv['wsgi.multithread']
21 46 self.multiprocess = wsgienv['wsgi.multiprocess']
22 47 self.run_once = wsgienv['wsgi.run_once']
23 48 self.env = wsgienv
24 self.form = cgi.parse(self.inp, self.env, keep_blank_values=1)
49 self.form = expand(cgi.parse(self.inp, self.env, keep_blank_values=1))
25 50 self._start_response = start_response
26 51 self.server_write = None
27 52 self.headers = []
28 53
29 54 def __iter__(self):
30 55 return iter([])
31 56
32 57 def read(self, count=-1):
33 58 return self.inp.read(count)
34 59
35 60 def respond(self, status, type=None, filename=None, length=0):
36 61 if self._start_response is not None:
37 62
38 63 self.httphdr(type, filename, length)
39 64 if not self.headers:
40 65 raise RuntimeError("request.write called before headers sent")
41 66
42 67 for k, v in self.headers:
43 68 if not isinstance(v, str):
44 69 raise TypeError('header value must be string: %r' % v)
45 70
46 71 if isinstance(status, ErrorResponse):
47 72 status = statusmessage(status.code)
48 73 elif status == 200:
49 74 status = '200 Script output follows'
50 75 elif isinstance(status, int):
51 76 status = statusmessage(status)
52 77
53 78 self.server_write = self._start_response(status, self.headers)
54 79 self._start_response = None
55 80 self.headers = []
56 81
57 82 def write(self, thing):
58 83 if hasattr(thing, "__iter__"):
59 84 for part in thing:
60 85 self.write(part)
61 86 else:
62 87 thing = str(thing)
63 88 try:
64 89 self.server_write(thing)
65 90 except socket.error, inst:
66 91 if inst[0] != errno.ECONNRESET:
67 92 raise
68 93
69 94 def writelines(self, lines):
70 95 for line in lines:
71 96 self.write(line)
72 97
73 98 def flush(self):
74 99 return None
75 100
76 101 def close(self):
77 102 return None
78 103
79 104 def header(self, headers=[('Content-Type','text/html')]):
80 105 self.headers.extend(headers)
81 106
82 107 def httphdr(self, type=None, filename=None, length=0, headers={}):
83 108 headers = headers.items()
84 109 if type is not None:
85 110 headers.append(('Content-Type', type))
86 111 if filename:
87 112 filename = (filename.split('/')[-1]
88 113 .replace('\\', '\\\\').replace('"', '\\"'))
89 114 headers.append(('Content-Disposition',
90 115 'inline; filename="%s"' % filename))
91 116 if length:
92 117 headers.append(('Content-Length', str(length)))
93 118 self.header(headers)
94 119
95 120 def wsgiapplication(app_maker):
96 121 '''For compatibility with old CGI scripts. A plain hgweb() or hgwebdir()
97 122 can and should now be used as a WSGI application.'''
98 123 application = app_maker()
99 124 def run_wsgi(env, respond):
100 125 return application(env, respond)
101 126 return run_wsgi
General Comments 0
You need to be logged in to leave comments. Login now