140 allowed = self.configlist('web', 'allow_archive') |
140 allowed = self.configlist('web', 'allow_archive') |
141 for typ, spec in self.archivespecs.iteritems(): |
141 for typ, spec in self.archivespecs.iteritems(): |
142 if typ in allowed or self.configbool('web', 'allow%s' % typ): |
142 if typ in allowed or self.configbool('web', 'allow%s' % typ): |
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid} |
143 yield {'type': typ, 'extension': spec[2], 'node': nodeid} |
144 |
144 |
145 def templater(self, req): |
145 def templater(self, wsgireq): |
146 # determine scheme, port and server name |
146 # determine scheme, port and server name |
147 # this is needed to create absolute urls |
147 # this is needed to create absolute urls |
148 |
148 |
149 proto = req.env.get('wsgi.url_scheme') |
149 proto = wsgireq.env.get('wsgi.url_scheme') |
150 if proto == 'https': |
150 if proto == 'https': |
151 proto = 'https' |
151 proto = 'https' |
152 default_port = '443' |
152 default_port = '443' |
153 else: |
153 else: |
154 proto = 'http' |
154 proto = 'http' |
155 default_port = '80' |
155 default_port = '80' |
156 |
156 |
157 port = req.env[r'SERVER_PORT'] |
157 port = wsgireq.env[r'SERVER_PORT'] |
158 port = port != default_port and (r':' + port) or r'' |
158 port = port != default_port and (r':' + port) or r'' |
159 urlbase = r'%s://%s%s' % (proto, req.env[r'SERVER_NAME'], port) |
159 urlbase = r'%s://%s%s' % (proto, wsgireq.env[r'SERVER_NAME'], port) |
160 logourl = self.config('web', 'logourl') |
160 logourl = self.config('web', 'logourl') |
161 logoimg = self.config('web', 'logoimg') |
161 logoimg = self.config('web', 'logoimg') |
162 staticurl = (self.config('web', 'staticurl') |
162 staticurl = (self.config('web', 'staticurl') |
163 or pycompat.sysbytes(req.url) + 'static/') |
163 or pycompat.sysbytes(wsgireq.url) + 'static/') |
164 if not staticurl.endswith('/'): |
164 if not staticurl.endswith('/'): |
165 staticurl += '/' |
165 staticurl += '/' |
166 |
166 |
167 # some functions for the templater |
167 # some functions for the templater |
168 |
168 |
170 yield self.config('web', 'motd') |
170 yield self.config('web', 'motd') |
171 |
171 |
172 # figure out which style to use |
172 # figure out which style to use |
173 |
173 |
174 vars = {} |
174 vars = {} |
175 styles, (style, mapfile) = getstyle(req, self.config, |
175 styles, (style, mapfile) = getstyle(wsgireq, self.config, |
176 self.templatepath) |
176 self.templatepath) |
177 if style == styles[0]: |
177 if style == styles[0]: |
178 vars['style'] = style |
178 vars['style'] = style |
179 |
179 |
180 start = '&' if req.url[-1] == r'?' else '?' |
180 start = '&' if wsgireq.url[-1] == r'?' else '?' |
181 sessionvars = webutil.sessionvars(vars, start) |
181 sessionvars = webutil.sessionvars(vars, start) |
182 |
182 |
183 if not self.reponame: |
183 if not self.reponame: |
184 self.reponame = (self.config('web', 'name', '') |
184 self.reponame = (self.config('web', 'name', '') |
185 or req.env.get('REPO_NAME') |
185 or wsgireq.env.get('REPO_NAME') |
186 or req.url.strip(r'/') or self.repo.root) |
186 or wsgireq.url.strip(r'/') or self.repo.root) |
187 |
187 |
188 def websubfilter(text): |
188 def websubfilter(text): |
189 return templatefilters.websub(text, self.websubtable) |
189 return templatefilters.websub(text, self.websubtable) |
190 |
190 |
191 # create the templater |
191 # create the templater |
192 # TODO: export all keywords: defaults = templatekw.keywords.copy() |
192 # TODO: export all keywords: defaults = templatekw.keywords.copy() |
193 defaults = { |
193 defaults = { |
194 'url': pycompat.sysbytes(req.url), |
194 'url': pycompat.sysbytes(wsgireq.url), |
195 'logourl': logourl, |
195 'logourl': logourl, |
196 'logoimg': logoimg, |
196 'logoimg': logoimg, |
197 'staticurl': staticurl, |
197 'staticurl': staticurl, |
198 'urlbase': urlbase, |
198 'urlbase': urlbase, |
199 'repo': self.reponame, |
199 'repo': self.reponame, |
200 'encoding': encoding.encoding, |
200 'encoding': encoding.encoding, |
201 'motd': motd, |
201 'motd': motd, |
202 'sessionvars': sessionvars, |
202 'sessionvars': sessionvars, |
203 'pathdef': makebreadcrumb(pycompat.sysbytes(req.url)), |
203 'pathdef': makebreadcrumb(pycompat.sysbytes(wsgireq.url)), |
204 'style': style, |
204 'style': style, |
205 'nonce': self.nonce, |
205 'nonce': self.nonce, |
206 } |
206 } |
207 tres = formatter.templateresources(self.repo.ui, self.repo) |
207 tres = formatter.templateresources(self.repo.ui, self.repo) |
208 tmpl = templater.templater.frommapfile(mapfile, |
208 tmpl = templater.templater.frommapfile(mapfile, |
299 def __call__(self, env, respond): |
299 def __call__(self, env, respond): |
300 """Run the WSGI application. |
300 """Run the WSGI application. |
301 |
301 |
302 This may be called by multiple threads. |
302 This may be called by multiple threads. |
303 """ |
303 """ |
304 req = wsgirequest(env, respond) |
304 req = requestmod.wsgirequest(env, respond) |
305 return self.run_wsgi(req) |
305 return self.run_wsgi(req) |
306 |
306 |
307 def run_wsgi(self, req): |
307 def run_wsgi(self, wsgireq): |
308 """Internal method to run the WSGI application. |
308 """Internal method to run the WSGI application. |
309 |
309 |
310 This is typically only called by Mercurial. External consumers |
310 This is typically only called by Mercurial. External consumers |
311 should be using instances of this class as the WSGI application. |
311 should be using instances of this class as the WSGI application. |
312 """ |
312 """ |
313 with self._obtainrepo() as repo: |
313 with self._obtainrepo() as repo: |
314 profile = repo.ui.configbool('profiling', 'enabled') |
314 profile = repo.ui.configbool('profiling', 'enabled') |
315 with profiling.profile(repo.ui, enabled=profile): |
315 with profiling.profile(repo.ui, enabled=profile): |
316 for r in self._runwsgi(req, repo): |
316 for r in self._runwsgi(wsgireq, repo): |
317 yield r |
317 yield r |
318 |
318 |
319 def _runwsgi(self, req, repo): |
319 def _runwsgi(self, wsgireq, repo): |
320 rctx = requestcontext(self, repo) |
320 rctx = requestcontext(self, repo) |
321 |
321 |
322 # This state is global across all threads. |
322 # This state is global across all threads. |
323 encoding.encoding = rctx.config('web', 'encoding') |
323 encoding.encoding = rctx.config('web', 'encoding') |
324 rctx.repo.ui.environ = req.env |
324 rctx.repo.ui.environ = wsgireq.env |
325 |
325 |
326 if rctx.csp: |
326 if rctx.csp: |
327 # hgwebdir may have added CSP header. Since we generate our own, |
327 # hgwebdir may have added CSP header. Since we generate our own, |
328 # replace it. |
328 # replace it. |
329 req.headers = [h for h in req.headers |
329 wsgireq.headers = [h for h in wsgireq.headers |
330 if h[0] != 'Content-Security-Policy'] |
330 if h[0] != 'Content-Security-Policy'] |
331 req.headers.append(('Content-Security-Policy', rctx.csp)) |
331 wsgireq.headers.append(('Content-Security-Policy', rctx.csp)) |
332 |
332 |
333 # work with CGI variables to create coherent structure |
333 # work with CGI variables to create coherent structure |
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME |
334 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME |
335 |
335 |
336 req.url = req.env[r'SCRIPT_NAME'] |
336 wsgireq.url = wsgireq.env[r'SCRIPT_NAME'] |
337 if not req.url.endswith(r'/'): |
337 if not wsgireq.url.endswith(r'/'): |
338 req.url += r'/' |
338 wsgireq.url += r'/' |
339 if req.env.get('REPO_NAME'): |
339 if wsgireq.env.get('REPO_NAME'): |
340 req.url += req.env[r'REPO_NAME'] + r'/' |
340 wsgireq.url += wsgireq.env[r'REPO_NAME'] + r'/' |
341 |
341 |
342 if r'PATH_INFO' in req.env: |
342 if r'PATH_INFO' in wsgireq.env: |
343 parts = req.env[r'PATH_INFO'].strip(r'/').split(r'/') |
343 parts = wsgireq.env[r'PATH_INFO'].strip(r'/').split(r'/') |
344 repo_parts = req.env.get(r'REPO_NAME', r'').split(r'/') |
344 repo_parts = wsgireq.env.get(r'REPO_NAME', r'').split(r'/') |
345 if parts[:len(repo_parts)] == repo_parts: |
345 if parts[:len(repo_parts)] == repo_parts: |
346 parts = parts[len(repo_parts):] |
346 parts = parts[len(repo_parts):] |
347 query = r'/'.join(parts) |
347 query = r'/'.join(parts) |
348 else: |
348 else: |
349 query = req.env[r'QUERY_STRING'].partition(r'&')[0] |
349 query = wsgireq.env[r'QUERY_STRING'].partition(r'&')[0] |
350 query = query.partition(r';')[0] |
350 query = query.partition(r';')[0] |
351 |
351 |
352 # Route it to a wire protocol handler if it looks like a wire protocol |
352 # Route it to a wire protocol handler if it looks like a wire protocol |
353 # request. |
353 # request. |
354 protohandler = wireprotoserver.parsehttprequest(rctx, req, query, |
354 protohandler = wireprotoserver.parsehttprequest(rctx, wsgireq, query, |
355 self.check_perm) |
355 self.check_perm) |
356 |
356 |
357 if protohandler: |
357 if protohandler: |
358 try: |
358 try: |
359 if query: |
359 if query: |
364 return protohandler['handleerror'](inst) |
364 return protohandler['handleerror'](inst) |
365 |
365 |
366 # translate user-visible url structure to internal structure |
366 # translate user-visible url structure to internal structure |
367 |
367 |
368 args = query.split(r'/', 2) |
368 args = query.split(r'/', 2) |
369 if 'cmd' not in req.form and args and args[0]: |
369 if 'cmd' not in wsgireq.form and args and args[0]: |
370 cmd = args.pop(0) |
370 cmd = args.pop(0) |
371 style = cmd.rfind('-') |
371 style = cmd.rfind('-') |
372 if style != -1: |
372 if style != -1: |
373 req.form['style'] = [cmd[:style]] |
373 wsgireq.form['style'] = [cmd[:style]] |
374 cmd = cmd[style + 1:] |
374 cmd = cmd[style + 1:] |
375 |
375 |
376 # avoid accepting e.g. style parameter as command |
376 # avoid accepting e.g. style parameter as command |
377 if util.safehasattr(webcommands, cmd): |
377 if util.safehasattr(webcommands, cmd): |
378 req.form['cmd'] = [cmd] |
378 wsgireq.form['cmd'] = [cmd] |
379 |
379 |
380 if cmd == 'static': |
380 if cmd == 'static': |
381 req.form['file'] = ['/'.join(args)] |
381 wsgireq.form['file'] = ['/'.join(args)] |
382 else: |
382 else: |
383 if args and args[0]: |
383 if args and args[0]: |
384 node = args.pop(0).replace('%2F', '/') |
384 node = args.pop(0).replace('%2F', '/') |
385 req.form['node'] = [node] |
385 wsgireq.form['node'] = [node] |
386 if args: |
386 if args: |
387 req.form['file'] = args |
387 wsgireq.form['file'] = args |
388 |
388 |
389 ua = req.env.get('HTTP_USER_AGENT', '') |
389 ua = wsgireq.env.get('HTTP_USER_AGENT', '') |
390 if cmd == 'rev' and 'mercurial' in ua: |
390 if cmd == 'rev' and 'mercurial' in ua: |
391 req.form['style'] = ['raw'] |
391 wsgireq.form['style'] = ['raw'] |
392 |
392 |
393 if cmd == 'archive': |
393 if cmd == 'archive': |
394 fn = req.form['node'][0] |
394 fn = wsgireq.form['node'][0] |
395 for type_, spec in rctx.archivespecs.iteritems(): |
395 for type_, spec in rctx.archivespecs.iteritems(): |
396 ext = spec[2] |
396 ext = spec[2] |
397 if fn.endswith(ext): |
397 if fn.endswith(ext): |
398 req.form['node'] = [fn[:-len(ext)]] |
398 wsgireq.form['node'] = [fn[:-len(ext)]] |
399 req.form['type'] = [type_] |
399 wsgireq.form['type'] = [type_] |
400 else: |
400 else: |
401 cmd = req.form.get('cmd', [''])[0] |
401 cmd = wsgireq.form.get('cmd', [''])[0] |
402 |
402 |
403 # process the web interface request |
403 # process the web interface request |
404 |
404 |
405 try: |
405 try: |
406 tmpl = rctx.templater(req) |
406 tmpl = rctx.templater(wsgireq) |
407 ctype = tmpl('mimetype', encoding=encoding.encoding) |
407 ctype = tmpl('mimetype', encoding=encoding.encoding) |
408 ctype = templater.stringify(ctype) |
408 ctype = templater.stringify(ctype) |
409 |
409 |
410 # check read permissions non-static content |
410 # check read permissions non-static content |
411 if cmd != 'static': |
411 if cmd != 'static': |
412 self.check_perm(rctx, req, None) |
412 self.check_perm(rctx, wsgireq, None) |
413 |
413 |
414 if cmd == '': |
414 if cmd == '': |
415 req.form['cmd'] = [tmpl.cache['default']] |
415 wsgireq.form['cmd'] = [tmpl.cache['default']] |
416 cmd = req.form['cmd'][0] |
416 cmd = wsgireq.form['cmd'][0] |
417 |
417 |
418 # Don't enable caching if using a CSP nonce because then it wouldn't |
418 # Don't enable caching if using a CSP nonce because then it wouldn't |
419 # be a nonce. |
419 # be a nonce. |
420 if rctx.configbool('web', 'cache') and not rctx.nonce: |
420 if rctx.configbool('web', 'cache') and not rctx.nonce: |
421 caching(self, req) # sets ETag header or raises NOT_MODIFIED |
421 caching(self, wsgireq) # sets ETag header or raises NOT_MODIFIED |
422 if cmd not in webcommands.__all__: |
422 if cmd not in webcommands.__all__: |
423 msg = 'no such method: %s' % cmd |
423 msg = 'no such method: %s' % cmd |
424 raise ErrorResponse(HTTP_BAD_REQUEST, msg) |
424 raise ErrorResponse(HTTP_BAD_REQUEST, msg) |
425 elif cmd == 'file' and 'raw' in req.form.get('style', []): |
425 elif cmd == 'file' and 'raw' in wsgireq.form.get('style', []): |
426 rctx.ctype = ctype |
426 rctx.ctype = ctype |
427 content = webcommands.rawfile(rctx, req, tmpl) |
427 content = webcommands.rawfile(rctx, wsgireq, tmpl) |
428 else: |
428 else: |
429 content = getattr(webcommands, cmd)(rctx, req, tmpl) |
429 content = getattr(webcommands, cmd)(rctx, wsgireq, tmpl) |
430 req.respond(HTTP_OK, ctype) |
430 wsgireq.respond(HTTP_OK, ctype) |
431 |
431 |
432 return content |
432 return content |
433 |
433 |
434 except (error.LookupError, error.RepoLookupError) as err: |
434 except (error.LookupError, error.RepoLookupError) as err: |
435 req.respond(HTTP_NOT_FOUND, ctype) |
435 wsgireq.respond(HTTP_NOT_FOUND, ctype) |
436 msg = pycompat.bytestr(err) |
436 msg = pycompat.bytestr(err) |
437 if (util.safehasattr(err, 'name') and |
437 if (util.safehasattr(err, 'name') and |
438 not isinstance(err, error.ManifestLookupError)): |
438 not isinstance(err, error.ManifestLookupError)): |
439 msg = 'revision not found: %s' % err.name |
439 msg = 'revision not found: %s' % err.name |
440 return tmpl('error', error=msg) |
440 return tmpl('error', error=msg) |
441 except (error.RepoError, error.RevlogError) as inst: |
441 except (error.RepoError, error.RevlogError) as inst: |
442 req.respond(HTTP_SERVER_ERROR, ctype) |
442 wsgireq.respond(HTTP_SERVER_ERROR, ctype) |
443 return tmpl('error', error=pycompat.bytestr(inst)) |
443 return tmpl('error', error=pycompat.bytestr(inst)) |
444 except ErrorResponse as inst: |
444 except ErrorResponse as inst: |
445 req.respond(inst, ctype) |
445 wsgireq.respond(inst, ctype) |
446 if inst.code == HTTP_NOT_MODIFIED: |
446 if inst.code == HTTP_NOT_MODIFIED: |
447 # Not allowed to return a body on a 304 |
447 # Not allowed to return a body on a 304 |
448 return [''] |
448 return [''] |
449 return tmpl('error', error=pycompat.bytestr(inst)) |
449 return tmpl('error', error=pycompat.bytestr(inst)) |
450 |
450 |