##// END OF EJS Templates
hgweb: drop the default argument for get_stat...
Pierre-Yves David -
r25717:46e2c570 default
parent child Browse files
Show More
@@ -1,192 +1,192 b''
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
1 # hgweb/common.py - Utility functions needed by hgweb_mod and hgwebdir_mod
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, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 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 errno, mimetypes, os
9 import errno, mimetypes, os
10
10
11 HTTP_OK = 200
11 HTTP_OK = 200
12 HTTP_NOT_MODIFIED = 304
12 HTTP_NOT_MODIFIED = 304
13 HTTP_BAD_REQUEST = 400
13 HTTP_BAD_REQUEST = 400
14 HTTP_UNAUTHORIZED = 401
14 HTTP_UNAUTHORIZED = 401
15 HTTP_FORBIDDEN = 403
15 HTTP_FORBIDDEN = 403
16 HTTP_NOT_FOUND = 404
16 HTTP_NOT_FOUND = 404
17 HTTP_METHOD_NOT_ALLOWED = 405
17 HTTP_METHOD_NOT_ALLOWED = 405
18 HTTP_SERVER_ERROR = 500
18 HTTP_SERVER_ERROR = 500
19
19
20
20
21 def ismember(ui, username, userlist):
21 def ismember(ui, username, userlist):
22 """Check if username is a member of userlist.
22 """Check if username is a member of userlist.
23
23
24 If userlist has a single '*' member, all users are considered members.
24 If userlist has a single '*' member, all users are considered members.
25 Can be overridden by extensions to provide more complex authorization
25 Can be overridden by extensions to provide more complex authorization
26 schemes.
26 schemes.
27 """
27 """
28 return userlist == ['*'] or username in userlist
28 return userlist == ['*'] or username in userlist
29
29
30 def checkauthz(hgweb, req, op):
30 def checkauthz(hgweb, req, op):
31 '''Check permission for operation based on request data (including
31 '''Check permission for operation based on request data (including
32 authentication info). Return if op allowed, else raise an ErrorResponse
32 authentication info). Return if op allowed, else raise an ErrorResponse
33 exception.'''
33 exception.'''
34
34
35 user = req.env.get('REMOTE_USER')
35 user = req.env.get('REMOTE_USER')
36
36
37 deny_read = hgweb.configlist('web', 'deny_read')
37 deny_read = hgweb.configlist('web', 'deny_read')
38 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
38 if deny_read and (not user or ismember(hgweb.repo.ui, user, deny_read)):
39 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
39 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
40
40
41 allow_read = hgweb.configlist('web', 'allow_read')
41 allow_read = hgweb.configlist('web', 'allow_read')
42 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
42 if allow_read and (not ismember(hgweb.repo.ui, user, allow_read)):
43 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
43 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
44
44
45 if op == 'pull' and not hgweb.allowpull:
45 if op == 'pull' and not hgweb.allowpull:
46 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
46 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
47 elif op == 'pull' or op is None: # op is None for interface requests
47 elif op == 'pull' or op is None: # op is None for interface requests
48 return
48 return
49
49
50 # enforce that you can only push using POST requests
50 # enforce that you can only push using POST requests
51 if req.env['REQUEST_METHOD'] != 'POST':
51 if req.env['REQUEST_METHOD'] != 'POST':
52 msg = 'push requires POST request'
52 msg = 'push requires POST request'
53 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
53 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
54
54
55 # require ssl by default for pushing, auth info cannot be sniffed
55 # require ssl by default for pushing, auth info cannot be sniffed
56 # and replayed
56 # and replayed
57 scheme = req.env.get('wsgi.url_scheme')
57 scheme = req.env.get('wsgi.url_scheme')
58 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
58 if hgweb.configbool('web', 'push_ssl', True) and scheme != 'https':
59 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
59 raise ErrorResponse(HTTP_FORBIDDEN, 'ssl required')
60
60
61 deny = hgweb.configlist('web', 'deny_push')
61 deny = hgweb.configlist('web', 'deny_push')
62 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
62 if deny and (not user or ismember(hgweb.repo.ui, user, deny)):
63 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
63 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
64
64
65 allow = hgweb.configlist('web', 'allow_push')
65 allow = hgweb.configlist('web', 'allow_push')
66 if not (allow and ismember(hgweb.repo.ui, user, allow)):
66 if not (allow and ismember(hgweb.repo.ui, user, allow)):
67 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
67 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
68
68
69 # Hooks for hgweb permission checks; extensions can add hooks here.
69 # Hooks for hgweb permission checks; extensions can add hooks here.
70 # Each hook is invoked like this: hook(hgweb, request, operation),
70 # Each hook is invoked like this: hook(hgweb, request, operation),
71 # where operation is either read, pull or push. Hooks should either
71 # where operation is either read, pull or push. Hooks should either
72 # raise an ErrorResponse exception, or just return.
72 # raise an ErrorResponse exception, or just return.
73 #
73 #
74 # It is possible to do both authentication and authorization through
74 # It is possible to do both authentication and authorization through
75 # this.
75 # this.
76 permhooks = [checkauthz]
76 permhooks = [checkauthz]
77
77
78
78
79 class ErrorResponse(Exception):
79 class ErrorResponse(Exception):
80 def __init__(self, code, message=None, headers=[]):
80 def __init__(self, code, message=None, headers=[]):
81 if message is None:
81 if message is None:
82 message = _statusmessage(code)
82 message = _statusmessage(code)
83 Exception.__init__(self)
83 Exception.__init__(self)
84 self.code = code
84 self.code = code
85 self.message = message
85 self.message = message
86 self.headers = headers
86 self.headers = headers
87 def __str__(self):
87 def __str__(self):
88 return self.message
88 return self.message
89
89
90 class continuereader(object):
90 class continuereader(object):
91 def __init__(self, f, write):
91 def __init__(self, f, write):
92 self.f = f
92 self.f = f
93 self._write = write
93 self._write = write
94 self.continued = False
94 self.continued = False
95
95
96 def read(self, amt=-1):
96 def read(self, amt=-1):
97 if not self.continued:
97 if not self.continued:
98 self.continued = True
98 self.continued = True
99 self._write('HTTP/1.1 100 Continue\r\n\r\n')
99 self._write('HTTP/1.1 100 Continue\r\n\r\n')
100 return self.f.read(amt)
100 return self.f.read(amt)
101
101
102 def __getattr__(self, attr):
102 def __getattr__(self, attr):
103 if attr in ('close', 'readline', 'readlines', '__iter__'):
103 if attr in ('close', 'readline', 'readlines', '__iter__'):
104 return getattr(self.f, attr)
104 return getattr(self.f, attr)
105 raise AttributeError
105 raise AttributeError
106
106
107 def _statusmessage(code):
107 def _statusmessage(code):
108 from BaseHTTPServer import BaseHTTPRequestHandler
108 from BaseHTTPServer import BaseHTTPRequestHandler
109 responses = BaseHTTPRequestHandler.responses
109 responses = BaseHTTPRequestHandler.responses
110 return responses.get(code, ('Error', 'Unknown error'))[0]
110 return responses.get(code, ('Error', 'Unknown error'))[0]
111
111
112 def statusmessage(code, message=None):
112 def statusmessage(code, message=None):
113 return '%d %s' % (code, message or _statusmessage(code))
113 return '%d %s' % (code, message or _statusmessage(code))
114
114
115 def get_stat(spath, fn="00changelog.i"):
115 def get_stat(spath, fn):
116 """stat fn (00changelog.i by default) if it exists, spath otherwise"""
116 """stat fn if it exists, spath otherwise"""
117 cl_path = os.path.join(spath, fn)
117 cl_path = os.path.join(spath, fn)
118 if os.path.exists(cl_path):
118 if os.path.exists(cl_path):
119 return os.stat(cl_path)
119 return os.stat(cl_path)
120 else:
120 else:
121 return os.stat(spath)
121 return os.stat(spath)
122
122
123 def get_mtime(spath):
123 def get_mtime(spath):
124 return get_stat(spath).st_mtime
124 return get_stat(spath, "00changelog.i").st_mtime
125
125
126 def staticfile(directory, fname, req):
126 def staticfile(directory, fname, req):
127 """return a file inside directory with guessed Content-Type header
127 """return a file inside directory with guessed Content-Type header
128
128
129 fname always uses '/' as directory separator and isn't allowed to
129 fname always uses '/' as directory separator and isn't allowed to
130 contain unusual path components.
130 contain unusual path components.
131 Content-Type is guessed using the mimetypes module.
131 Content-Type is guessed using the mimetypes module.
132 Return an empty string if fname is illegal or file not found.
132 Return an empty string if fname is illegal or file not found.
133
133
134 """
134 """
135 parts = fname.split('/')
135 parts = fname.split('/')
136 for part in parts:
136 for part in parts:
137 if (part in ('', os.curdir, os.pardir) or
137 if (part in ('', os.curdir, os.pardir) or
138 os.sep in part or os.altsep is not None and os.altsep in part):
138 os.sep in part or os.altsep is not None and os.altsep in part):
139 return
139 return
140 fpath = os.path.join(*parts)
140 fpath = os.path.join(*parts)
141 if isinstance(directory, str):
141 if isinstance(directory, str):
142 directory = [directory]
142 directory = [directory]
143 for d in directory:
143 for d in directory:
144 path = os.path.join(d, fpath)
144 path = os.path.join(d, fpath)
145 if os.path.exists(path):
145 if os.path.exists(path):
146 break
146 break
147 try:
147 try:
148 os.stat(path)
148 os.stat(path)
149 ct = mimetypes.guess_type(path)[0] or "text/plain"
149 ct = mimetypes.guess_type(path)[0] or "text/plain"
150 fp = open(path, 'rb')
150 fp = open(path, 'rb')
151 data = fp.read()
151 data = fp.read()
152 fp.close()
152 fp.close()
153 req.respond(HTTP_OK, ct, body=data)
153 req.respond(HTTP_OK, ct, body=data)
154 except TypeError:
154 except TypeError:
155 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
155 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal filename')
156 except OSError as err:
156 except OSError as err:
157 if err.errno == errno.ENOENT:
157 if err.errno == errno.ENOENT:
158 raise ErrorResponse(HTTP_NOT_FOUND)
158 raise ErrorResponse(HTTP_NOT_FOUND)
159 else:
159 else:
160 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
160 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
161
161
162 def paritygen(stripecount, offset=0):
162 def paritygen(stripecount, offset=0):
163 """count parity of horizontal stripes for easier reading"""
163 """count parity of horizontal stripes for easier reading"""
164 if stripecount and offset:
164 if stripecount and offset:
165 # account for offset, e.g. due to building the list in reverse
165 # account for offset, e.g. due to building the list in reverse
166 count = (stripecount + offset) % stripecount
166 count = (stripecount + offset) % stripecount
167 parity = (stripecount + offset) / stripecount & 1
167 parity = (stripecount + offset) / stripecount & 1
168 else:
168 else:
169 count = 0
169 count = 0
170 parity = 0
170 parity = 0
171 while True:
171 while True:
172 yield parity
172 yield parity
173 count += 1
173 count += 1
174 if stripecount and count >= stripecount:
174 if stripecount and count >= stripecount:
175 parity = 1 - parity
175 parity = 1 - parity
176 count = 0
176 count = 0
177
177
178 def get_contact(config):
178 def get_contact(config):
179 """Return repo contact information or empty string.
179 """Return repo contact information or empty string.
180
180
181 web.contact is the primary source, but if that is not set, try
181 web.contact is the primary source, but if that is not set, try
182 ui.username or $EMAIL as a fallback to display something useful.
182 ui.username or $EMAIL as a fallback to display something useful.
183 """
183 """
184 return (config("web", "contact") or
184 return (config("web", "contact") or
185 config("ui", "username") or
185 config("ui", "username") or
186 os.environ.get("EMAIL") or "")
186 os.environ.get("EMAIL") or "")
187
187
188 def caching(web, req):
188 def caching(web, req):
189 tag = str(web.mtime)
189 tag = str(web.mtime)
190 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
190 if req.env.get('HTTP_IF_NONE_MATCH') == tag:
191 raise ErrorResponse(HTTP_NOT_MODIFIED)
191 raise ErrorResponse(HTTP_NOT_MODIFIED)
192 req.headers.append(('ETag', tag))
192 req.headers.append(('ETag', tag))
@@ -1,412 +1,412 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 def makebreadcrumb(url, prefix=''):
29 def makebreadcrumb(url, prefix=''):
30 '''Return a 'URL breadcrumb' list
30 '''Return a 'URL breadcrumb' list
31
31
32 A 'URL breadcrumb' is a list of URL-name pairs,
32 A 'URL breadcrumb' is a list of URL-name pairs,
33 corresponding to each of the path items on a URL.
33 corresponding to each of the path items on a URL.
34 This can be used to create path navigation entries.
34 This can be used to create path navigation entries.
35 '''
35 '''
36 if url.endswith('/'):
36 if url.endswith('/'):
37 url = url[:-1]
37 url = url[:-1]
38 if prefix:
38 if prefix:
39 url = '/' + prefix + url
39 url = '/' + prefix + url
40 relpath = url
40 relpath = url
41 if relpath.startswith('/'):
41 if relpath.startswith('/'):
42 relpath = relpath[1:]
42 relpath = relpath[1:]
43
43
44 breadcrumb = []
44 breadcrumb = []
45 urlel = url
45 urlel = url
46 pathitems = [''] + relpath.split('/')
46 pathitems = [''] + relpath.split('/')
47 for pathel in reversed(pathitems):
47 for pathel in reversed(pathitems):
48 if not pathel or not urlel:
48 if not pathel or not urlel:
49 break
49 break
50 breadcrumb.append({'url': urlel, 'name': pathel})
50 breadcrumb.append({'url': urlel, 'name': pathel})
51 urlel = os.path.dirname(urlel)
51 urlel = os.path.dirname(urlel)
52 return reversed(breadcrumb)
52 return reversed(breadcrumb)
53
53
54
54
55 class hgweb(object):
55 class hgweb(object):
56 def __init__(self, repo, name=None, baseui=None):
56 def __init__(self, repo, name=None, baseui=None):
57 if isinstance(repo, str):
57 if isinstance(repo, str):
58 if baseui:
58 if baseui:
59 u = baseui.copy()
59 u = baseui.copy()
60 else:
60 else:
61 u = ui.ui()
61 u = ui.ui()
62 r = hg.repository(u, repo)
62 r = hg.repository(u, repo)
63 else:
63 else:
64 # we trust caller to give us a private copy
64 # we trust caller to give us a private copy
65 r = repo
65 r = repo
66
66
67 r = self._getview(r)
67 r = self._getview(r)
68 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
68 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
69 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
69 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
70 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
70 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
71 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
71 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
72 # displaying bundling progress bar while serving feel wrong and may
72 # displaying bundling progress bar while serving feel wrong and may
73 # break some wsgi implementation.
73 # break some wsgi implementation.
74 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
74 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
75 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
75 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
76 self.repo = r
76 self.repo = r
77 hook.redirect(True)
77 hook.redirect(True)
78 self.repostate = ((-1, -1), (-1, -1))
78 self.repostate = ((-1, -1), (-1, -1))
79 self.mtime = -1
79 self.mtime = -1
80 self.reponame = name
80 self.reponame = name
81 self.archives = 'zip', 'gz', 'bz2'
81 self.archives = 'zip', 'gz', 'bz2'
82 self.stripecount = 1
82 self.stripecount = 1
83 # a repo owner may set web.templates in .hg/hgrc to get any file
83 # a repo owner may set web.templates in .hg/hgrc to get any file
84 # readable by the user running the CGI script
84 # readable by the user running the CGI script
85 self.templatepath = self.config('web', 'templates')
85 self.templatepath = self.config('web', 'templates')
86 self.websubtable = self.loadwebsub()
86 self.websubtable = self.loadwebsub()
87
87
88 # The CGI scripts are often run by a user different from the repo owner.
88 # The CGI scripts are often run by a user different from the repo owner.
89 # Trust the settings from the .hg/hgrc files by default.
89 # Trust the settings from the .hg/hgrc files by default.
90 def config(self, section, name, default=None, untrusted=True):
90 def config(self, section, name, default=None, untrusted=True):
91 return self.repo.ui.config(section, name, default,
91 return self.repo.ui.config(section, name, default,
92 untrusted=untrusted)
92 untrusted=untrusted)
93
93
94 def configbool(self, section, name, default=False, untrusted=True):
94 def configbool(self, section, name, default=False, untrusted=True):
95 return self.repo.ui.configbool(section, name, default,
95 return self.repo.ui.configbool(section, name, default,
96 untrusted=untrusted)
96 untrusted=untrusted)
97
97
98 def configlist(self, section, name, default=None, untrusted=True):
98 def configlist(self, section, name, default=None, untrusted=True):
99 return self.repo.ui.configlist(section, name, default,
99 return self.repo.ui.configlist(section, name, default,
100 untrusted=untrusted)
100 untrusted=untrusted)
101
101
102 def _getview(self, repo):
102 def _getview(self, repo):
103 """The 'web.view' config controls changeset filter to hgweb. Possible
103 """The 'web.view' config controls changeset filter to hgweb. Possible
104 values are ``served``, ``visible`` and ``all``. Default is ``served``.
104 values are ``served``, ``visible`` and ``all``. Default is ``served``.
105 The ``served`` filter only shows changesets that can be pulled from the
105 The ``served`` filter only shows changesets that can be pulled from the
106 hgweb instance. The``visible`` filter includes secret changesets but
106 hgweb instance. The``visible`` filter includes secret changesets but
107 still excludes "hidden" one.
107 still excludes "hidden" one.
108
108
109 See the repoview module for details.
109 See the repoview module for details.
110
110
111 The option has been around undocumented since Mercurial 2.5, but no
111 The option has been around undocumented since Mercurial 2.5, but no
112 user ever asked about it. So we better keep it undocumented for now."""
112 user ever asked about it. So we better keep it undocumented for now."""
113 viewconfig = repo.ui.config('web', 'view', 'served',
113 viewconfig = repo.ui.config('web', 'view', 'served',
114 untrusted=True)
114 untrusted=True)
115 if viewconfig == 'all':
115 if viewconfig == 'all':
116 return repo.unfiltered()
116 return repo.unfiltered()
117 elif viewconfig in repoview.filtertable:
117 elif viewconfig in repoview.filtertable:
118 return repo.filtered(viewconfig)
118 return repo.filtered(viewconfig)
119 else:
119 else:
120 return repo.filtered('served')
120 return repo.filtered('served')
121
121
122 def refresh(self, request=None):
122 def refresh(self, request=None):
123 st = get_stat(self.repo.spath)
123 st = get_stat(self.repo.spath, '00changelog.i')
124 pst = get_stat(self.repo.spath, 'phaseroots')
124 pst = get_stat(self.repo.spath, 'phaseroots')
125 # changelog mtime and size, phaseroots mtime and size
125 # changelog mtime and size, phaseroots mtime and size
126 repostate = ((st.st_mtime, st.st_size), (pst.st_mtime, pst.st_size))
126 repostate = ((st.st_mtime, st.st_size), (pst.st_mtime, pst.st_size))
127 # we need to compare file size in addition to mtime to catch
127 # we need to compare file size in addition to mtime to catch
128 # changes made less than a second ago
128 # changes made less than a second ago
129 if repostate != self.repostate:
129 if repostate != self.repostate:
130 r = hg.repository(self.repo.baseui, self.repo.url())
130 r = hg.repository(self.repo.baseui, self.repo.url())
131 self.repo = self._getview(r)
131 self.repo = self._getview(r)
132 self.maxchanges = int(self.config("web", "maxchanges", 10))
132 self.maxchanges = int(self.config("web", "maxchanges", 10))
133 self.stripecount = int(self.config("web", "stripes", 1))
133 self.stripecount = int(self.config("web", "stripes", 1))
134 self.maxshortchanges = int(self.config("web", "maxshortchanges",
134 self.maxshortchanges = int(self.config("web", "maxshortchanges",
135 60))
135 60))
136 self.maxfiles = int(self.config("web", "maxfiles", 10))
136 self.maxfiles = int(self.config("web", "maxfiles", 10))
137 self.allowpull = self.configbool("web", "allowpull", True)
137 self.allowpull = self.configbool("web", "allowpull", True)
138 encoding.encoding = self.config("web", "encoding",
138 encoding.encoding = self.config("web", "encoding",
139 encoding.encoding)
139 encoding.encoding)
140 # update these last to avoid threads seeing empty settings
140 # update these last to avoid threads seeing empty settings
141 self.repostate = repostate
141 self.repostate = repostate
142 # mtime is needed for ETag
142 # mtime is needed for ETag
143 self.mtime = st.st_mtime
143 self.mtime = st.st_mtime
144 if request:
144 if request:
145 self.repo.ui.environ = request.env
145 self.repo.ui.environ = request.env
146
146
147 def run(self):
147 def run(self):
148 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
148 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
149 raise RuntimeError("This function is only intended to be "
149 raise RuntimeError("This function is only intended to be "
150 "called while running as a CGI script.")
150 "called while running as a CGI script.")
151 import mercurial.hgweb.wsgicgi as wsgicgi
151 import mercurial.hgweb.wsgicgi as wsgicgi
152 wsgicgi.launch(self)
152 wsgicgi.launch(self)
153
153
154 def __call__(self, env, respond):
154 def __call__(self, env, respond):
155 req = wsgirequest(env, respond)
155 req = wsgirequest(env, respond)
156 return self.run_wsgi(req)
156 return self.run_wsgi(req)
157
157
158 def run_wsgi(self, req):
158 def run_wsgi(self, req):
159
159
160 self.refresh(req)
160 self.refresh(req)
161
161
162 # work with CGI variables to create coherent structure
162 # work with CGI variables to create coherent structure
163 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
163 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
164
164
165 req.url = req.env['SCRIPT_NAME']
165 req.url = req.env['SCRIPT_NAME']
166 if not req.url.endswith('/'):
166 if not req.url.endswith('/'):
167 req.url += '/'
167 req.url += '/'
168 if 'REPO_NAME' in req.env:
168 if 'REPO_NAME' in req.env:
169 req.url += req.env['REPO_NAME'] + '/'
169 req.url += req.env['REPO_NAME'] + '/'
170
170
171 if 'PATH_INFO' in req.env:
171 if 'PATH_INFO' in req.env:
172 parts = req.env['PATH_INFO'].strip('/').split('/')
172 parts = req.env['PATH_INFO'].strip('/').split('/')
173 repo_parts = req.env.get('REPO_NAME', '').split('/')
173 repo_parts = req.env.get('REPO_NAME', '').split('/')
174 if parts[:len(repo_parts)] == repo_parts:
174 if parts[:len(repo_parts)] == repo_parts:
175 parts = parts[len(repo_parts):]
175 parts = parts[len(repo_parts):]
176 query = '/'.join(parts)
176 query = '/'.join(parts)
177 else:
177 else:
178 query = req.env['QUERY_STRING'].split('&', 1)[0]
178 query = req.env['QUERY_STRING'].split('&', 1)[0]
179 query = query.split(';', 1)[0]
179 query = query.split(';', 1)[0]
180
180
181 # process this if it's a protocol request
181 # process this if it's a protocol request
182 # protocol bits don't need to create any URLs
182 # protocol bits don't need to create any URLs
183 # and the clients always use the old URL structure
183 # and the clients always use the old URL structure
184
184
185 cmd = req.form.get('cmd', [''])[0]
185 cmd = req.form.get('cmd', [''])[0]
186 if protocol.iscmd(cmd):
186 if protocol.iscmd(cmd):
187 try:
187 try:
188 if query:
188 if query:
189 raise ErrorResponse(HTTP_NOT_FOUND)
189 raise ErrorResponse(HTTP_NOT_FOUND)
190 if cmd in perms:
190 if cmd in perms:
191 self.check_perm(req, perms[cmd])
191 self.check_perm(req, perms[cmd])
192 return protocol.call(self.repo, req, cmd)
192 return protocol.call(self.repo, req, cmd)
193 except ErrorResponse as inst:
193 except ErrorResponse as inst:
194 # A client that sends unbundle without 100-continue will
194 # A client that sends unbundle without 100-continue will
195 # break if we respond early.
195 # break if we respond early.
196 if (cmd == 'unbundle' and
196 if (cmd == 'unbundle' and
197 (req.env.get('HTTP_EXPECT',
197 (req.env.get('HTTP_EXPECT',
198 '').lower() != '100-continue') or
198 '').lower() != '100-continue') or
199 req.env.get('X-HgHttp2', '')):
199 req.env.get('X-HgHttp2', '')):
200 req.drain()
200 req.drain()
201 else:
201 else:
202 req.headers.append(('Connection', 'Close'))
202 req.headers.append(('Connection', 'Close'))
203 req.respond(inst, protocol.HGTYPE,
203 req.respond(inst, protocol.HGTYPE,
204 body='0\n%s\n' % inst.message)
204 body='0\n%s\n' % inst.message)
205 return ''
205 return ''
206
206
207 # translate user-visible url structure to internal structure
207 # translate user-visible url structure to internal structure
208
208
209 args = query.split('/', 2)
209 args = query.split('/', 2)
210 if 'cmd' not in req.form and args and args[0]:
210 if 'cmd' not in req.form and args and args[0]:
211
211
212 cmd = args.pop(0)
212 cmd = args.pop(0)
213 style = cmd.rfind('-')
213 style = cmd.rfind('-')
214 if style != -1:
214 if style != -1:
215 req.form['style'] = [cmd[:style]]
215 req.form['style'] = [cmd[:style]]
216 cmd = cmd[style + 1:]
216 cmd = cmd[style + 1:]
217
217
218 # avoid accepting e.g. style parameter as command
218 # avoid accepting e.g. style parameter as command
219 if util.safehasattr(webcommands, cmd):
219 if util.safehasattr(webcommands, cmd):
220 req.form['cmd'] = [cmd]
220 req.form['cmd'] = [cmd]
221
221
222 if cmd == 'static':
222 if cmd == 'static':
223 req.form['file'] = ['/'.join(args)]
223 req.form['file'] = ['/'.join(args)]
224 else:
224 else:
225 if args and args[0]:
225 if args and args[0]:
226 node = args.pop(0)
226 node = args.pop(0)
227 req.form['node'] = [node]
227 req.form['node'] = [node]
228 if args:
228 if args:
229 req.form['file'] = args
229 req.form['file'] = args
230
230
231 ua = req.env.get('HTTP_USER_AGENT', '')
231 ua = req.env.get('HTTP_USER_AGENT', '')
232 if cmd == 'rev' and 'mercurial' in ua:
232 if cmd == 'rev' and 'mercurial' in ua:
233 req.form['style'] = ['raw']
233 req.form['style'] = ['raw']
234
234
235 if cmd == 'archive':
235 if cmd == 'archive':
236 fn = req.form['node'][0]
236 fn = req.form['node'][0]
237 for type_, spec in self.archive_specs.iteritems():
237 for type_, spec in self.archive_specs.iteritems():
238 ext = spec[2]
238 ext = spec[2]
239 if fn.endswith(ext):
239 if fn.endswith(ext):
240 req.form['node'] = [fn[:-len(ext)]]
240 req.form['node'] = [fn[:-len(ext)]]
241 req.form['type'] = [type_]
241 req.form['type'] = [type_]
242
242
243 # process the web interface request
243 # process the web interface request
244
244
245 try:
245 try:
246 tmpl = self.templater(req)
246 tmpl = self.templater(req)
247 ctype = tmpl('mimetype', encoding=encoding.encoding)
247 ctype = tmpl('mimetype', encoding=encoding.encoding)
248 ctype = templater.stringify(ctype)
248 ctype = templater.stringify(ctype)
249
249
250 # check read permissions non-static content
250 # check read permissions non-static content
251 if cmd != 'static':
251 if cmd != 'static':
252 self.check_perm(req, None)
252 self.check_perm(req, None)
253
253
254 if cmd == '':
254 if cmd == '':
255 req.form['cmd'] = [tmpl.cache['default']]
255 req.form['cmd'] = [tmpl.cache['default']]
256 cmd = req.form['cmd'][0]
256 cmd = req.form['cmd'][0]
257
257
258 if self.configbool('web', 'cache', True):
258 if self.configbool('web', 'cache', True):
259 caching(self, req) # sets ETag header or raises NOT_MODIFIED
259 caching(self, req) # sets ETag header or raises NOT_MODIFIED
260 if cmd not in webcommands.__all__:
260 if cmd not in webcommands.__all__:
261 msg = 'no such method: %s' % cmd
261 msg = 'no such method: %s' % cmd
262 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
262 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
263 elif cmd == 'file' and 'raw' in req.form.get('style', []):
263 elif cmd == 'file' and 'raw' in req.form.get('style', []):
264 self.ctype = ctype
264 self.ctype = ctype
265 content = webcommands.rawfile(self, req, tmpl)
265 content = webcommands.rawfile(self, req, tmpl)
266 else:
266 else:
267 content = getattr(webcommands, cmd)(self, req, tmpl)
267 content = getattr(webcommands, cmd)(self, req, tmpl)
268 req.respond(HTTP_OK, ctype)
268 req.respond(HTTP_OK, ctype)
269
269
270 return content
270 return content
271
271
272 except (error.LookupError, error.RepoLookupError) as err:
272 except (error.LookupError, error.RepoLookupError) as err:
273 req.respond(HTTP_NOT_FOUND, ctype)
273 req.respond(HTTP_NOT_FOUND, ctype)
274 msg = str(err)
274 msg = str(err)
275 if (util.safehasattr(err, 'name') and
275 if (util.safehasattr(err, 'name') and
276 not isinstance(err, error.ManifestLookupError)):
276 not isinstance(err, error.ManifestLookupError)):
277 msg = 'revision not found: %s' % err.name
277 msg = 'revision not found: %s' % err.name
278 return tmpl('error', error=msg)
278 return tmpl('error', error=msg)
279 except (error.RepoError, error.RevlogError) as inst:
279 except (error.RepoError, error.RevlogError) as inst:
280 req.respond(HTTP_SERVER_ERROR, ctype)
280 req.respond(HTTP_SERVER_ERROR, ctype)
281 return tmpl('error', error=str(inst))
281 return tmpl('error', error=str(inst))
282 except ErrorResponse as inst:
282 except ErrorResponse as inst:
283 req.respond(inst, ctype)
283 req.respond(inst, ctype)
284 if inst.code == HTTP_NOT_MODIFIED:
284 if inst.code == HTTP_NOT_MODIFIED:
285 # Not allowed to return a body on a 304
285 # Not allowed to return a body on a 304
286 return ['']
286 return ['']
287 return tmpl('error', error=inst.message)
287 return tmpl('error', error=inst.message)
288
288
289 def loadwebsub(self):
289 def loadwebsub(self):
290 websubtable = []
290 websubtable = []
291 websubdefs = self.repo.ui.configitems('websub')
291 websubdefs = self.repo.ui.configitems('websub')
292 # we must maintain interhg backwards compatibility
292 # we must maintain interhg backwards compatibility
293 websubdefs += self.repo.ui.configitems('interhg')
293 websubdefs += self.repo.ui.configitems('interhg')
294 for key, pattern in websubdefs:
294 for key, pattern in websubdefs:
295 # grab the delimiter from the character after the "s"
295 # grab the delimiter from the character after the "s"
296 unesc = pattern[1]
296 unesc = pattern[1]
297 delim = re.escape(unesc)
297 delim = re.escape(unesc)
298
298
299 # identify portions of the pattern, taking care to avoid escaped
299 # identify portions of the pattern, taking care to avoid escaped
300 # delimiters. the replace format and flags are optional, but
300 # delimiters. the replace format and flags are optional, but
301 # delimiters are required.
301 # delimiters are required.
302 match = re.match(
302 match = re.match(
303 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
303 r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
304 % (delim, delim, delim), pattern)
304 % (delim, delim, delim), pattern)
305 if not match:
305 if not match:
306 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
306 self.repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
307 % (key, pattern))
307 % (key, pattern))
308 continue
308 continue
309
309
310 # we need to unescape the delimiter for regexp and format
310 # we need to unescape the delimiter for regexp and format
311 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
311 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
312 regexp = delim_re.sub(unesc, match.group(1))
312 regexp = delim_re.sub(unesc, match.group(1))
313 format = delim_re.sub(unesc, match.group(2))
313 format = delim_re.sub(unesc, match.group(2))
314
314
315 # the pattern allows for 6 regexp flags, so set them if necessary
315 # the pattern allows for 6 regexp flags, so set them if necessary
316 flagin = match.group(3)
316 flagin = match.group(3)
317 flags = 0
317 flags = 0
318 if flagin:
318 if flagin:
319 for flag in flagin.upper():
319 for flag in flagin.upper():
320 flags |= re.__dict__[flag]
320 flags |= re.__dict__[flag]
321
321
322 try:
322 try:
323 regexp = re.compile(regexp, flags)
323 regexp = re.compile(regexp, flags)
324 websubtable.append((regexp, format))
324 websubtable.append((regexp, format))
325 except re.error:
325 except re.error:
326 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
326 self.repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
327 % (key, regexp))
327 % (key, regexp))
328 return websubtable
328 return websubtable
329
329
330 def templater(self, req):
330 def templater(self, req):
331
331
332 # determine scheme, port and server name
332 # determine scheme, port and server name
333 # this is needed to create absolute urls
333 # this is needed to create absolute urls
334
334
335 proto = req.env.get('wsgi.url_scheme')
335 proto = req.env.get('wsgi.url_scheme')
336 if proto == 'https':
336 if proto == 'https':
337 proto = 'https'
337 proto = 'https'
338 default_port = "443"
338 default_port = "443"
339 else:
339 else:
340 proto = 'http'
340 proto = 'http'
341 default_port = "80"
341 default_port = "80"
342
342
343 port = req.env["SERVER_PORT"]
343 port = req.env["SERVER_PORT"]
344 port = port != default_port and (":" + port) or ""
344 port = port != default_port and (":" + port) or ""
345 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
345 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
346 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
346 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
347 logoimg = self.config("web", "logoimg", "hglogo.png")
347 logoimg = self.config("web", "logoimg", "hglogo.png")
348 staticurl = self.config("web", "staticurl") or req.url + 'static/'
348 staticurl = self.config("web", "staticurl") or req.url + 'static/'
349 if not staticurl.endswith('/'):
349 if not staticurl.endswith('/'):
350 staticurl += '/'
350 staticurl += '/'
351
351
352 # some functions for the templater
352 # some functions for the templater
353
353
354 def motd(**map):
354 def motd(**map):
355 yield self.config("web", "motd", "")
355 yield self.config("web", "motd", "")
356
356
357 # figure out which style to use
357 # figure out which style to use
358
358
359 vars = {}
359 vars = {}
360 styles = (
360 styles = (
361 req.form.get('style', [None])[0],
361 req.form.get('style', [None])[0],
362 self.config('web', 'style'),
362 self.config('web', 'style'),
363 'paper',
363 'paper',
364 )
364 )
365 style, mapfile = templater.stylemap(styles, self.templatepath)
365 style, mapfile = templater.stylemap(styles, self.templatepath)
366 if style == styles[0]:
366 if style == styles[0]:
367 vars['style'] = style
367 vars['style'] = style
368
368
369 start = req.url[-1] == '?' and '&' or '?'
369 start = req.url[-1] == '?' and '&' or '?'
370 sessionvars = webutil.sessionvars(vars, start)
370 sessionvars = webutil.sessionvars(vars, start)
371
371
372 if not self.reponame:
372 if not self.reponame:
373 self.reponame = (self.config("web", "name")
373 self.reponame = (self.config("web", "name")
374 or req.env.get('REPO_NAME')
374 or req.env.get('REPO_NAME')
375 or req.url.strip('/') or self.repo.root)
375 or req.url.strip('/') or self.repo.root)
376
376
377 def websubfilter(text):
377 def websubfilter(text):
378 return websub(text, self.websubtable)
378 return websub(text, self.websubtable)
379
379
380 # create the templater
380 # create the templater
381
381
382 tmpl = templater.templater(mapfile,
382 tmpl = templater.templater(mapfile,
383 filters={"websub": websubfilter},
383 filters={"websub": websubfilter},
384 defaults={"url": req.url,
384 defaults={"url": req.url,
385 "logourl": logourl,
385 "logourl": logourl,
386 "logoimg": logoimg,
386 "logoimg": logoimg,
387 "staticurl": staticurl,
387 "staticurl": staticurl,
388 "urlbase": urlbase,
388 "urlbase": urlbase,
389 "repo": self.reponame,
389 "repo": self.reponame,
390 "encoding": encoding.encoding,
390 "encoding": encoding.encoding,
391 "motd": motd,
391 "motd": motd,
392 "sessionvars": sessionvars,
392 "sessionvars": sessionvars,
393 "pathdef": makebreadcrumb(req.url),
393 "pathdef": makebreadcrumb(req.url),
394 "style": style,
394 "style": style,
395 })
395 })
396 return tmpl
396 return tmpl
397
397
398 def archivelist(self, nodeid):
398 def archivelist(self, nodeid):
399 allowed = self.configlist("web", "allow_archive")
399 allowed = self.configlist("web", "allow_archive")
400 for i, spec in self.archive_specs.iteritems():
400 for i, spec in self.archive_specs.iteritems():
401 if i in allowed or self.configbool("web", "allow" + i):
401 if i in allowed or self.configbool("web", "allow" + i):
402 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
402 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
403
403
404 archive_specs = {
404 archive_specs = {
405 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
405 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
406 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
406 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
407 'zip': ('application/zip', 'zip', '.zip', None),
407 'zip': ('application/zip', 'zip', '.zip', None),
408 }
408 }
409
409
410 def check_perm(self, req, op):
410 def check_perm(self, req, op):
411 for permhook in permhooks:
411 for permhook in permhooks:
412 permhook(self, req, op)
412 permhook(self, req, op)
General Comments 0
You need to be logged in to leave comments. Login now