##// END OF EJS Templates
hgweb: establish class for holding per request context...
Gregory Szorc -
r26134:e0a6908f default
parent child Browse files
Show More
@@ -1,447 +1,467 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 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os, re
9 import os, re
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
10 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 from mercurial.templatefilters import websub
11 from mercurial.templatefilters import websub
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from common import get_stat, ErrorResponse, permhooks, caching
13 from common import get_stat, ErrorResponse, permhooks, caching
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 from request import wsgirequest
16 from request import wsgirequest
17 import webcommands, protocol, webutil
17 import webcommands, protocol, webutil
18
18
19 perms = {
19 perms = {
20 'changegroup': 'pull',
20 'changegroup': 'pull',
21 'changegroupsubset': 'pull',
21 'changegroupsubset': 'pull',
22 'getbundle': 'pull',
22 'getbundle': 'pull',
23 'stream_out': 'pull',
23 'stream_out': 'pull',
24 'listkeys': 'pull',
24 'listkeys': 'pull',
25 'unbundle': 'push',
25 'unbundle': 'push',
26 'pushkey': 'push',
26 'pushkey': 'push',
27 }
27 }
28
28
29 ## Files of interest
29 ## Files of interest
30 # Used to check if the repository has changed looking at mtime and size of
30 # Used to check if the repository has changed looking at mtime and size of
31 # theses files. This should probably be relocated a bit higher in core.
31 # theses files. This should probably be relocated a bit higher in core.
32 foi = [('spath', '00changelog.i'),
32 foi = [('spath', '00changelog.i'),
33 ('spath', 'phaseroots'), # ! phase can change content at the same size
33 ('spath', 'phaseroots'), # ! phase can change content at the same size
34 ('spath', 'obsstore'),
34 ('spath', 'obsstore'),
35 ('path', 'bookmarks'), # ! bookmark can change content at the same size
35 ('path', 'bookmarks'), # ! bookmark can change content at the same size
36 ]
36 ]
37
37
38 def makebreadcrumb(url, prefix=''):
38 def makebreadcrumb(url, prefix=''):
39 '''Return a 'URL breadcrumb' list
39 '''Return a 'URL breadcrumb' list
40
40
41 A 'URL breadcrumb' is a list of URL-name pairs,
41 A 'URL breadcrumb' is a list of URL-name pairs,
42 corresponding to each of the path items on a URL.
42 corresponding to each of the path items on a URL.
43 This can be used to create path navigation entries.
43 This can be used to create path navigation entries.
44 '''
44 '''
45 if url.endswith('/'):
45 if url.endswith('/'):
46 url = url[:-1]
46 url = url[:-1]
47 if prefix:
47 if prefix:
48 url = '/' + prefix + url
48 url = '/' + prefix + url
49 relpath = url
49 relpath = url
50 if relpath.startswith('/'):
50 if relpath.startswith('/'):
51 relpath = relpath[1:]
51 relpath = relpath[1:]
52
52
53 breadcrumb = []
53 breadcrumb = []
54 urlel = url
54 urlel = url
55 pathitems = [''] + relpath.split('/')
55 pathitems = [''] + relpath.split('/')
56 for pathel in reversed(pathitems):
56 for pathel in reversed(pathitems):
57 if not pathel or not urlel:
57 if not pathel or not urlel:
58 break
58 break
59 breadcrumb.append({'url': urlel, 'name': pathel})
59 breadcrumb.append({'url': urlel, 'name': pathel})
60 urlel = os.path.dirname(urlel)
60 urlel = os.path.dirname(urlel)
61 return reversed(breadcrumb)
61 return reversed(breadcrumb)
62
62
63
63
64 class requestcontext(object):
65 """Holds state/context for an individual request.
66
67 Servers can be multi-threaded. Holding state on the WSGI application
68 is prone to race conditions. Instances of this class exist to hold
69 mutable and race-free state for requests.
70 """
71 def __init__(self, app):
72 object.__setattr__(self, 'app', app)
73 object.__setattr__(self, 'repo', app.repo)
74
75 # Proxy unknown reads and writes to the application instance
76 # until everything is moved to us.
77 def __getattr__(self, name):
78 return getattr(self.app, name)
79
80 def __setattr__(self, name, value):
81 return setattr(self.app, name, value)
82
64 class hgweb(object):
83 class hgweb(object):
65 """HTTP server for individual repositories.
84 """HTTP server for individual repositories.
66
85
67 Instances of this class serve HTTP responses for a particular
86 Instances of this class serve HTTP responses for a particular
68 repository.
87 repository.
69
88
70 Instances are typically used as WSGI applications.
89 Instances are typically used as WSGI applications.
71
90
72 Some servers are multi-threaded. On these servers, there may
91 Some servers are multi-threaded. On these servers, there may
73 be multiple active threads inside __call__.
92 be multiple active threads inside __call__.
74 """
93 """
75 def __init__(self, repo, name=None, baseui=None):
94 def __init__(self, repo, name=None, baseui=None):
76 if isinstance(repo, str):
95 if isinstance(repo, str):
77 if baseui:
96 if baseui:
78 u = baseui.copy()
97 u = baseui.copy()
79 else:
98 else:
80 u = ui.ui()
99 u = ui.ui()
81 r = hg.repository(u, repo)
100 r = hg.repository(u, repo)
82 else:
101 else:
83 # we trust caller to give us a private copy
102 # we trust caller to give us a private copy
84 r = repo
103 r = repo
85
104
86 r = self._getview(r)
105 r = self._getview(r)
87 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
106 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
88 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
107 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
89 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
108 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
90 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
109 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
91 # displaying bundling progress bar while serving feel wrong and may
110 # displaying bundling progress bar while serving feel wrong and may
92 # break some wsgi implementation.
111 # break some wsgi implementation.
93 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
112 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
94 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
113 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
95 self.repo = r
114 self.repo = r
96 hook.redirect(True)
115 hook.redirect(True)
97 self.repostate = ((-1, -1), (-1, -1))
116 self.repostate = ((-1, -1), (-1, -1))
98 self.mtime = -1
117 self.mtime = -1
99 self.reponame = name
118 self.reponame = name
100 self.archives = 'zip', 'gz', 'bz2'
119 self.archives = 'zip', 'gz', 'bz2'
101 self.stripecount = 1
120 self.stripecount = 1
102 # a repo owner may set web.templates in .hg/hgrc to get any file
121 # a repo owner may set web.templates in .hg/hgrc to get any file
103 # readable by the user running the CGI script
122 # readable by the user running the CGI script
104 self.templatepath = self.config('web', 'templates')
123 self.templatepath = self.config('web', 'templates')
105 self.websubtable = self.loadwebsub()
124 self.websubtable = self.loadwebsub()
106
125
107 # The CGI scripts are often run by a user different from the repo owner.
126 # The CGI scripts are often run by a user different from the repo owner.
108 # Trust the settings from the .hg/hgrc files by default.
127 # Trust the settings from the .hg/hgrc files by default.
109 def config(self, section, name, default=None, untrusted=True):
128 def config(self, section, name, default=None, untrusted=True):
110 return self.repo.ui.config(section, name, default,
129 return self.repo.ui.config(section, name, default,
111 untrusted=untrusted)
130 untrusted=untrusted)
112
131
113 def configbool(self, section, name, default=False, untrusted=True):
132 def configbool(self, section, name, default=False, untrusted=True):
114 return self.repo.ui.configbool(section, name, default,
133 return self.repo.ui.configbool(section, name, default,
115 untrusted=untrusted)
134 untrusted=untrusted)
116
135
117 def configlist(self, section, name, default=None, untrusted=True):
136 def configlist(self, section, name, default=None, untrusted=True):
118 return self.repo.ui.configlist(section, name, default,
137 return self.repo.ui.configlist(section, name, default,
119 untrusted=untrusted)
138 untrusted=untrusted)
120
139
121 def _getview(self, repo):
140 def _getview(self, repo):
122 """The 'web.view' config controls changeset filter to hgweb. Possible
141 """The 'web.view' config controls changeset filter to hgweb. Possible
123 values are ``served``, ``visible`` and ``all``. Default is ``served``.
142 values are ``served``, ``visible`` and ``all``. Default is ``served``.
124 The ``served`` filter only shows changesets that can be pulled from the
143 The ``served`` filter only shows changesets that can be pulled from the
125 hgweb instance. The``visible`` filter includes secret changesets but
144 hgweb instance. The``visible`` filter includes secret changesets but
126 still excludes "hidden" one.
145 still excludes "hidden" one.
127
146
128 See the repoview module for details.
147 See the repoview module for details.
129
148
130 The option has been around undocumented since Mercurial 2.5, but no
149 The option has been around undocumented since Mercurial 2.5, but no
131 user ever asked about it. So we better keep it undocumented for now."""
150 user ever asked about it. So we better keep it undocumented for now."""
132 viewconfig = repo.ui.config('web', 'view', 'served',
151 viewconfig = repo.ui.config('web', 'view', 'served',
133 untrusted=True)
152 untrusted=True)
134 if viewconfig == 'all':
153 if viewconfig == 'all':
135 return repo.unfiltered()
154 return repo.unfiltered()
136 elif viewconfig in repoview.filtertable:
155 elif viewconfig in repoview.filtertable:
137 return repo.filtered(viewconfig)
156 return repo.filtered(viewconfig)
138 else:
157 else:
139 return repo.filtered('served')
158 return repo.filtered('served')
140
159
141 def refresh(self, request):
160 def refresh(self, request):
142 repostate = []
161 repostate = []
143 # file of interrests mtime and size
162 # file of interrests mtime and size
144 for meth, fname in foi:
163 for meth, fname in foi:
145 prefix = getattr(self.repo, meth)
164 prefix = getattr(self.repo, meth)
146 st = get_stat(prefix, fname)
165 st = get_stat(prefix, fname)
147 repostate.append((st.st_mtime, st.st_size))
166 repostate.append((st.st_mtime, st.st_size))
148 repostate = tuple(repostate)
167 repostate = tuple(repostate)
149 # we need to compare file size in addition to mtime to catch
168 # we need to compare file size in addition to mtime to catch
150 # changes made less than a second ago
169 # changes made less than a second ago
151 if repostate != self.repostate:
170 if repostate != self.repostate:
152 r = hg.repository(self.repo.baseui, self.repo.url())
171 r = hg.repository(self.repo.baseui, self.repo.url())
153 self.repo = self._getview(r)
172 self.repo = self._getview(r)
154 self.maxchanges = int(self.config("web", "maxchanges", 10))
173 self.maxchanges = int(self.config("web", "maxchanges", 10))
155 self.stripecount = int(self.config("web", "stripes", 1))
174 self.stripecount = int(self.config("web", "stripes", 1))
156 self.maxshortchanges = int(self.config("web", "maxshortchanges",
175 self.maxshortchanges = int(self.config("web", "maxshortchanges",
157 60))
176 60))
158 self.maxfiles = int(self.config("web", "maxfiles", 10))
177 self.maxfiles = int(self.config("web", "maxfiles", 10))
159 self.allowpull = self.configbool("web", "allowpull", True)
178 self.allowpull = self.configbool("web", "allowpull", True)
160 encoding.encoding = self.config("web", "encoding",
179 encoding.encoding = self.config("web", "encoding",
161 encoding.encoding)
180 encoding.encoding)
162 # update these last to avoid threads seeing empty settings
181 # update these last to avoid threads seeing empty settings
163 self.repostate = repostate
182 self.repostate = repostate
164 # mtime is needed for ETag
183 # mtime is needed for ETag
165 self.mtime = st.st_mtime
184 self.mtime = st.st_mtime
166
185
167 self.repo.ui.environ = request.env
186 self.repo.ui.environ = request.env
168
187
169 def run(self):
188 def run(self):
170 """Start a server from CGI environment.
189 """Start a server from CGI environment.
171
190
172 Modern servers should be using WSGI and should avoid this
191 Modern servers should be using WSGI and should avoid this
173 method, if possible.
192 method, if possible.
174 """
193 """
175 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
194 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
176 raise RuntimeError("This function is only intended to be "
195 raise RuntimeError("This function is only intended to be "
177 "called while running as a CGI script.")
196 "called while running as a CGI script.")
178 import mercurial.hgweb.wsgicgi as wsgicgi
197 import mercurial.hgweb.wsgicgi as wsgicgi
179 wsgicgi.launch(self)
198 wsgicgi.launch(self)
180
199
181 def __call__(self, env, respond):
200 def __call__(self, env, respond):
182 """Run the WSGI application.
201 """Run the WSGI application.
183
202
184 This may be called by multiple threads.
203 This may be called by multiple threads.
185 """
204 """
186 req = wsgirequest(env, respond)
205 req = wsgirequest(env, respond)
187 return self.run_wsgi(req)
206 return self.run_wsgi(req)
188
207
189 def run_wsgi(self, req):
208 def run_wsgi(self, req):
190 """Internal method to run the WSGI application.
209 """Internal method to run the WSGI application.
191
210
192 This is typically only called by Mercurial. External consumers
211 This is typically only called by Mercurial. External consumers
193 should be using instances of this class as the WSGI application.
212 should be using instances of this class as the WSGI application.
194 """
213 """
195 self.refresh(req)
214 self.refresh(req)
215 rctx = requestcontext(self)
196
216
197 # work with CGI variables to create coherent structure
217 # work with CGI variables to create coherent structure
198 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
218 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
199
219
200 req.url = req.env['SCRIPT_NAME']
220 req.url = req.env['SCRIPT_NAME']
201 if not req.url.endswith('/'):
221 if not req.url.endswith('/'):
202 req.url += '/'
222 req.url += '/'
203 if 'REPO_NAME' in req.env:
223 if 'REPO_NAME' in req.env:
204 req.url += req.env['REPO_NAME'] + '/'
224 req.url += req.env['REPO_NAME'] + '/'
205
225
206 if 'PATH_INFO' in req.env:
226 if 'PATH_INFO' in req.env:
207 parts = req.env['PATH_INFO'].strip('/').split('/')
227 parts = req.env['PATH_INFO'].strip('/').split('/')
208 repo_parts = req.env.get('REPO_NAME', '').split('/')
228 repo_parts = req.env.get('REPO_NAME', '').split('/')
209 if parts[:len(repo_parts)] == repo_parts:
229 if parts[:len(repo_parts)] == repo_parts:
210 parts = parts[len(repo_parts):]
230 parts = parts[len(repo_parts):]
211 query = '/'.join(parts)
231 query = '/'.join(parts)
212 else:
232 else:
213 query = req.env['QUERY_STRING'].split('&', 1)[0]
233 query = req.env['QUERY_STRING'].split('&', 1)[0]
214 query = query.split(';', 1)[0]
234 query = query.split(';', 1)[0]
215
235
216 # process this if it's a protocol request
236 # process this if it's a protocol request
217 # protocol bits don't need to create any URLs
237 # protocol bits don't need to create any URLs
218 # and the clients always use the old URL structure
238 # and the clients always use the old URL structure
219
239
220 cmd = req.form.get('cmd', [''])[0]
240 cmd = req.form.get('cmd', [''])[0]
221 if protocol.iscmd(cmd):
241 if protocol.iscmd(cmd):
222 try:
242 try:
223 if query:
243 if query:
224 raise ErrorResponse(HTTP_NOT_FOUND)
244 raise ErrorResponse(HTTP_NOT_FOUND)
225 if cmd in perms:
245 if cmd in perms:
226 self.check_perm(req, perms[cmd])
246 self.check_perm(rctx, req, perms[cmd])
227 return protocol.call(self.repo, req, cmd)
247 return protocol.call(self.repo, req, cmd)
228 except ErrorResponse as inst:
248 except ErrorResponse as inst:
229 # A client that sends unbundle without 100-continue will
249 # A client that sends unbundle without 100-continue will
230 # break if we respond early.
250 # break if we respond early.
231 if (cmd == 'unbundle' and
251 if (cmd == 'unbundle' and
232 (req.env.get('HTTP_EXPECT',
252 (req.env.get('HTTP_EXPECT',
233 '').lower() != '100-continue') or
253 '').lower() != '100-continue') or
234 req.env.get('X-HgHttp2', '')):
254 req.env.get('X-HgHttp2', '')):
235 req.drain()
255 req.drain()
236 else:
256 else:
237 req.headers.append(('Connection', 'Close'))
257 req.headers.append(('Connection', 'Close'))
238 req.respond(inst, protocol.HGTYPE,
258 req.respond(inst, protocol.HGTYPE,
239 body='0\n%s\n' % inst.message)
259 body='0\n%s\n' % inst.message)
240 return ''
260 return ''
241
261
242 # translate user-visible url structure to internal structure
262 # translate user-visible url structure to internal structure
243
263
244 args = query.split('/', 2)
264 args = query.split('/', 2)
245 if 'cmd' not in req.form and args and args[0]:
265 if 'cmd' not in req.form and args and args[0]:
246
266
247 cmd = args.pop(0)
267 cmd = args.pop(0)
248 style = cmd.rfind('-')
268 style = cmd.rfind('-')
249 if style != -1:
269 if style != -1:
250 req.form['style'] = [cmd[:style]]
270 req.form['style'] = [cmd[:style]]
251 cmd = cmd[style + 1:]
271 cmd = cmd[style + 1:]
252
272
253 # avoid accepting e.g. style parameter as command
273 # avoid accepting e.g. style parameter as command
254 if util.safehasattr(webcommands, cmd):
274 if util.safehasattr(webcommands, cmd):
255 req.form['cmd'] = [cmd]
275 req.form['cmd'] = [cmd]
256
276
257 if cmd == 'static':
277 if cmd == 'static':
258 req.form['file'] = ['/'.join(args)]
278 req.form['file'] = ['/'.join(args)]
259 else:
279 else:
260 if args and args[0]:
280 if args and args[0]:
261 node = args.pop(0).replace('%2F', '/')
281 node = args.pop(0).replace('%2F', '/')
262 req.form['node'] = [node]
282 req.form['node'] = [node]
263 if args:
283 if args:
264 req.form['file'] = args
284 req.form['file'] = args
265
285
266 ua = req.env.get('HTTP_USER_AGENT', '')
286 ua = req.env.get('HTTP_USER_AGENT', '')
267 if cmd == 'rev' and 'mercurial' in ua:
287 if cmd == 'rev' and 'mercurial' in ua:
268 req.form['style'] = ['raw']
288 req.form['style'] = ['raw']
269
289
270 if cmd == 'archive':
290 if cmd == 'archive':
271 fn = req.form['node'][0]
291 fn = req.form['node'][0]
272 for type_, spec in self.archive_specs.iteritems():
292 for type_, spec in self.archive_specs.iteritems():
273 ext = spec[2]
293 ext = spec[2]
274 if fn.endswith(ext):
294 if fn.endswith(ext):
275 req.form['node'] = [fn[:-len(ext)]]
295 req.form['node'] = [fn[:-len(ext)]]
276 req.form['type'] = [type_]
296 req.form['type'] = [type_]
277
297
278 # process the web interface request
298 # process the web interface request
279
299
280 try:
300 try:
281 tmpl = self.templater(req)
301 tmpl = self.templater(req)
282 ctype = tmpl('mimetype', encoding=encoding.encoding)
302 ctype = tmpl('mimetype', encoding=encoding.encoding)
283 ctype = templater.stringify(ctype)
303 ctype = templater.stringify(ctype)
284
304
285 # check read permissions non-static content
305 # check read permissions non-static content
286 if cmd != 'static':
306 if cmd != 'static':
287 self.check_perm(req, None)
307 self.check_perm(rctx, req, None)
288
308
289 if cmd == '':
309 if cmd == '':
290 req.form['cmd'] = [tmpl.cache['default']]
310 req.form['cmd'] = [tmpl.cache['default']]
291 cmd = req.form['cmd'][0]
311 cmd = req.form['cmd'][0]
292
312
293 if self.configbool('web', 'cache', True):
313 if self.configbool('web', 'cache', True):
294 caching(self, req) # sets ETag header or raises NOT_MODIFIED
314 caching(self, req) # sets ETag header or raises NOT_MODIFIED
295 if cmd not in webcommands.__all__:
315 if cmd not in webcommands.__all__:
296 msg = 'no such method: %s' % cmd
316 msg = 'no such method: %s' % cmd
297 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
317 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
298 elif cmd == 'file' and 'raw' in req.form.get('style', []):
318 elif cmd == 'file' and 'raw' in req.form.get('style', []):
299 self.ctype = ctype
319 self.ctype = ctype
300 content = webcommands.rawfile(self, req, tmpl)
320 content = webcommands.rawfile(rctx, req, tmpl)
301 else:
321 else:
302 content = getattr(webcommands, cmd)(self, req, tmpl)
322 content = getattr(webcommands, cmd)(rctx, req, tmpl)
303 req.respond(HTTP_OK, ctype)
323 req.respond(HTTP_OK, ctype)
304
324
305 return content
325 return content
306
326
307 except (error.LookupError, error.RepoLookupError) as err:
327 except (error.LookupError, error.RepoLookupError) as err:
308 req.respond(HTTP_NOT_FOUND, ctype)
328 req.respond(HTTP_NOT_FOUND, ctype)
309 msg = str(err)
329 msg = str(err)
310 if (util.safehasattr(err, 'name') and
330 if (util.safehasattr(err, 'name') and
311 not isinstance(err, error.ManifestLookupError)):
331 not isinstance(err, error.ManifestLookupError)):
312 msg = 'revision not found: %s' % err.name
332 msg = 'revision not found: %s' % err.name
313 return tmpl('error', error=msg)
333 return tmpl('error', error=msg)
314 except (error.RepoError, error.RevlogError) as inst:
334 except (error.RepoError, error.RevlogError) as inst:
315 req.respond(HTTP_SERVER_ERROR, ctype)
335 req.respond(HTTP_SERVER_ERROR, ctype)
316 return tmpl('error', error=str(inst))
336 return tmpl('error', error=str(inst))
317 except ErrorResponse as inst:
337 except ErrorResponse as inst:
318 req.respond(inst, ctype)
338 req.respond(inst, ctype)
319 if inst.code == HTTP_NOT_MODIFIED:
339 if inst.code == HTTP_NOT_MODIFIED:
320 # Not allowed to return a body on a 304
340 # Not allowed to return a body on a 304
321 return ['']
341 return ['']
322 return tmpl('error', error=inst.message)
342 return tmpl('error', error=inst.message)
323
343
324 def loadwebsub(self):
344 def loadwebsub(self):
325 websubtable = []
345 websubtable = []
326 websubdefs = self.repo.ui.configitems('websub')
346 websubdefs = self.repo.ui.configitems('websub')
327 # we must maintain interhg backwards compatibility
347 # we must maintain interhg backwards compatibility
328 websubdefs += self.repo.ui.configitems('interhg')
348 websubdefs += self.repo.ui.configitems('interhg')
329 for key, pattern in websubdefs:
349 for key, pattern in websubdefs:
330 # grab the delimiter from the character after the "s"
350 # grab the delimiter from the character after the "s"
331 unesc = pattern[1]
351 unesc = pattern[1]
332 delim = re.escape(unesc)
352 delim = re.escape(unesc)
333
353
334 # identify portions of the pattern, taking care to avoid escaped
354 # identify portions of the pattern, taking care to avoid escaped
335 # delimiters. the replace format and flags are optional, but
355 # delimiters. the replace format and flags are optional, but
336 # delimiters are required.
356 # delimiters are required.
337 match = re.match(
357 match = re.match(
338 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
358 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
339 % (delim, delim, delim), pattern)
359 % (delim, delim, delim), pattern)
340 if not match:
360 if not match:
341 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
361 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
342 % (key, pattern))
362 % (key, pattern))
343 continue
363 continue
344
364
345 # we need to unescape the delimiter for regexp and format
365 # we need to unescape the delimiter for regexp and format
346 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
366 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
347 regexp = delim_re.sub(unesc, match.group(1))
367 regexp = delim_re.sub(unesc, match.group(1))
348 format = delim_re.sub(unesc, match.group(2))
368 format = delim_re.sub(unesc, match.group(2))
349
369
350 # the pattern allows for 6 regexp flags, so set them if necessary
370 # the pattern allows for 6 regexp flags, so set them if necessary
351 flagin = match.group(3)
371 flagin = match.group(3)
352 flags = 0
372 flags = 0
353 if flagin:
373 if flagin:
354 for flag in flagin.upper():
374 for flag in flagin.upper():
355 flags |= re.__dict__[flag]
375 flags |= re.__dict__[flag]
356
376
357 try:
377 try:
358 regexp = re.compile(regexp, flags)
378 regexp = re.compile(regexp, flags)
359 websubtable.append((regexp, format))
379 websubtable.append((regexp, format))
360 except re.error:
380 except re.error:
361 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
381 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
362 % (key, regexp))
382 % (key, regexp))
363 return websubtable
383 return websubtable
364
384
365 def templater(self, req):
385 def templater(self, req):
366
386
367 # determine scheme, port and server name
387 # determine scheme, port and server name
368 # this is needed to create absolute urls
388 # this is needed to create absolute urls
369
389
370 proto = req.env.get('wsgi.url_scheme')
390 proto = req.env.get('wsgi.url_scheme')
371 if proto == 'https':
391 if proto == 'https':
372 proto = 'https'
392 proto = 'https'
373 default_port = "443"
393 default_port = "443"
374 else:
394 else:
375 proto = 'http'
395 proto = 'http'
376 default_port = "80"
396 default_port = "80"
377
397
378 port = req.env["SERVER_PORT"]
398 port = req.env["SERVER_PORT"]
379 port = port != default_port and (":" + port) or ""
399 port = port != default_port and (":" + port) or ""
380 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
400 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
381 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
401 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
382 logoimg = self.config("web", "logoimg", "hglogo.png")
402 logoimg = self.config("web", "logoimg", "hglogo.png")
383 staticurl = self.config("web", "staticurl") or req.url + 'static/'
403 staticurl = self.config("web", "staticurl") or req.url + 'static/'
384 if not staticurl.endswith('/'):
404 if not staticurl.endswith('/'):
385 staticurl += '/'
405 staticurl += '/'
386
406
387 # some functions for the templater
407 # some functions for the templater
388
408
389 def motd(**map):
409 def motd(**map):
390 yield self.config("web", "motd", "")
410 yield self.config("web", "motd", "")
391
411
392 # figure out which style to use
412 # figure out which style to use
393
413
394 vars = {}
414 vars = {}
395 styles = (
415 styles = (
396 req.form.get('style', [None])[0],
416 req.form.get('style', [None])[0],
397 self.config('web', 'style'),
417 self.config('web', 'style'),
398 'paper',
418 'paper',
399 )
419 )
400 style, mapfile = templater.stylemap(styles, self.templatepath)
420 style, mapfile = templater.stylemap(styles, self.templatepath)
401 if style == styles[0]:
421 if style == styles[0]:
402 vars['style'] = style
422 vars['style'] = style
403
423
404 start = req.url[-1] == '?' and '&' or '?'
424 start = req.url[-1] == '?' and '&' or '?'
405 sessionvars = webutil.sessionvars(vars, start)
425 sessionvars = webutil.sessionvars(vars, start)
406
426
407 if not self.reponame:
427 if not self.reponame:
408 self.reponame = (self.config("web", "name")
428 self.reponame = (self.config("web", "name")
409 or req.env.get('REPO_NAME')
429 or req.env.get('REPO_NAME')
410 or req.url.strip('/') or self.repo.root)
430 or req.url.strip('/') or self.repo.root)
411
431
412 def websubfilter(text):
432 def websubfilter(text):
413 return websub(text, self.websubtable)
433 return websub(text, self.websubtable)
414
434
415 # create the templater
435 # create the templater
416
436
417 tmpl = templater.templater(mapfile,
437 tmpl = templater.templater(mapfile,
418 filters={"websub": websubfilter},
438 filters={"websub": websubfilter},
419 defaults={"url": req.url,
439 defaults={"url": req.url,
420 "logourl": logourl,
440 "logourl": logourl,
421 "logoimg": logoimg,
441 "logoimg": logoimg,
422 "staticurl": staticurl,
442 "staticurl": staticurl,
423 "urlbase": urlbase,
443 "urlbase": urlbase,
424 "repo": self.reponame,
444 "repo": self.reponame,
425 "encoding": encoding.encoding,
445 "encoding": encoding.encoding,
426 "motd": motd,
446 "motd": motd,
427 "sessionvars": sessionvars,
447 "sessionvars": sessionvars,
428 "pathdef": makebreadcrumb(req.url),
448 "pathdef": makebreadcrumb(req.url),
429 "style": style,
449 "style": style,
430 })
450 })
431 return tmpl
451 return tmpl
432
452
433 def archivelist(self, nodeid):
453 def archivelist(self, nodeid):
434 allowed = self.configlist("web", "allow_archive")
454 allowed = self.configlist("web", "allow_archive")
435 for i, spec in self.archive_specs.iteritems():
455 for i, spec in self.archive_specs.iteritems():
436 if i in allowed or self.configbool("web", "allow" + i):
456 if i in allowed or self.configbool("web", "allow" + i):
437 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
457 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
438
458
439 archive_specs = {
459 archive_specs = {
440 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
460 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
441 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
461 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
442 'zip': ('application/zip', 'zip', '.zip', None),
462 'zip': ('application/zip', 'zip', '.zip', None),
443 }
463 }
444
464
445 def check_perm(self, req, op):
465 def check_perm(self, rctx, req, op):
446 for permhook in permhooks:
466 for permhook in permhooks:
447 permhook(self, req, op)
467 permhook(rctx, req, op)
General Comments 0
You need to be logged in to leave comments. Login now