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