##// END OF EJS Templates
hgweb: handle subdirectories within static directory
Brendan Cully -
r7287:6e9fe4ff default
parent child Browse files
Show More
@@ -1,380 +1,381 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
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 common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
17 17 from request import wsgirequest
18 18 import webcommands, protocol, webutil
19 19
20 20 perms = {
21 21 'changegroup': 'pull',
22 22 'changegroupsubset': 'pull',
23 23 'unbundle': 'push',
24 24 'stream_out': 'pull',
25 25 }
26 26
27 27 class hgweb(object):
28 28 def __init__(self, repo, name=None):
29 29 if isinstance(repo, str):
30 30 parentui = ui.ui(report_untrusted=False, interactive=False)
31 31 self.repo = hg.repository(parentui, repo)
32 32 else:
33 33 self.repo = repo
34 34
35 35 hook.redirect(True)
36 36 self.mtime = -1
37 37 self.reponame = name
38 38 self.archives = 'zip', 'gz', 'bz2'
39 39 self.stripecount = 1
40 40 # a repo owner may set web.templates in .hg/hgrc to get any file
41 41 # readable by the user running the CGI script
42 42 self.templatepath = self.config("web", "templates",
43 43 templater.templatepath(),
44 44 untrusted=False)
45 45
46 46 # The CGI scripts are often run by a user different from the repo owner.
47 47 # Trust the settings from the .hg/hgrc files by default.
48 48 def config(self, section, name, default=None, untrusted=True):
49 49 return self.repo.ui.config(section, name, default,
50 50 untrusted=untrusted)
51 51
52 52 def configbool(self, section, name, default=False, untrusted=True):
53 53 return self.repo.ui.configbool(section, name, default,
54 54 untrusted=untrusted)
55 55
56 56 def configlist(self, section, name, default=None, untrusted=True):
57 57 return self.repo.ui.configlist(section, name, default,
58 58 untrusted=untrusted)
59 59
60 60 def refresh(self):
61 61 mtime = get_mtime(self.repo.root)
62 62 if mtime != self.mtime:
63 63 self.mtime = mtime
64 64 self.repo = hg.repository(self.repo.ui, self.repo.root)
65 65 self.maxchanges = int(self.config("web", "maxchanges", 10))
66 66 self.stripecount = int(self.config("web", "stripes", 1))
67 67 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
68 68 self.maxfiles = int(self.config("web", "maxfiles", 10))
69 69 self.allowpull = self.configbool("web", "allowpull", True)
70 70 self.encoding = self.config("web", "encoding", util._encoding)
71 71
72 72 def run(self):
73 73 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
74 74 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
75 75 import mercurial.hgweb.wsgicgi as wsgicgi
76 76 wsgicgi.launch(self)
77 77
78 78 def __call__(self, env, respond):
79 79 req = wsgirequest(env, respond)
80 80 return self.run_wsgi(req)
81 81
82 82 def run_wsgi(self, req):
83 83
84 84 self.refresh()
85 85
86 86 # process this if it's a protocol request
87 87 # protocol bits don't need to create any URLs
88 88 # and the clients always use the old URL structure
89 89
90 90 cmd = req.form.get('cmd', [''])[0]
91 91 if cmd and cmd in protocol.__all__:
92 92 try:
93 93 if cmd in perms:
94 94 try:
95 95 self.check_perm(req, perms[cmd])
96 96 except ErrorResponse, inst:
97 97 if cmd == 'unbundle':
98 98 req.drain()
99 99 raise
100 100 method = getattr(protocol, cmd)
101 101 return method(self.repo, req)
102 102 except ErrorResponse, inst:
103 103 req.respond(inst.code, protocol.HGTYPE)
104 104 if not inst.message:
105 105 return []
106 106 return '0\n%s\n' % inst.message,
107 107
108 108 # work with CGI variables to create coherent structure
109 109 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
110 110
111 111 req.url = req.env['SCRIPT_NAME']
112 112 if not req.url.endswith('/'):
113 113 req.url += '/'
114 114 if 'REPO_NAME' in req.env:
115 115 req.url += req.env['REPO_NAME'] + '/'
116 116
117 117 if 'PATH_INFO' in req.env:
118 118 parts = req.env['PATH_INFO'].strip('/').split('/')
119 119 repo_parts = req.env.get('REPO_NAME', '').split('/')
120 120 if parts[:len(repo_parts)] == repo_parts:
121 121 parts = parts[len(repo_parts):]
122 122 query = '/'.join(parts)
123 123 else:
124 124 query = req.env['QUERY_STRING'].split('&', 1)[0]
125 125 query = query.split(';', 1)[0]
126 126
127 127 # translate user-visible url structure to internal structure
128 128
129 129 args = query.split('/', 2)
130 130 if 'cmd' not in req.form and args and args[0]:
131 131
132 132 cmd = args.pop(0)
133 133 style = cmd.rfind('-')
134 134 if style != -1:
135 135 req.form['style'] = [cmd[:style]]
136 136 cmd = cmd[style+1:]
137 137
138 138 # avoid accepting e.g. style parameter as command
139 139 if hasattr(webcommands, cmd):
140 140 req.form['cmd'] = [cmd]
141 141 else:
142 142 cmd = ''
143 143
144 if args and args[0]:
145 node = args.pop(0)
146 req.form['node'] = [node]
147 if args:
148 req.form['file'] = args
144 if cmd == 'static':
145 req.form['file'] = ['/'.join(args)]
146 else:
147 if args and args[0]:
148 node = args.pop(0)
149 req.form['node'] = [node]
150 if args:
151 req.form['file'] = args
149 152
150 if cmd == 'static':
151 req.form['file'] = req.form['node']
152 elif cmd == 'archive':
153 if cmd == 'archive':
153 154 fn = req.form['node'][0]
154 155 for type_, spec in self.archive_specs.iteritems():
155 156 ext = spec[2]
156 157 if fn.endswith(ext):
157 158 req.form['node'] = [fn[:-len(ext)]]
158 159 req.form['type'] = [type_]
159 160
160 161 # process the web interface request
161 162
162 163 try:
163 164
164 165 tmpl = self.templater(req)
165 166 ctype = tmpl('mimetype', encoding=self.encoding)
166 167 ctype = templater.stringify(ctype)
167 168
168 169 if cmd == '':
169 170 req.form['cmd'] = [tmpl.cache['default']]
170 171 cmd = req.form['cmd'][0]
171 172
172 173 if cmd not in webcommands.__all__:
173 174 msg = 'no such method: %s' % cmd
174 175 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
175 176 elif cmd == 'file' and 'raw' in req.form.get('style', []):
176 177 self.ctype = ctype
177 178 content = webcommands.rawfile(self, req, tmpl)
178 179 else:
179 180 content = getattr(webcommands, cmd)(self, req, tmpl)
180 181 req.respond(HTTP_OK, ctype)
181 182
182 183 return ''.join(content),
183 184
184 185 except revlog.LookupError, err:
185 186 req.respond(HTTP_NOT_FOUND, ctype)
186 187 msg = str(err)
187 188 if 'manifest' not in msg:
188 189 msg = 'revision not found: %s' % err.name
189 190 return ''.join(tmpl('error', error=msg)),
190 191 except (RepoError, revlog.RevlogError), inst:
191 192 req.respond(HTTP_SERVER_ERROR, ctype)
192 193 return ''.join(tmpl('error', error=str(inst))),
193 194 except ErrorResponse, inst:
194 195 req.respond(inst.code, ctype)
195 196 return ''.join(tmpl('error', error=inst.message)),
196 197
197 198 def templater(self, req):
198 199
199 200 # determine scheme, port and server name
200 201 # this is needed to create absolute urls
201 202
202 203 proto = req.env.get('wsgi.url_scheme')
203 204 if proto == 'https':
204 205 proto = 'https'
205 206 default_port = "443"
206 207 else:
207 208 proto = 'http'
208 209 default_port = "80"
209 210
210 211 port = req.env["SERVER_PORT"]
211 212 port = port != default_port and (":" + port) or ""
212 213 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
213 214 staticurl = self.config("web", "staticurl") or req.url + 'static/'
214 215 if not staticurl.endswith('/'):
215 216 staticurl += '/'
216 217
217 218 # some functions for the templater
218 219
219 220 def header(**map):
220 221 yield tmpl('header', encoding=self.encoding, **map)
221 222
222 223 def footer(**map):
223 224 yield tmpl("footer", **map)
224 225
225 226 def motd(**map):
226 227 yield self.config("web", "motd", "")
227 228
228 229 def sessionvars(**map):
229 230 fields = []
230 231 if 'style' in req.form:
231 232 style = req.form['style'][0]
232 233 if style != self.config('web', 'style', ''):
233 234 fields.append(('style', style))
234 235
235 236 separator = req.url[-1] == '?' and ';' or '?'
236 237 for name, value in fields:
237 238 yield dict(name=name, value=value, separator=separator)
238 239 separator = ';'
239 240
240 241 # figure out which style to use
241 242
242 243 style = self.config("web", "style", "")
243 244 if 'style' in req.form:
244 245 style = req.form['style'][0]
245 246 mapfile = style_map(self.templatepath, style)
246 247
247 248 if not self.reponame:
248 249 self.reponame = (self.config("web", "name")
249 250 or req.env.get('REPO_NAME')
250 251 or req.url.strip('/') or self.repo.root)
251 252
252 253 # create the templater
253 254
254 255 tmpl = templater.templater(mapfile, templatefilters.filters,
255 256 defaults={"url": req.url,
256 257 "staticurl": staticurl,
257 258 "urlbase": urlbase,
258 259 "repo": self.reponame,
259 260 "header": header,
260 261 "footer": footer,
261 262 "motd": motd,
262 263 "sessionvars": sessionvars
263 264 })
264 265 return tmpl
265 266
266 267 def archivelist(self, nodeid):
267 268 allowed = self.configlist("web", "allow_archive")
268 269 for i, spec in self.archive_specs.iteritems():
269 270 if i in allowed or self.configbool("web", "allow" + i):
270 271 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
271 272
272 273 def listfilediffs(self, tmpl, files, changeset):
273 274 for f in files[:self.maxfiles]:
274 275 yield tmpl("filedifflink", node=hex(changeset), file=f)
275 276 if len(files) > self.maxfiles:
276 277 yield tmpl("fileellipses")
277 278
278 279 def diff(self, tmpl, node1, node2, files):
279 280 def filterfiles(filters, files):
280 281 l = [x for x in files if x in filters]
281 282
282 283 for t in filters:
283 284 if t and t[-1] != os.sep:
284 285 t += os.sep
285 286 l += [x for x in files if x.startswith(t)]
286 287 return l
287 288
288 289 parity = paritygen(self.stripecount)
289 290 def diffblock(diff, f, fn):
290 291 yield tmpl("diffblock",
291 292 lines=prettyprintlines(diff),
292 293 parity=parity.next(),
293 294 file=f,
294 295 filenode=hex(fn or nullid))
295 296
296 297 blockcount = countgen()
297 298 def prettyprintlines(diff):
298 299 blockno = blockcount.next()
299 300 for lineno, l in enumerate(diff.splitlines(1)):
300 301 if blockno == 0:
301 302 lineno = lineno + 1
302 303 else:
303 304 lineno = "%d.%d" % (blockno, lineno + 1)
304 305 if l.startswith('+'):
305 306 ltype = "difflineplus"
306 307 elif l.startswith('-'):
307 308 ltype = "difflineminus"
308 309 elif l.startswith('@'):
309 310 ltype = "difflineat"
310 311 else:
311 312 ltype = "diffline"
312 313 yield tmpl(ltype,
313 314 line=l,
314 315 lineid="l%s" % lineno,
315 316 linenumber="% 8s" % lineno)
316 317
317 318 r = self.repo
318 319 c1 = r[node1]
319 320 c2 = r[node2]
320 321 date1 = util.datestr(c1.date())
321 322 date2 = util.datestr(c2.date())
322 323
323 324 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
324 325 if files:
325 326 modified, added, removed = map(lambda x: filterfiles(files, x),
326 327 (modified, added, removed))
327 328
328 329 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
329 330 for f in modified:
330 331 to = c1.filectx(f).data()
331 332 tn = c2.filectx(f).data()
332 333 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
333 334 opts=diffopts), f, tn)
334 335 for f in added:
335 336 to = None
336 337 tn = c2.filectx(f).data()
337 338 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
338 339 opts=diffopts), f, tn)
339 340 for f in removed:
340 341 to = c1.filectx(f).data()
341 342 tn = None
342 343 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
343 344 opts=diffopts), f, tn)
344 345
345 346 archive_specs = {
346 347 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
347 348 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
348 349 'zip': ('application/zip', 'zip', '.zip', None),
349 350 }
350 351
351 352 def check_perm(self, req, op):
352 353 '''Check permission for operation based on request data (including
353 354 authentication info. Return true if op allowed, else false.'''
354 355
355 356 if op == 'pull' and not self.allowpull:
356 357 raise ErrorResponse(HTTP_OK, '')
357 358 elif op == 'pull':
358 359 return
359 360
360 361 # enforce that you can only push using POST requests
361 362 if req.env['REQUEST_METHOD'] != 'POST':
362 363 msg = 'push requires POST request'
363 364 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
364 365
365 366 # require ssl by default for pushing, auth info cannot be sniffed
366 367 # and replayed
367 368 scheme = req.env.get('wsgi.url_scheme')
368 369 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
369 370 raise ErrorResponse(HTTP_OK, 'ssl required')
370 371
371 372 user = req.env.get('REMOTE_USER')
372 373
373 374 deny = self.configlist('web', 'deny_push')
374 375 if deny and (not user or deny == ['*'] or user in deny):
375 376 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
376 377
377 378 allow = self.configlist('web', 'allow_push')
378 379 result = allow and (allow == ['*'] or user in allow)
379 380 if not result:
380 381 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
General Comments 0
You need to be logged in to leave comments. Login now