##// END OF EJS Templates
hgweb: add compatibility code for old templates...
Alexis S. L. Carvalho -
r6379:d2bb66a8 default
parent child Browse files
Show More
@@ -1,978 +1,993 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 import os, mimetypes, re
9 import os, mimetypes, re, mimetools, cStringIO
10 10 from mercurial.node import hex, nullid, short
11 11 from mercurial.repo import RepoError
12 12 from mercurial import mdiff, ui, hg, util, archival, patch, hook
13 13 from mercurial import revlog, templater, templatefilters, changegroup
14 14 from common import get_mtime, style_map, paritygen, countgen, get_contact
15 15 from common import ErrorResponse
16 16 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
17 17 from request import wsgirequest
18 18 import webcommands, protocol
19 19
20 20 shortcuts = {
21 21 'cl': [('cmd', ['changelog']), ('rev', None)],
22 22 'sl': [('cmd', ['shortlog']), ('rev', None)],
23 23 'cs': [('cmd', ['changeset']), ('node', None)],
24 24 'f': [('cmd', ['file']), ('filenode', None)],
25 25 'fl': [('cmd', ['filelog']), ('filenode', None)],
26 26 'fd': [('cmd', ['filediff']), ('node', None)],
27 27 'fa': [('cmd', ['annotate']), ('filenode', None)],
28 28 'mf': [('cmd', ['manifest']), ('manifest', None)],
29 29 'ca': [('cmd', ['archive']), ('node', None)],
30 30 'tags': [('cmd', ['tags'])],
31 31 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
32 32 'static': [('cmd', ['static']), ('file', None)]
33 33 }
34 34
35 35 def _up(p):
36 36 if p[0] != "/":
37 37 p = "/" + p
38 38 if p[-1] == "/":
39 39 p = p[:-1]
40 40 up = os.path.dirname(p)
41 41 if up == "/":
42 42 return "/"
43 43 return up + "/"
44 44
45 45 def revnavgen(pos, pagelen, limit, nodefunc):
46 46 def seq(factor, limit=None):
47 47 if limit:
48 48 yield limit
49 49 if limit >= 20 and limit <= 40:
50 50 yield 50
51 51 else:
52 52 yield 1 * factor
53 53 yield 3 * factor
54 54 for f in seq(factor * 10):
55 55 yield f
56 56
57 57 def nav(**map):
58 58 l = []
59 59 last = 0
60 60 for f in seq(1, pagelen):
61 61 if f < pagelen or f <= last:
62 62 continue
63 63 if f > limit:
64 64 break
65 65 last = f
66 66 if pos + f < limit:
67 67 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
68 68 if pos - f >= 0:
69 69 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
70 70
71 71 try:
72 72 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
73 73
74 74 for label, node in l:
75 75 yield {"label": label, "node": node}
76 76
77 77 yield {"label": "tip", "node": "tip"}
78 78 except RepoError:
79 79 pass
80 80
81 81 return nav
82 82
83 83 class hgweb(object):
84 84 def __init__(self, repo, name=None):
85 85 if isinstance(repo, str):
86 86 parentui = ui.ui(report_untrusted=False, interactive=False)
87 87 self.repo = hg.repository(parentui, repo)
88 88 else:
89 89 self.repo = repo
90 90
91 91 hook.redirect(True)
92 92 self.mtime = -1
93 93 self.reponame = name
94 94 self.archives = 'zip', 'gz', 'bz2'
95 95 self.stripecount = 1
96 96 self._capabilities = None
97 97 # a repo owner may set web.templates in .hg/hgrc to get any file
98 98 # readable by the user running the CGI script
99 99 self.templatepath = self.config("web", "templates",
100 100 templater.templatepath(),
101 101 untrusted=False)
102 102
103 103 # The CGI scripts are often run by a user different from the repo owner.
104 104 # Trust the settings from the .hg/hgrc files by default.
105 105 def config(self, section, name, default=None, untrusted=True):
106 106 return self.repo.ui.config(section, name, default,
107 107 untrusted=untrusted)
108 108
109 109 def configbool(self, section, name, default=False, untrusted=True):
110 110 return self.repo.ui.configbool(section, name, default,
111 111 untrusted=untrusted)
112 112
113 113 def configlist(self, section, name, default=None, untrusted=True):
114 114 return self.repo.ui.configlist(section, name, default,
115 115 untrusted=untrusted)
116 116
117 117 def refresh(self):
118 118 mtime = get_mtime(self.repo.root)
119 119 if mtime != self.mtime:
120 120 self.mtime = mtime
121 121 self.repo = hg.repository(self.repo.ui, self.repo.root)
122 122 self.maxchanges = int(self.config("web", "maxchanges", 10))
123 123 self.stripecount = int(self.config("web", "stripes", 1))
124 124 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
125 125 self.maxfiles = int(self.config("web", "maxfiles", 10))
126 126 self.allowpull = self.configbool("web", "allowpull", True)
127 127 self.encoding = self.config("web", "encoding", util._encoding)
128 128 self._capabilities = None
129 129
130 130 def capabilities(self):
131 131 if self._capabilities is not None:
132 132 return self._capabilities
133 133 caps = ['lookup', 'changegroupsubset']
134 134 if self.configbool('server', 'uncompressed'):
135 135 caps.append('stream=%d' % self.repo.changelog.version)
136 136 if changegroup.bundlepriority:
137 137 caps.append('unbundle=%s' % ','.join(changegroup.bundlepriority))
138 138 self._capabilities = caps
139 139 return caps
140 140
141 141 def run(self):
142 142 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
143 143 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
144 144 import mercurial.hgweb.wsgicgi as wsgicgi
145 145 wsgicgi.launch(self)
146 146
147 147 def __call__(self, env, respond):
148 148 req = wsgirequest(env, respond)
149 149 self.run_wsgi(req)
150 150 return req
151 151
152 152 def run_wsgi(self, req):
153 153
154 154 self.refresh()
155 155
156 156 # expand form shortcuts
157 157
158 158 for k in shortcuts.iterkeys():
159 159 if k in req.form:
160 160 for name, value in shortcuts[k]:
161 161 if value is None:
162 162 value = req.form[k]
163 163 req.form[name] = value
164 164 del req.form[k]
165 165
166 166 # work with CGI variables to create coherent structure
167 167 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
168 168
169 169 req.url = req.env['SCRIPT_NAME']
170 170 if not req.url.endswith('/'):
171 171 req.url += '/'
172 172 if 'REPO_NAME' in req.env:
173 173 req.url += req.env['REPO_NAME'] + '/'
174 174
175 175 if req.env.get('PATH_INFO'):
176 176 parts = req.env.get('PATH_INFO').strip('/').split('/')
177 177 repo_parts = req.env.get('REPO_NAME', '').split('/')
178 178 if parts[:len(repo_parts)] == repo_parts:
179 179 parts = parts[len(repo_parts):]
180 180 query = '/'.join(parts)
181 181 else:
182 182 query = req.env['QUERY_STRING'].split('&', 1)[0]
183 183 query = query.split(';', 1)[0]
184 184
185 185 # translate user-visible url structure to internal structure
186 186
187 187 args = query.split('/', 2)
188 188 if 'cmd' not in req.form and args and args[0]:
189 189
190 190 cmd = args.pop(0)
191 191 style = cmd.rfind('-')
192 192 if style != -1:
193 193 req.form['style'] = [cmd[:style]]
194 194 cmd = cmd[style+1:]
195 195
196 196 # avoid accepting e.g. style parameter as command
197 197 if hasattr(webcommands, cmd) or hasattr(protocol, cmd):
198 198 req.form['cmd'] = [cmd]
199 199
200 200 if args and args[0]:
201 201 node = args.pop(0)
202 202 req.form['node'] = [node]
203 203 if args:
204 204 req.form['file'] = args
205 205
206 206 if cmd == 'static':
207 207 req.form['file'] = req.form['node']
208 208 elif cmd == 'archive':
209 209 fn = req.form['node'][0]
210 210 for type_, spec in self.archive_specs.iteritems():
211 211 ext = spec[2]
212 212 if fn.endswith(ext):
213 213 req.form['node'] = [fn[:-len(ext)]]
214 214 req.form['type'] = [type_]
215 215
216 216 # process this if it's a protocol request
217 217
218 218 cmd = req.form.get('cmd', [''])[0]
219 219 if cmd in protocol.__all__:
220 220 method = getattr(protocol, cmd)
221 221 method(self, req)
222 222 return
223 223
224 224 # process the web interface request
225 225
226 226 try:
227 227
228 228 tmpl = self.templater(req)
229 ctype = tmpl('mimetype', encoding=self.encoding)
230 ctype = templater.stringify(ctype)
229 try:
230 ctype = tmpl('mimetype', encoding=self.encoding)
231 ctype = templater.stringify(ctype)
232 except KeyError:
233 # old templates with inline HTTP headers?
234 if 'mimetype' in tmpl:
235 raise
236 header = tmpl('header', encoding=self.encoding)
237 header_file = cStringIO.StringIO(templater.stringify(header))
238 msg = mimetools.Message(header_file, 0)
239 ctype = msg['content-type']
231 240
232 241 if cmd == '':
233 242 req.form['cmd'] = [tmpl.cache['default']]
234 243 cmd = req.form['cmd'][0]
235 244
236 245 if cmd not in webcommands.__all__:
237 246 msg = 'no such method: %s' % cmd
238 247 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
239 248 elif cmd == 'file' and 'raw' in req.form.get('style', []):
240 249 self.ctype = ctype
241 250 content = webcommands.rawfile(self, req, tmpl)
242 251 else:
243 252 content = getattr(webcommands, cmd)(self, req, tmpl)
244 253 req.respond(HTTP_OK, ctype)
245 254
246 255 req.write(content)
247 256 del tmpl
248 257
249 258 except revlog.LookupError, err:
250 259 req.respond(HTTP_NOT_FOUND, ctype)
251 260 msg = str(err)
252 261 if 'manifest' not in msg:
253 262 msg = 'revision not found: %s' % err.name
254 263 req.write(tmpl('error', error=msg))
255 264 except (RepoError, revlog.RevlogError), inst:
256 265 req.respond(HTTP_SERVER_ERROR, ctype)
257 266 req.write(tmpl('error', error=str(inst)))
258 267 except ErrorResponse, inst:
259 268 req.respond(inst.code, ctype)
260 269 req.write(tmpl('error', error=inst.message))
261 270
262 271 def templater(self, req):
263 272
264 273 # determine scheme, port and server name
265 274 # this is needed to create absolute urls
266 275
267 276 proto = req.env.get('wsgi.url_scheme')
268 277 if proto == 'https':
269 278 proto = 'https'
270 279 default_port = "443"
271 280 else:
272 281 proto = 'http'
273 282 default_port = "80"
274 283
275 284 port = req.env["SERVER_PORT"]
276 285 port = port != default_port and (":" + port) or ""
277 286 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
278 287 staticurl = self.config("web", "staticurl") or req.url + 'static/'
279 288 if not staticurl.endswith('/'):
280 289 staticurl += '/'
281 290
282 291 # some functions for the templater
283 292
284 293 def header(**map):
285 yield tmpl('header', encoding=self.encoding, **map)
294 header = tmpl('header', encoding=self.encoding, **map)
295 if 'mimetype' not in tmpl:
296 # old template with inline HTTP headers
297 header_file = cStringIO.StringIO(templater.stringify(header))
298 msg = mimetools.Message(header_file, 0)
299 header = header_file.read()
300 yield header
286 301
287 302 def footer(**map):
288 303 yield tmpl("footer", **map)
289 304
290 305 def motd(**map):
291 306 yield self.config("web", "motd", "")
292 307
293 308 def sessionvars(**map):
294 309 fields = []
295 310 if 'style' in req.form:
296 311 style = req.form['style'][0]
297 312 if style != self.config('web', 'style', ''):
298 313 fields.append(('style', style))
299 314
300 315 separator = req.url[-1] == '?' and ';' or '?'
301 316 for name, value in fields:
302 317 yield dict(name=name, value=value, separator=separator)
303 318 separator = ';'
304 319
305 320 # figure out which style to use
306 321
307 322 style = self.config("web", "style", "")
308 323 if 'style' in req.form:
309 324 style = req.form['style'][0]
310 325 mapfile = style_map(self.templatepath, style)
311 326
312 327 if not self.reponame:
313 328 self.reponame = (self.config("web", "name")
314 329 or req.env.get('REPO_NAME')
315 330 or req.url.strip('/') or self.repo.root)
316 331
317 332 # create the templater
318 333
319 334 tmpl = templater.templater(mapfile, templatefilters.filters,
320 335 defaults={"url": req.url,
321 336 "staticurl": staticurl,
322 337 "urlbase": urlbase,
323 338 "repo": self.reponame,
324 339 "header": header,
325 340 "footer": footer,
326 341 "motd": motd,
327 342 "sessionvars": sessionvars
328 343 })
329 344 return tmpl
330 345
331 346 def archivelist(self, nodeid):
332 347 allowed = self.configlist("web", "allow_archive")
333 348 for i, spec in self.archive_specs.iteritems():
334 349 if i in allowed or self.configbool("web", "allow" + i):
335 350 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
336 351
337 352 def listfilediffs(self, tmpl, files, changeset):
338 353 for f in files[:self.maxfiles]:
339 354 yield tmpl("filedifflink", node=hex(changeset), file=f)
340 355 if len(files) > self.maxfiles:
341 356 yield tmpl("fileellipses")
342 357
343 358 def siblings(self, siblings=[], hiderev=None, **args):
344 359 siblings = [s for s in siblings if s.node() != nullid]
345 360 if len(siblings) == 1 and siblings[0].rev() == hiderev:
346 361 return
347 362 for s in siblings:
348 363 d = {'node': hex(s.node()), 'rev': s.rev()}
349 364 if hasattr(s, 'path'):
350 365 d['file'] = s.path()
351 366 d.update(args)
352 367 yield d
353 368
354 369 def renamelink(self, fl, node):
355 370 r = fl.renamed(node)
356 371 if r:
357 372 return [dict(file=r[0], node=hex(r[1]))]
358 373 return []
359 374
360 375 def nodetagsdict(self, node):
361 376 return [{"name": i} for i in self.repo.nodetags(node)]
362 377
363 378 def nodebranchdict(self, ctx):
364 379 branches = []
365 380 branch = ctx.branch()
366 381 # If this is an empty repo, ctx.node() == nullid,
367 382 # ctx.branch() == 'default', but branchtags() is
368 383 # an empty dict. Using dict.get avoids a traceback.
369 384 if self.repo.branchtags().get(branch) == ctx.node():
370 385 branches.append({"name": branch})
371 386 return branches
372 387
373 388 def nodeinbranch(self, ctx):
374 389 branches = []
375 390 branch = ctx.branch()
376 391 if branch != 'default' and self.repo.branchtags().get(branch) != ctx.node():
377 392 branches.append({"name": branch})
378 393 return branches
379 394
380 395 def nodebranchnodefault(self, ctx):
381 396 branches = []
382 397 branch = ctx.branch()
383 398 if branch != 'default':
384 399 branches.append({"name": branch})
385 400 return branches
386 401
387 402 def showtag(self, tmpl, t1, node=nullid, **args):
388 403 for t in self.repo.nodetags(node):
389 404 yield tmpl(t1, tag=t, **args)
390 405
391 406 def diff(self, tmpl, node1, node2, files):
392 407 def filterfiles(filters, files):
393 408 l = [x for x in files if x in filters]
394 409
395 410 for t in filters:
396 411 if t and t[-1] != os.sep:
397 412 t += os.sep
398 413 l += [x for x in files if x.startswith(t)]
399 414 return l
400 415
401 416 parity = paritygen(self.stripecount)
402 417 def diffblock(diff, f, fn):
403 418 yield tmpl("diffblock",
404 419 lines=prettyprintlines(diff),
405 420 parity=parity.next(),
406 421 file=f,
407 422 filenode=hex(fn or nullid))
408 423
409 424 blockcount = countgen()
410 425 def prettyprintlines(diff):
411 426 blockno = blockcount.next()
412 427 for lineno, l in enumerate(diff.splitlines(1)):
413 428 if blockno == 0:
414 429 lineno = lineno + 1
415 430 else:
416 431 lineno = "%d.%d" % (blockno, lineno + 1)
417 432 if l.startswith('+'):
418 433 ltype = "difflineplus"
419 434 elif l.startswith('-'):
420 435 ltype = "difflineminus"
421 436 elif l.startswith('@'):
422 437 ltype = "difflineat"
423 438 else:
424 439 ltype = "diffline"
425 440 yield tmpl(ltype,
426 441 line=l,
427 442 lineid="l%s" % lineno,
428 443 linenumber="% 8s" % lineno)
429 444
430 445 r = self.repo
431 446 c1 = r.changectx(node1)
432 447 c2 = r.changectx(node2)
433 448 date1 = util.datestr(c1.date())
434 449 date2 = util.datestr(c2.date())
435 450
436 451 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
437 452 if files:
438 453 modified, added, removed = map(lambda x: filterfiles(files, x),
439 454 (modified, added, removed))
440 455
441 456 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
442 457 for f in modified:
443 458 to = c1.filectx(f).data()
444 459 tn = c2.filectx(f).data()
445 460 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
446 461 opts=diffopts), f, tn)
447 462 for f in added:
448 463 to = None
449 464 tn = c2.filectx(f).data()
450 465 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
451 466 opts=diffopts), f, tn)
452 467 for f in removed:
453 468 to = c1.filectx(f).data()
454 469 tn = None
455 470 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
456 471 opts=diffopts), f, tn)
457 472
458 473 def changelog(self, tmpl, ctx, shortlog=False):
459 474 def changelist(limit=0,**map):
460 475 cl = self.repo.changelog
461 476 l = [] # build a list in forward order for efficiency
462 477 for i in xrange(start, end):
463 478 ctx = self.repo.changectx(i)
464 479 n = ctx.node()
465 480 showtags = self.showtag(tmpl, 'changelogtag', n)
466 481
467 482 l.insert(0, {"parity": parity.next(),
468 483 "author": ctx.user(),
469 484 "parent": self.siblings(ctx.parents(), i - 1),
470 485 "child": self.siblings(ctx.children(), i + 1),
471 486 "changelogtag": showtags,
472 487 "desc": ctx.description(),
473 488 "date": ctx.date(),
474 489 "files": self.listfilediffs(tmpl, ctx.files(), n),
475 490 "rev": i,
476 491 "node": hex(n),
477 492 "tags": self.nodetagsdict(n),
478 493 "inbranch": self.nodeinbranch(ctx),
479 494 "branches": self.nodebranchdict(ctx)})
480 495
481 496 if limit > 0:
482 497 l = l[:limit]
483 498
484 499 for e in l:
485 500 yield e
486 501
487 502 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
488 503 cl = self.repo.changelog
489 504 count = cl.count()
490 505 pos = ctx.rev()
491 506 start = max(0, pos - maxchanges + 1)
492 507 end = min(count, start + maxchanges)
493 508 pos = end - 1
494 509 parity = paritygen(self.stripecount, offset=start-end)
495 510
496 511 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
497 512
498 513 return tmpl(shortlog and 'shortlog' or 'changelog',
499 514 changenav=changenav,
500 515 node=hex(cl.tip()),
501 516 rev=pos, changesets=count,
502 517 entries=lambda **x: changelist(limit=0,**x),
503 518 latestentry=lambda **x: changelist(limit=1,**x),
504 519 archives=self.archivelist("tip"))
505 520
506 521 def search(self, tmpl, query):
507 522
508 523 def changelist(**map):
509 524 cl = self.repo.changelog
510 525 count = 0
511 526 qw = query.lower().split()
512 527
513 528 def revgen():
514 529 for i in xrange(cl.count() - 1, 0, -100):
515 530 l = []
516 531 for j in xrange(max(0, i - 100), i + 1):
517 532 ctx = self.repo.changectx(j)
518 533 l.append(ctx)
519 534 l.reverse()
520 535 for e in l:
521 536 yield e
522 537
523 538 for ctx in revgen():
524 539 miss = 0
525 540 for q in qw:
526 541 if not (q in ctx.user().lower() or
527 542 q in ctx.description().lower() or
528 543 q in " ".join(ctx.files()).lower()):
529 544 miss = 1
530 545 break
531 546 if miss:
532 547 continue
533 548
534 549 count += 1
535 550 n = ctx.node()
536 551 showtags = self.showtag(tmpl, 'changelogtag', n)
537 552
538 553 yield tmpl('searchentry',
539 554 parity=parity.next(),
540 555 author=ctx.user(),
541 556 parent=self.siblings(ctx.parents()),
542 557 child=self.siblings(ctx.children()),
543 558 changelogtag=showtags,
544 559 desc=ctx.description(),
545 560 date=ctx.date(),
546 561 files=self.listfilediffs(tmpl, ctx.files(), n),
547 562 rev=ctx.rev(),
548 563 node=hex(n),
549 564 tags=self.nodetagsdict(n),
550 565 inbranch=self.nodeinbranch(ctx),
551 566 branches=self.nodebranchdict(ctx))
552 567
553 568 if count >= self.maxchanges:
554 569 break
555 570
556 571 cl = self.repo.changelog
557 572 parity = paritygen(self.stripecount)
558 573
559 574 return tmpl('search',
560 575 query=query,
561 576 node=hex(cl.tip()),
562 577 entries=changelist,
563 578 archives=self.archivelist("tip"))
564 579
565 580 def changeset(self, tmpl, ctx):
566 581 n = ctx.node()
567 582 showtags = self.showtag(tmpl, 'changesettag', n)
568 583 parents = ctx.parents()
569 584 p1 = parents[0].node()
570 585
571 586 files = []
572 587 parity = paritygen(self.stripecount)
573 588 for f in ctx.files():
574 589 files.append(tmpl("filenodelink",
575 590 node=hex(n), file=f,
576 591 parity=parity.next()))
577 592
578 593 def diff(**map):
579 594 yield self.diff(tmpl, p1, n, None)
580 595
581 596 return tmpl('changeset',
582 597 diff=diff,
583 598 rev=ctx.rev(),
584 599 node=hex(n),
585 600 parent=self.siblings(parents),
586 601 child=self.siblings(ctx.children()),
587 602 changesettag=showtags,
588 603 author=ctx.user(),
589 604 desc=ctx.description(),
590 605 date=ctx.date(),
591 606 files=files,
592 607 archives=self.archivelist(hex(n)),
593 608 tags=self.nodetagsdict(n),
594 609 branch=self.nodebranchnodefault(ctx),
595 610 inbranch=self.nodeinbranch(ctx),
596 611 branches=self.nodebranchdict(ctx))
597 612
598 613 def filelog(self, tmpl, fctx):
599 614 f = fctx.path()
600 615 fl = fctx.filelog()
601 616 count = fl.count()
602 617 pagelen = self.maxshortchanges
603 618 pos = fctx.filerev()
604 619 start = max(0, pos - pagelen + 1)
605 620 end = min(count, start + pagelen)
606 621 pos = end - 1
607 622 parity = paritygen(self.stripecount, offset=start-end)
608 623
609 624 def entries(limit=0, **map):
610 625 l = []
611 626
612 627 for i in xrange(start, end):
613 628 ctx = fctx.filectx(i)
614 629 n = fl.node(i)
615 630
616 631 l.insert(0, {"parity": parity.next(),
617 632 "filerev": i,
618 633 "file": f,
619 634 "node": hex(ctx.node()),
620 635 "author": ctx.user(),
621 636 "date": ctx.date(),
622 637 "rename": self.renamelink(fl, n),
623 638 "parent": self.siblings(fctx.parents()),
624 639 "child": self.siblings(fctx.children()),
625 640 "desc": ctx.description()})
626 641
627 642 if limit > 0:
628 643 l = l[:limit]
629 644
630 645 for e in l:
631 646 yield e
632 647
633 648 nodefunc = lambda x: fctx.filectx(fileid=x)
634 649 nav = revnavgen(pos, pagelen, count, nodefunc)
635 650 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
636 651 entries=lambda **x: entries(limit=0, **x),
637 652 latestentry=lambda **x: entries(limit=1, **x))
638 653
639 654 def filerevision(self, tmpl, fctx):
640 655 f = fctx.path()
641 656 text = fctx.data()
642 657 fl = fctx.filelog()
643 658 n = fctx.filenode()
644 659 parity = paritygen(self.stripecount)
645 660
646 661 if util.binary(text):
647 662 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
648 663 text = '(binary:%s)' % mt
649 664
650 665 def lines():
651 666 for lineno, t in enumerate(text.splitlines(1)):
652 667 yield {"line": t,
653 668 "lineid": "l%d" % (lineno + 1),
654 669 "linenumber": "% 6d" % (lineno + 1),
655 670 "parity": parity.next()}
656 671
657 672 return tmpl("filerevision",
658 673 file=f,
659 674 path=_up(f),
660 675 text=lines(),
661 676 rev=fctx.rev(),
662 677 node=hex(fctx.node()),
663 678 author=fctx.user(),
664 679 date=fctx.date(),
665 680 desc=fctx.description(),
666 681 branch=self.nodebranchnodefault(fctx),
667 682 parent=self.siblings(fctx.parents()),
668 683 child=self.siblings(fctx.children()),
669 684 rename=self.renamelink(fl, n),
670 685 permissions=fctx.manifest().flags(f))
671 686
672 687 def fileannotate(self, tmpl, fctx):
673 688 f = fctx.path()
674 689 n = fctx.filenode()
675 690 fl = fctx.filelog()
676 691 parity = paritygen(self.stripecount)
677 692
678 693 def annotate(**map):
679 694 last = None
680 695 if util.binary(fctx.data()):
681 696 mt = (mimetypes.guess_type(fctx.path())[0]
682 697 or 'application/octet-stream')
683 698 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
684 699 '(binary:%s)' % mt)])
685 700 else:
686 701 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
687 702 for lineno, ((f, targetline), l) in lines:
688 703 fnode = f.filenode()
689 704 name = self.repo.ui.shortuser(f.user())
690 705
691 706 if last != fnode:
692 707 last = fnode
693 708
694 709 yield {"parity": parity.next(),
695 710 "node": hex(f.node()),
696 711 "rev": f.rev(),
697 712 "author": name,
698 713 "file": f.path(),
699 714 "targetline": targetline,
700 715 "line": l,
701 716 "lineid": "l%d" % (lineno + 1),
702 717 "linenumber": "% 6d" % (lineno + 1)}
703 718
704 719 return tmpl("fileannotate",
705 720 file=f,
706 721 annotate=annotate,
707 722 path=_up(f),
708 723 rev=fctx.rev(),
709 724 node=hex(fctx.node()),
710 725 author=fctx.user(),
711 726 date=fctx.date(),
712 727 desc=fctx.description(),
713 728 rename=self.renamelink(fl, n),
714 729 branch=self.nodebranchnodefault(fctx),
715 730 parent=self.siblings(fctx.parents()),
716 731 child=self.siblings(fctx.children()),
717 732 permissions=fctx.manifest().flags(f))
718 733
719 734 def manifest(self, tmpl, ctx, path):
720 735 mf = ctx.manifest()
721 736 node = ctx.node()
722 737
723 738 files = {}
724 739 parity = paritygen(self.stripecount)
725 740
726 741 if path and path[-1] != "/":
727 742 path += "/"
728 743 l = len(path)
729 744 abspath = "/" + path
730 745
731 746 for f, n in mf.items():
732 747 if f[:l] != path:
733 748 continue
734 749 remain = f[l:]
735 750 if "/" in remain:
736 751 short = remain[:remain.index("/") + 1] # bleah
737 752 files[short] = (f, None)
738 753 else:
739 754 short = os.path.basename(remain)
740 755 files[short] = (f, n)
741 756
742 757 if not files:
743 758 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
744 759
745 760 def filelist(**map):
746 761 fl = files.keys()
747 762 fl.sort()
748 763 for f in fl:
749 764 full, fnode = files[f]
750 765 if not fnode:
751 766 continue
752 767
753 768 fctx = ctx.filectx(full)
754 769 yield {"file": full,
755 770 "parity": parity.next(),
756 771 "basename": f,
757 772 "date": fctx.changectx().date(),
758 773 "size": fctx.size(),
759 774 "permissions": mf.flags(full)}
760 775
761 776 def dirlist(**map):
762 777 fl = files.keys()
763 778 fl.sort()
764 779 for f in fl:
765 780 full, fnode = files[f]
766 781 if fnode:
767 782 continue
768 783
769 784 yield {"parity": parity.next(),
770 785 "path": "%s%s" % (abspath, f),
771 786 "basename": f[:-1]}
772 787
773 788 return tmpl("manifest",
774 789 rev=ctx.rev(),
775 790 node=hex(node),
776 791 path=abspath,
777 792 up=_up(abspath),
778 793 upparity=parity.next(),
779 794 fentries=filelist,
780 795 dentries=dirlist,
781 796 archives=self.archivelist(hex(node)),
782 797 tags=self.nodetagsdict(node),
783 798 inbranch=self.nodeinbranch(ctx),
784 799 branches=self.nodebranchdict(ctx))
785 800
786 801 def tags(self, tmpl):
787 802 i = self.repo.tagslist()
788 803 i.reverse()
789 804 parity = paritygen(self.stripecount)
790 805
791 806 def entries(notip=False,limit=0, **map):
792 807 count = 0
793 808 for k, n in i:
794 809 if notip and k == "tip":
795 810 continue
796 811 if limit > 0 and count >= limit:
797 812 continue
798 813 count = count + 1
799 814 yield {"parity": parity.next(),
800 815 "tag": k,
801 816 "date": self.repo.changectx(n).date(),
802 817 "node": hex(n)}
803 818
804 819 return tmpl("tags",
805 820 node=hex(self.repo.changelog.tip()),
806 821 entries=lambda **x: entries(False,0, **x),
807 822 entriesnotip=lambda **x: entries(True,0, **x),
808 823 latestentry=lambda **x: entries(True,1, **x))
809 824
810 825 def summary(self, tmpl):
811 826 i = self.repo.tagslist()
812 827 i.reverse()
813 828
814 829 def tagentries(**map):
815 830 parity = paritygen(self.stripecount)
816 831 count = 0
817 832 for k, n in i:
818 833 if k == "tip": # skip tip
819 834 continue;
820 835
821 836 count += 1
822 837 if count > 10: # limit to 10 tags
823 838 break;
824 839
825 840 yield tmpl("tagentry",
826 841 parity=parity.next(),
827 842 tag=k,
828 843 node=hex(n),
829 844 date=self.repo.changectx(n).date())
830 845
831 846
832 847 def branches(**map):
833 848 parity = paritygen(self.stripecount)
834 849
835 850 b = self.repo.branchtags()
836 851 l = [(-self.repo.changelog.rev(n), n, t) for t, n in b.items()]
837 852 l.sort()
838 853
839 854 for r,n,t in l:
840 855 ctx = self.repo.changectx(n)
841 856
842 857 yield {'parity': parity.next(),
843 858 'branch': t,
844 859 'node': hex(n),
845 860 'date': ctx.date()}
846 861
847 862 def changelist(**map):
848 863 parity = paritygen(self.stripecount, offset=start-end)
849 864 l = [] # build a list in forward order for efficiency
850 865 for i in xrange(start, end):
851 866 ctx = self.repo.changectx(i)
852 867 n = ctx.node()
853 868 hn = hex(n)
854 869
855 870 l.insert(0, tmpl(
856 871 'shortlogentry',
857 872 parity=parity.next(),
858 873 author=ctx.user(),
859 874 desc=ctx.description(),
860 875 date=ctx.date(),
861 876 rev=i,
862 877 node=hn,
863 878 tags=self.nodetagsdict(n),
864 879 inbranch=self.nodeinbranch(ctx),
865 880 branches=self.nodebranchdict(ctx)))
866 881
867 882 yield l
868 883
869 884 cl = self.repo.changelog
870 885 count = cl.count()
871 886 start = max(0, count - self.maxchanges)
872 887 end = min(count, start + self.maxchanges)
873 888
874 889 return tmpl("summary",
875 890 desc=self.config("web", "description", "unknown"),
876 891 owner=get_contact(self.config) or "unknown",
877 892 lastchange=cl.read(cl.tip())[2],
878 893 tags=tagentries,
879 894 branches=branches,
880 895 shortlog=changelist,
881 896 node=hex(cl.tip()),
882 897 archives=self.archivelist("tip"))
883 898
884 899 def filediff(self, tmpl, fctx):
885 900 n = fctx.node()
886 901 path = fctx.path()
887 902 parents = fctx.parents()
888 903 p1 = parents and parents[0].node() or nullid
889 904
890 905 def diff(**map):
891 906 yield self.diff(tmpl, p1, n, [path])
892 907
893 908 return tmpl("filediff",
894 909 file=path,
895 910 node=hex(n),
896 911 rev=fctx.rev(),
897 912 branch=self.nodebranchnodefault(fctx),
898 913 parent=self.siblings(parents),
899 914 child=self.siblings(fctx.children()),
900 915 diff=diff)
901 916
902 917 archive_specs = {
903 918 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
904 919 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
905 920 'zip': ('application/zip', 'zip', '.zip', None),
906 921 }
907 922
908 923 def archive(self, tmpl, req, key, type_):
909 924 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
910 925 cnode = self.repo.lookup(key)
911 926 arch_version = key
912 927 if cnode == key or key == 'tip':
913 928 arch_version = short(cnode)
914 929 name = "%s-%s" % (reponame, arch_version)
915 930 mimetype, artype, extension, encoding = self.archive_specs[type_]
916 931 headers = [
917 932 ('Content-Type', mimetype),
918 933 ('Content-Disposition', 'attachment; filename=%s%s' %
919 934 (name, extension))
920 935 ]
921 936 if encoding:
922 937 headers.append(('Content-Encoding', encoding))
923 938 req.header(headers)
924 939 req.respond(HTTP_OK)
925 940 archival.archive(self.repo, req, cnode, artype, prefix=name)
926 941
927 942 # add tags to things
928 943 # tags -> list of changesets corresponding to tags
929 944 # find tag, changeset, file
930 945
931 946 def cleanpath(self, path):
932 947 path = path.lstrip('/')
933 948 return util.canonpath(self.repo.root, '', path)
934 949
935 950 def changectx(self, req):
936 951 if 'node' in req.form:
937 952 changeid = req.form['node'][0]
938 953 elif 'manifest' in req.form:
939 954 changeid = req.form['manifest'][0]
940 955 else:
941 956 changeid = self.repo.changelog.count() - 1
942 957
943 958 try:
944 959 ctx = self.repo.changectx(changeid)
945 960 except RepoError:
946 961 man = self.repo.manifest
947 962 mn = man.lookup(changeid)
948 963 ctx = self.repo.changectx(man.linkrev(mn))
949 964
950 965 return ctx
951 966
952 967 def filectx(self, req):
953 968 path = self.cleanpath(req.form['file'][0])
954 969 if 'node' in req.form:
955 970 changeid = req.form['node'][0]
956 971 else:
957 972 changeid = req.form['filenode'][0]
958 973 try:
959 974 ctx = self.repo.changectx(changeid)
960 975 fctx = ctx.filectx(path)
961 976 except RepoError:
962 977 fctx = self.repo.filectx(path, fileid=changeid)
963 978
964 979 return fctx
965 980
966 981 def check_perm(self, req, op, default):
967 982 '''check permission for operation based on user auth.
968 983 return true if op allowed, else false.
969 984 default is policy to use if no config given.'''
970 985
971 986 user = req.env.get('REMOTE_USER')
972 987
973 988 deny = self.configlist('web', 'deny_' + op)
974 989 if deny and (not user or deny == ['*'] or user in deny):
975 990 return False
976 991
977 992 allow = self.configlist('web', 'allow_' + op)
978 993 return (allow and (allow == ['*'] or user in allow)) or default
@@ -1,288 +1,303 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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 import os
9 import os, mimetools, cStringIO
10 10 from mercurial.i18n import gettext as _
11 11 from mercurial.repo import RepoError
12 12 from mercurial import ui, hg, util, templater, templatefilters
13 13 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen,\
14 14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 15 from hgweb_mod import hgweb
16 16 from request import wsgirequest
17 17
18 18 # This is a stopgap
19 19 class hgwebdir(object):
20 20 def __init__(self, config, parentui=None):
21 21 def cleannames(items):
22 22 return [(util.pconvert(name).strip('/'), path)
23 23 for name, path in items]
24 24
25 25 self.parentui = parentui or ui.ui(report_untrusted=False,
26 26 interactive = False)
27 27 self.motd = None
28 28 self.style = None
29 29 self.stripecount = None
30 30 self.repos_sorted = ('name', False)
31 31 self._baseurl = None
32 32 if isinstance(config, (list, tuple)):
33 33 self.repos = cleannames(config)
34 34 self.repos_sorted = ('', False)
35 35 elif isinstance(config, dict):
36 36 self.repos = cleannames(config.items())
37 37 self.repos.sort()
38 38 else:
39 39 if isinstance(config, util.configparser):
40 40 cp = config
41 41 else:
42 42 cp = util.configparser()
43 43 cp.read(config)
44 44 self.repos = []
45 45 if cp.has_section('web'):
46 46 if cp.has_option('web', 'motd'):
47 47 self.motd = cp.get('web', 'motd')
48 48 if cp.has_option('web', 'style'):
49 49 self.style = cp.get('web', 'style')
50 50 if cp.has_option('web', 'stripes'):
51 51 self.stripecount = int(cp.get('web', 'stripes'))
52 52 if cp.has_option('web', 'baseurl'):
53 53 self._baseurl = cp.get('web', 'baseurl')
54 54 if cp.has_section('paths'):
55 55 self.repos.extend(cleannames(cp.items('paths')))
56 56 if cp.has_section('collections'):
57 57 for prefix, root in cp.items('collections'):
58 58 for path in util.walkrepos(root, followsym=True):
59 59 repo = os.path.normpath(path)
60 60 name = repo
61 61 if name.startswith(prefix):
62 62 name = name[len(prefix):]
63 63 self.repos.append((name.lstrip(os.sep), repo))
64 64 self.repos.sort()
65 65
66 66 def run(self):
67 67 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
68 68 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
69 69 import mercurial.hgweb.wsgicgi as wsgicgi
70 70 wsgicgi.launch(self)
71 71
72 72 def __call__(self, env, respond):
73 73 req = wsgirequest(env, respond)
74 74 self.run_wsgi(req)
75 75 return req
76 76
77 77 def run_wsgi(self, req):
78 78
79 79 try:
80 80 try:
81 81
82 82 virtual = req.env.get("PATH_INFO", "").strip('/')
83 83 tmpl = self.templater(req)
84 ctype = tmpl('mimetype', encoding=util._encoding)
85 ctype = templater.stringify(ctype)
84 try:
85 ctype = tmpl('mimetype', encoding=util._encoding)
86 ctype = templater.stringify(ctype)
87 except KeyError:
88 # old templates with inline HTTP headers?
89 if 'mimetype' in tmpl:
90 raise
91 header = tmpl('header', encoding=util._encoding)
92 header_file = cStringIO.StringIO(templater.stringify(header))
93 msg = mimetools.Message(header_file, 0)
94 ctype = msg['content-type']
86 95
87 96 # a static file
88 97 if virtual.startswith('static/') or 'static' in req.form:
89 98 static = os.path.join(templater.templatepath(), 'static')
90 99 if virtual.startswith('static/'):
91 100 fname = virtual[7:]
92 101 else:
93 102 fname = req.form['static'][0]
94 103 req.write(staticfile(static, fname, req))
95 104 return
96 105
97 106 # top-level index
98 107 elif not virtual:
99 108 req.respond(HTTP_OK, ctype)
100 109 req.write(self.makeindex(req, tmpl))
101 110 return
102 111
103 112 # nested indexes and hgwebs
104 113
105 114 repos = dict(self.repos)
106 115 while virtual:
107 116 real = repos.get(virtual)
108 117 if real:
109 118 req.env['REPO_NAME'] = virtual
110 119 try:
111 120 repo = hg.repository(self.parentui, real)
112 121 hgweb(repo).run_wsgi(req)
113 122 return
114 123 except IOError, inst:
115 124 msg = inst.strerror
116 125 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
117 126 except RepoError, inst:
118 127 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
119 128
120 129 # browse subdirectories
121 130 subdir = virtual + '/'
122 131 if [r for r in repos if r.startswith(subdir)]:
123 132 req.respond(HTTP_OK, ctype)
124 133 req.write(self.makeindex(req, tmpl, subdir))
125 134 return
126 135
127 136 up = virtual.rfind('/')
128 137 if up < 0:
129 138 break
130 139 virtual = virtual[:up]
131 140
132 141 # prefixes not found
133 142 req.respond(HTTP_NOT_FOUND, ctype)
134 143 req.write(tmpl("notfound", repo=virtual))
135 144
136 145 except ErrorResponse, err:
137 146 req.respond(err.code, ctype)
138 147 req.write(tmpl('error', error=err.message or ''))
139 148 finally:
140 149 tmpl = None
141 150
142 151 def makeindex(self, req, tmpl, subdir=""):
143 152
144 153 def archivelist(ui, nodeid, url):
145 154 allowed = ui.configlist("web", "allow_archive", untrusted=True)
146 155 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
147 156 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
148 157 untrusted=True):
149 158 yield {"type" : i[0], "extension": i[1],
150 159 "node": nodeid, "url": url}
151 160
152 161 def entries(sortcolumn="", descending=False, subdir="", **map):
153 162 def sessionvars(**map):
154 163 fields = []
155 164 if 'style' in req.form:
156 165 style = req.form['style'][0]
157 166 if style != get('web', 'style', ''):
158 167 fields.append(('style', style))
159 168
160 169 separator = url[-1] == '?' and ';' or '?'
161 170 for name, value in fields:
162 171 yield dict(name=name, value=value, separator=separator)
163 172 separator = ';'
164 173
165 174 rows = []
166 175 parity = paritygen(self.stripecount)
167 176 for name, path in self.repos:
168 177 if not name.startswith(subdir):
169 178 continue
170 179 name = name[len(subdir):]
171 180
172 181 u = ui.ui(parentui=self.parentui)
173 182 try:
174 183 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
175 184 except Exception, e:
176 185 u.warn(_('error reading %s/.hg/hgrc: %s\n' % (path, e)))
177 186 continue
178 187 def get(section, name, default=None):
179 188 return u.config(section, name, default, untrusted=True)
180 189
181 190 if u.configbool("web", "hidden", untrusted=True):
182 191 continue
183 192
184 193 parts = [req.env['PATH_INFO'].rstrip('/'), name]
185 194 if req.env['SCRIPT_NAME']:
186 195 parts.insert(0, req.env['SCRIPT_NAME'])
187 196 url = ('/'.join(parts).replace("//", "/")) + '/'
188 197
189 198 # update time with local timezone
190 199 try:
191 200 d = (get_mtime(path), util.makedate()[1])
192 201 except OSError:
193 202 continue
194 203
195 204 contact = get_contact(get)
196 205 description = get("web", "description", "")
197 206 name = get("web", "name", name)
198 207 row = dict(contact=contact or "unknown",
199 208 contact_sort=contact.upper() or "unknown",
200 209 name=name,
201 210 name_sort=name,
202 211 url=url,
203 212 description=description or "unknown",
204 213 description_sort=description.upper() or "unknown",
205 214 lastchange=d,
206 215 lastchange_sort=d[1]-d[0],
207 216 sessionvars=sessionvars,
208 217 archives=archivelist(u, "tip", url))
209 218 if (not sortcolumn
210 219 or (sortcolumn, descending) == self.repos_sorted):
211 220 # fast path for unsorted output
212 221 row['parity'] = parity.next()
213 222 yield row
214 223 else:
215 224 rows.append((row["%s_sort" % sortcolumn], row))
216 225 if rows:
217 226 rows.sort()
218 227 if descending:
219 228 rows.reverse()
220 229 for key, row in rows:
221 230 row['parity'] = parity.next()
222 231 yield row
223 232
224 233 sortable = ["name", "description", "contact", "lastchange"]
225 234 sortcolumn, descending = self.repos_sorted
226 235 if 'sort' in req.form:
227 236 sortcolumn = req.form['sort'][0]
228 237 descending = sortcolumn.startswith('-')
229 238 if descending:
230 239 sortcolumn = sortcolumn[1:]
231 240 if sortcolumn not in sortable:
232 241 sortcolumn = ""
233 242
234 243 sort = [("sort_%s" % column,
235 244 "%s%s" % ((not descending and column == sortcolumn)
236 245 and "-" or "", column))
237 246 for column in sortable]
238 247
239 248 if self._baseurl is not None:
240 249 req.env['SCRIPT_NAME'] = self._baseurl
241 250
242 251 return tmpl("index", entries=entries, subdir=subdir,
243 252 sortcolumn=sortcolumn, descending=descending,
244 253 **dict(sort))
245 254
246 255 def templater(self, req):
247 256
248 257 def header(**map):
249 yield tmpl('header', encoding=util._encoding, **map)
258 header = tmpl('header', encoding=util._encoding, **map)
259 if 'mimetype' not in tmpl:
260 # old template with inline HTTP headers
261 header_file = cStringIO.StringIO(templater.stringify(header))
262 msg = mimetools.Message(header_file, 0)
263 header = header_file.read()
264 yield header
250 265
251 266 def footer(**map):
252 267 yield tmpl("footer", **map)
253 268
254 269 def motd(**map):
255 270 if self.motd is not None:
256 271 yield self.motd
257 272 else:
258 273 yield config('web', 'motd', '')
259 274
260 275 def config(section, name, default=None, untrusted=True):
261 276 return self.parentui.config(section, name, default, untrusted)
262 277
263 278 if self._baseurl is not None:
264 279 req.env['SCRIPT_NAME'] = self._baseurl
265 280
266 281 url = req.env.get('SCRIPT_NAME', '')
267 282 if not url.endswith('/'):
268 283 url += '/'
269 284
270 285 staticurl = config('web', 'staticurl') or url + 'static/'
271 286 if not staticurl.endswith('/'):
272 287 staticurl += '/'
273 288
274 289 style = self.style
275 290 if style is None:
276 291 style = config('web', 'style', '')
277 292 if 'style' in req.form:
278 293 style = req.form['style'][0]
279 294 if self.stripecount is None:
280 295 self.stripecount = int(config('web', 'stripes', 1))
281 296 mapfile = style_map(templater.templatepath(), style)
282 297 tmpl = templater.templater(mapfile, templatefilters.filters,
283 298 defaults={"header": header,
284 299 "footer": footer,
285 300 "motd": motd,
286 301 "url": url,
287 302 "staticurl": staticurl})
288 303 return tmpl
General Comments 0
You need to be logged in to leave comments. Login now