##// END OF EJS Templates
hgweb: raise ErrorResponses to communicate protocol errors
Dirkjan Ochtman -
r6926:57b954d8 default
parent child Browse files
Show More
@@ -1,118 +1,120 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
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import errno, mimetypes, os
9 import errno, mimetypes, os
10
10
11 HTTP_OK = 200
11 HTTP_OK = 200
12 HTTP_BAD_REQUEST = 400
12 HTTP_BAD_REQUEST = 400
13 HTTP_UNAUTHORIZED = 401
13 HTTP_NOT_FOUND = 404
14 HTTP_NOT_FOUND = 404
15 HTTP_METHOD_NOT_ALLOWED = 405
14 HTTP_SERVER_ERROR = 500
16 HTTP_SERVER_ERROR = 500
15
17
16 class ErrorResponse(Exception):
18 class ErrorResponse(Exception):
17 def __init__(self, code, message=None):
19 def __init__(self, code, message=None):
18 Exception.__init__(self)
20 Exception.__init__(self)
19 self.code = code
21 self.code = code
20 if message is not None:
22 if message is not None:
21 self.message = message
23 self.message = message
22 else:
24 else:
23 self.message = _statusmessage(code)
25 self.message = _statusmessage(code)
24
26
25 def _statusmessage(code):
27 def _statusmessage(code):
26 from BaseHTTPServer import BaseHTTPRequestHandler
28 from BaseHTTPServer import BaseHTTPRequestHandler
27 responses = BaseHTTPRequestHandler.responses
29 responses = BaseHTTPRequestHandler.responses
28 return responses.get(code, ('Error', 'Unknown error'))[0]
30 return responses.get(code, ('Error', 'Unknown error'))[0]
29
31
30 def statusmessage(code):
32 def statusmessage(code):
31 return '%d %s' % (code, _statusmessage(code))
33 return '%d %s' % (code, _statusmessage(code))
32
34
33 def get_mtime(repo_path):
35 def get_mtime(repo_path):
34 store_path = os.path.join(repo_path, ".hg")
36 store_path = os.path.join(repo_path, ".hg")
35 if not os.path.isdir(os.path.join(store_path, "data")):
37 if not os.path.isdir(os.path.join(store_path, "data")):
36 store_path = os.path.join(store_path, "store")
38 store_path = os.path.join(store_path, "store")
37 cl_path = os.path.join(store_path, "00changelog.i")
39 cl_path = os.path.join(store_path, "00changelog.i")
38 if os.path.exists(cl_path):
40 if os.path.exists(cl_path):
39 return os.stat(cl_path).st_mtime
41 return os.stat(cl_path).st_mtime
40 else:
42 else:
41 return os.stat(store_path).st_mtime
43 return os.stat(store_path).st_mtime
42
44
43 def staticfile(directory, fname, req):
45 def staticfile(directory, fname, req):
44 """return a file inside directory with guessed Content-Type header
46 """return a file inside directory with guessed Content-Type header
45
47
46 fname always uses '/' as directory separator and isn't allowed to
48 fname always uses '/' as directory separator and isn't allowed to
47 contain unusual path components.
49 contain unusual path components.
48 Content-Type is guessed using the mimetypes module.
50 Content-Type is guessed using the mimetypes module.
49 Return an empty string if fname is illegal or file not found.
51 Return an empty string if fname is illegal or file not found.
50
52
51 """
53 """
52 parts = fname.split('/')
54 parts = fname.split('/')
53 path = directory
55 path = directory
54 for part in parts:
56 for part in parts:
55 if (part in ('', os.curdir, os.pardir) or
57 if (part in ('', os.curdir, os.pardir) or
56 os.sep in part or os.altsep is not None and os.altsep in part):
58 os.sep in part or os.altsep is not None and os.altsep in part):
57 return ""
59 return ""
58 path = os.path.join(path, part)
60 path = os.path.join(path, part)
59 try:
61 try:
60 os.stat(path)
62 os.stat(path)
61 ct = mimetypes.guess_type(path)[0] or "text/plain"
63 ct = mimetypes.guess_type(path)[0] or "text/plain"
62 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
64 req.respond(HTTP_OK, ct, length = os.path.getsize(path))
63 return file(path, 'rb').read()
65 return file(path, 'rb').read()
64 except TypeError:
66 except TypeError:
65 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal file name')
67 raise ErrorResponse(HTTP_SERVER_ERROR, 'illegal file name')
66 except OSError, err:
68 except OSError, err:
67 if err.errno == errno.ENOENT:
69 if err.errno == errno.ENOENT:
68 raise ErrorResponse(HTTP_NOT_FOUND)
70 raise ErrorResponse(HTTP_NOT_FOUND)
69 else:
71 else:
70 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
72 raise ErrorResponse(HTTP_SERVER_ERROR, err.strerror)
71
73
72 def style_map(templatepath, style):
74 def style_map(templatepath, style):
73 """Return path to mapfile for a given style.
75 """Return path to mapfile for a given style.
74
76
75 Searches mapfile in the following locations:
77 Searches mapfile in the following locations:
76 1. templatepath/style/map
78 1. templatepath/style/map
77 2. templatepath/map-style
79 2. templatepath/map-style
78 3. templatepath/map
80 3. templatepath/map
79 """
81 """
80 locations = style and [os.path.join(style, "map"), "map-"+style] or []
82 locations = style and [os.path.join(style, "map"), "map-"+style] or []
81 locations.append("map")
83 locations.append("map")
82 for location in locations:
84 for location in locations:
83 mapfile = os.path.join(templatepath, location)
85 mapfile = os.path.join(templatepath, location)
84 if os.path.isfile(mapfile):
86 if os.path.isfile(mapfile):
85 return mapfile
87 return mapfile
86 raise RuntimeError("No hgweb templates found in %r" % templatepath)
88 raise RuntimeError("No hgweb templates found in %r" % templatepath)
87
89
88 def paritygen(stripecount, offset=0):
90 def paritygen(stripecount, offset=0):
89 """count parity of horizontal stripes for easier reading"""
91 """count parity of horizontal stripes for easier reading"""
90 if stripecount and offset:
92 if stripecount and offset:
91 # account for offset, e.g. due to building the list in reverse
93 # account for offset, e.g. due to building the list in reverse
92 count = (stripecount + offset) % stripecount
94 count = (stripecount + offset) % stripecount
93 parity = (stripecount + offset) / stripecount & 1
95 parity = (stripecount + offset) / stripecount & 1
94 else:
96 else:
95 count = 0
97 count = 0
96 parity = 0
98 parity = 0
97 while True:
99 while True:
98 yield parity
100 yield parity
99 count += 1
101 count += 1
100 if stripecount and count >= stripecount:
102 if stripecount and count >= stripecount:
101 parity = 1 - parity
103 parity = 1 - parity
102 count = 0
104 count = 0
103
105
104 def countgen(start=0, step=1):
106 def countgen(start=0, step=1):
105 """count forever -- useful for line numbers"""
107 """count forever -- useful for line numbers"""
106 while True:
108 while True:
107 yield start
109 yield start
108 start += step
110 start += step
109
111
110 def get_contact(config):
112 def get_contact(config):
111 """Return repo contact information or empty string.
113 """Return repo contact information or empty string.
112
114
113 web.contact is the primary source, but if that is not set, try
115 web.contact is the primary source, but if that is not set, try
114 ui.username or $EMAIL as a fallback to display something useful.
116 ui.username or $EMAIL as a fallback to display something useful.
115 """
117 """
116 return (config("web", "contact") or
118 return (config("web", "contact") or
117 config("ui", "username") or
119 config("ui", "username") or
118 os.environ.get("EMAIL") or "")
120 os.environ.get("EMAIL") or "")
@@ -1,378 +1,379 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
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os, mimetypes
9 import os, mimetypes
10 from mercurial.node import hex, nullid
10 from mercurial.node import hex, nullid
11 from mercurial.repo import RepoError
11 from mercurial.repo import RepoError
12 from mercurial import mdiff, ui, hg, util, patch, hook
12 from mercurial import mdiff, ui, hg, util, patch, hook
13 from mercurial import revlog, templater, templatefilters
13 from mercurial import revlog, templater, templatefilters
14 from common import get_mtime, style_map, paritygen, countgen, ErrorResponse
14 from common import get_mtime, style_map, paritygen, countgen, ErrorResponse
15 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
16 from request import wsgirequest
17 from request import wsgirequest
17 import webcommands, protocol, webutil
18 import webcommands, protocol, webutil
18
19
19 perms = {
20 perms = {
20 'changegroup': 'pull',
21 'changegroup': 'pull',
21 'changegroupsubset': 'pull',
22 'changegroupsubset': 'pull',
22 'unbundle': 'push',
23 'unbundle': 'push',
23 'stream_out': 'pull',
24 'stream_out': 'pull',
24 }
25 }
25
26
26 class hgweb(object):
27 class hgweb(object):
27 def __init__(self, repo, name=None):
28 def __init__(self, repo, name=None):
28 if isinstance(repo, str):
29 if isinstance(repo, str):
29 parentui = ui.ui(report_untrusted=False, interactive=False)
30 parentui = ui.ui(report_untrusted=False, interactive=False)
30 self.repo = hg.repository(parentui, repo)
31 self.repo = hg.repository(parentui, repo)
31 else:
32 else:
32 self.repo = repo
33 self.repo = repo
33
34
34 hook.redirect(True)
35 hook.redirect(True)
35 self.mtime = -1
36 self.mtime = -1
36 self.reponame = name
37 self.reponame = name
37 self.archives = 'zip', 'gz', 'bz2'
38 self.archives = 'zip', 'gz', 'bz2'
38 self.stripecount = 1
39 self.stripecount = 1
39 # a repo owner may set web.templates in .hg/hgrc to get any file
40 # a repo owner may set web.templates in .hg/hgrc to get any file
40 # readable by the user running the CGI script
41 # readable by the user running the CGI script
41 self.templatepath = self.config("web", "templates",
42 self.templatepath = self.config("web", "templates",
42 templater.templatepath(),
43 templater.templatepath(),
43 untrusted=False)
44 untrusted=False)
44
45
45 # The CGI scripts are often run by a user different from the repo owner.
46 # The CGI scripts are often run by a user different from the repo owner.
46 # Trust the settings from the .hg/hgrc files by default.
47 # Trust the settings from the .hg/hgrc files by default.
47 def config(self, section, name, default=None, untrusted=True):
48 def config(self, section, name, default=None, untrusted=True):
48 return self.repo.ui.config(section, name, default,
49 return self.repo.ui.config(section, name, default,
49 untrusted=untrusted)
50 untrusted=untrusted)
50
51
51 def configbool(self, section, name, default=False, untrusted=True):
52 def configbool(self, section, name, default=False, untrusted=True):
52 return self.repo.ui.configbool(section, name, default,
53 return self.repo.ui.configbool(section, name, default,
53 untrusted=untrusted)
54 untrusted=untrusted)
54
55
55 def configlist(self, section, name, default=None, untrusted=True):
56 def configlist(self, section, name, default=None, untrusted=True):
56 return self.repo.ui.configlist(section, name, default,
57 return self.repo.ui.configlist(section, name, default,
57 untrusted=untrusted)
58 untrusted=untrusted)
58
59
59 def refresh(self):
60 def refresh(self):
60 mtime = get_mtime(self.repo.root)
61 mtime = get_mtime(self.repo.root)
61 if mtime != self.mtime:
62 if mtime != self.mtime:
62 self.mtime = mtime
63 self.mtime = mtime
63 self.repo = hg.repository(self.repo.ui, self.repo.root)
64 self.repo = hg.repository(self.repo.ui, self.repo.root)
64 self.maxchanges = int(self.config("web", "maxchanges", 10))
65 self.maxchanges = int(self.config("web", "maxchanges", 10))
65 self.stripecount = int(self.config("web", "stripes", 1))
66 self.stripecount = int(self.config("web", "stripes", 1))
66 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
67 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
67 self.maxfiles = int(self.config("web", "maxfiles", 10))
68 self.maxfiles = int(self.config("web", "maxfiles", 10))
68 self.allowpull = self.configbool("web", "allowpull", True)
69 self.allowpull = self.configbool("web", "allowpull", True)
69 self.encoding = self.config("web", "encoding", util._encoding)
70 self.encoding = self.config("web", "encoding", util._encoding)
70
71
71 def run(self):
72 def run(self):
72 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
73 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
73 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
74 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
74 import mercurial.hgweb.wsgicgi as wsgicgi
75 import mercurial.hgweb.wsgicgi as wsgicgi
75 wsgicgi.launch(self)
76 wsgicgi.launch(self)
76
77
77 def __call__(self, env, respond):
78 def __call__(self, env, respond):
78 req = wsgirequest(env, respond)
79 req = wsgirequest(env, respond)
79 return self.run_wsgi(req)
80 return self.run_wsgi(req)
80
81
81 def run_wsgi(self, req):
82 def run_wsgi(self, req):
82
83
83 self.refresh()
84 self.refresh()
84
85
85 # process this if it's a protocol request
86 # process this if it's a protocol request
86 # protocol bits don't need to create any URLs
87 # protocol bits don't need to create any URLs
87 # and the clients always use the old URL structure
88 # and the clients always use the old URL structure
88
89
89 cmd = req.form.get('cmd', [''])[0]
90 cmd = req.form.get('cmd', [''])[0]
90 if cmd and cmd in protocol.__all__:
91 if cmd and cmd in protocol.__all__:
91 if cmd in perms and not self.check_perm(req, perms[cmd]):
92 try:
92 return []
93 if cmd in perms:
94 self.check_perm(req, perms[cmd])
93 method = getattr(protocol, cmd)
95 method = getattr(protocol, cmd)
94 return method(self.repo, req)
96 return method(self.repo, req)
97 except ErrorResponse, inst:
98 req.respond(inst.code, protocol.HGTYPE)
99 if not inst.message:
100 return []
101 return '0\n%s\n' % inst.message,
95
102
96 # work with CGI variables to create coherent structure
103 # work with CGI variables to create coherent structure
97 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
104 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
98
105
99 req.url = req.env['SCRIPT_NAME']
106 req.url = req.env['SCRIPT_NAME']
100 if not req.url.endswith('/'):
107 if not req.url.endswith('/'):
101 req.url += '/'
108 req.url += '/'
102 if 'REPO_NAME' in req.env:
109 if 'REPO_NAME' in req.env:
103 req.url += req.env['REPO_NAME'] + '/'
110 req.url += req.env['REPO_NAME'] + '/'
104
111
105 if 'PATH_INFO' in req.env:
112 if 'PATH_INFO' in req.env:
106 parts = req.env['PATH_INFO'].strip('/').split('/')
113 parts = req.env['PATH_INFO'].strip('/').split('/')
107 repo_parts = req.env.get('REPO_NAME', '').split('/')
114 repo_parts = req.env.get('REPO_NAME', '').split('/')
108 if parts[:len(repo_parts)] == repo_parts:
115 if parts[:len(repo_parts)] == repo_parts:
109 parts = parts[len(repo_parts):]
116 parts = parts[len(repo_parts):]
110 query = '/'.join(parts)
117 query = '/'.join(parts)
111 else:
118 else:
112 query = req.env['QUERY_STRING'].split('&', 1)[0]
119 query = req.env['QUERY_STRING'].split('&', 1)[0]
113 query = query.split(';', 1)[0]
120 query = query.split(';', 1)[0]
114
121
115 # translate user-visible url structure to internal structure
122 # translate user-visible url structure to internal structure
116
123
117 args = query.split('/', 2)
124 args = query.split('/', 2)
118 if 'cmd' not in req.form and args and args[0]:
125 if 'cmd' not in req.form and args and args[0]:
119
126
120 cmd = args.pop(0)
127 cmd = args.pop(0)
121 style = cmd.rfind('-')
128 style = cmd.rfind('-')
122 if style != -1:
129 if style != -1:
123 req.form['style'] = [cmd[:style]]
130 req.form['style'] = [cmd[:style]]
124 cmd = cmd[style+1:]
131 cmd = cmd[style+1:]
125
132
126 # avoid accepting e.g. style parameter as command
133 # avoid accepting e.g. style parameter as command
127 if hasattr(webcommands, cmd):
134 if hasattr(webcommands, cmd):
128 req.form['cmd'] = [cmd]
135 req.form['cmd'] = [cmd]
129 else:
136 else:
130 cmd = ''
137 cmd = ''
131
138
132 if args and args[0]:
139 if args and args[0]:
133 node = args.pop(0)
140 node = args.pop(0)
134 req.form['node'] = [node]
141 req.form['node'] = [node]
135 if args:
142 if args:
136 req.form['file'] = args
143 req.form['file'] = args
137
144
138 if cmd == 'static':
145 if cmd == 'static':
139 req.form['file'] = req.form['node']
146 req.form['file'] = req.form['node']
140 elif cmd == 'archive':
147 elif cmd == 'archive':
141 fn = req.form['node'][0]
148 fn = req.form['node'][0]
142 for type_, spec in self.archive_specs.iteritems():
149 for type_, spec in self.archive_specs.iteritems():
143 ext = spec[2]
150 ext = spec[2]
144 if fn.endswith(ext):
151 if fn.endswith(ext):
145 req.form['node'] = [fn[:-len(ext)]]
152 req.form['node'] = [fn[:-len(ext)]]
146 req.form['type'] = [type_]
153 req.form['type'] = [type_]
147
154
148 # process the web interface request
155 # process the web interface request
149
156
150 try:
157 try:
151
158
152 tmpl = self.templater(req)
159 tmpl = self.templater(req)
153 ctype = tmpl('mimetype', encoding=self.encoding)
160 ctype = tmpl('mimetype', encoding=self.encoding)
154 ctype = templater.stringify(ctype)
161 ctype = templater.stringify(ctype)
155
162
156 if cmd == '':
163 if cmd == '':
157 req.form['cmd'] = [tmpl.cache['default']]
164 req.form['cmd'] = [tmpl.cache['default']]
158 cmd = req.form['cmd'][0]
165 cmd = req.form['cmd'][0]
159
166
160 if cmd not in webcommands.__all__:
167 if cmd not in webcommands.__all__:
161 msg = 'no such method: %s' % cmd
168 msg = 'no such method: %s' % cmd
162 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
169 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
163 elif cmd == 'file' and 'raw' in req.form.get('style', []):
170 elif cmd == 'file' and 'raw' in req.form.get('style', []):
164 self.ctype = ctype
171 self.ctype = ctype
165 content = webcommands.rawfile(self, req, tmpl)
172 content = webcommands.rawfile(self, req, tmpl)
166 else:
173 else:
167 content = getattr(webcommands, cmd)(self, req, tmpl)
174 content = getattr(webcommands, cmd)(self, req, tmpl)
168 req.respond(HTTP_OK, ctype)
175 req.respond(HTTP_OK, ctype)
169
176
170 req.write(content)
177 req.write(content)
171 return []
178 return []
172
179
173 except revlog.LookupError, err:
180 except revlog.LookupError, err:
174 req.respond(HTTP_NOT_FOUND, ctype)
181 req.respond(HTTP_NOT_FOUND, ctype)
175 msg = str(err)
182 msg = str(err)
176 if 'manifest' not in msg:
183 if 'manifest' not in msg:
177 msg = 'revision not found: %s' % err.name
184 msg = 'revision not found: %s' % err.name
178 req.write(tmpl('error', error=msg))
185 req.write(tmpl('error', error=msg))
179 return []
186 return []
180 except (RepoError, revlog.RevlogError), inst:
187 except (RepoError, revlog.RevlogError), inst:
181 req.respond(HTTP_SERVER_ERROR, ctype)
188 req.respond(HTTP_SERVER_ERROR, ctype)
182 req.write(tmpl('error', error=str(inst)))
189 req.write(tmpl('error', error=str(inst)))
183 return []
190 return []
184 except ErrorResponse, inst:
191 except ErrorResponse, inst:
185 req.respond(inst.code, ctype)
192 req.respond(inst.code, ctype)
186 req.write(tmpl('error', error=inst.message))
193 req.write(tmpl('error', error=inst.message))
187 return []
194 return []
188
195
189 def templater(self, req):
196 def templater(self, req):
190
197
191 # determine scheme, port and server name
198 # determine scheme, port and server name
192 # this is needed to create absolute urls
199 # this is needed to create absolute urls
193
200
194 proto = req.env.get('wsgi.url_scheme')
201 proto = req.env.get('wsgi.url_scheme')
195 if proto == 'https':
202 if proto == 'https':
196 proto = 'https'
203 proto = 'https'
197 default_port = "443"
204 default_port = "443"
198 else:
205 else:
199 proto = 'http'
206 proto = 'http'
200 default_port = "80"
207 default_port = "80"
201
208
202 port = req.env["SERVER_PORT"]
209 port = req.env["SERVER_PORT"]
203 port = port != default_port and (":" + port) or ""
210 port = port != default_port and (":" + port) or ""
204 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
211 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
205 staticurl = self.config("web", "staticurl") or req.url + 'static/'
212 staticurl = self.config("web", "staticurl") or req.url + 'static/'
206 if not staticurl.endswith('/'):
213 if not staticurl.endswith('/'):
207 staticurl += '/'
214 staticurl += '/'
208
215
209 # some functions for the templater
216 # some functions for the templater
210
217
211 def header(**map):
218 def header(**map):
212 yield tmpl('header', encoding=self.encoding, **map)
219 yield tmpl('header', encoding=self.encoding, **map)
213
220
214 def footer(**map):
221 def footer(**map):
215 yield tmpl("footer", **map)
222 yield tmpl("footer", **map)
216
223
217 def motd(**map):
224 def motd(**map):
218 yield self.config("web", "motd", "")
225 yield self.config("web", "motd", "")
219
226
220 def sessionvars(**map):
227 def sessionvars(**map):
221 fields = []
228 fields = []
222 if 'style' in req.form:
229 if 'style' in req.form:
223 style = req.form['style'][0]
230 style = req.form['style'][0]
224 if style != self.config('web', 'style', ''):
231 if style != self.config('web', 'style', ''):
225 fields.append(('style', style))
232 fields.append(('style', style))
226
233
227 separator = req.url[-1] == '?' and ';' or '?'
234 separator = req.url[-1] == '?' and ';' or '?'
228 for name, value in fields:
235 for name, value in fields:
229 yield dict(name=name, value=value, separator=separator)
236 yield dict(name=name, value=value, separator=separator)
230 separator = ';'
237 separator = ';'
231
238
232 # figure out which style to use
239 # figure out which style to use
233
240
234 style = self.config("web", "style", "")
241 style = self.config("web", "style", "")
235 if 'style' in req.form:
242 if 'style' in req.form:
236 style = req.form['style'][0]
243 style = req.form['style'][0]
237 mapfile = style_map(self.templatepath, style)
244 mapfile = style_map(self.templatepath, style)
238
245
239 if not self.reponame:
246 if not self.reponame:
240 self.reponame = (self.config("web", "name")
247 self.reponame = (self.config("web", "name")
241 or req.env.get('REPO_NAME')
248 or req.env.get('REPO_NAME')
242 or req.url.strip('/') or self.repo.root)
249 or req.url.strip('/') or self.repo.root)
243
250
244 # create the templater
251 # create the templater
245
252
246 tmpl = templater.templater(mapfile, templatefilters.filters,
253 tmpl = templater.templater(mapfile, templatefilters.filters,
247 defaults={"url": req.url,
254 defaults={"url": req.url,
248 "staticurl": staticurl,
255 "staticurl": staticurl,
249 "urlbase": urlbase,
256 "urlbase": urlbase,
250 "repo": self.reponame,
257 "repo": self.reponame,
251 "header": header,
258 "header": header,
252 "footer": footer,
259 "footer": footer,
253 "motd": motd,
260 "motd": motd,
254 "sessionvars": sessionvars
261 "sessionvars": sessionvars
255 })
262 })
256 return tmpl
263 return tmpl
257
264
258 def archivelist(self, nodeid):
265 def archivelist(self, nodeid):
259 allowed = self.configlist("web", "allow_archive")
266 allowed = self.configlist("web", "allow_archive")
260 for i, spec in self.archive_specs.iteritems():
267 for i, spec in self.archive_specs.iteritems():
261 if i in allowed or self.configbool("web", "allow" + i):
268 if i in allowed or self.configbool("web", "allow" + i):
262 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
269 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
263
270
264 def listfilediffs(self, tmpl, files, changeset):
271 def listfilediffs(self, tmpl, files, changeset):
265 for f in files[:self.maxfiles]:
272 for f in files[:self.maxfiles]:
266 yield tmpl("filedifflink", node=hex(changeset), file=f)
273 yield tmpl("filedifflink", node=hex(changeset), file=f)
267 if len(files) > self.maxfiles:
274 if len(files) > self.maxfiles:
268 yield tmpl("fileellipses")
275 yield tmpl("fileellipses")
269
276
270 def diff(self, tmpl, node1, node2, files):
277 def diff(self, tmpl, node1, node2, files):
271 def filterfiles(filters, files):
278 def filterfiles(filters, files):
272 l = [x for x in files if x in filters]
279 l = [x for x in files if x in filters]
273
280
274 for t in filters:
281 for t in filters:
275 if t and t[-1] != os.sep:
282 if t and t[-1] != os.sep:
276 t += os.sep
283 t += os.sep
277 l += [x for x in files if x.startswith(t)]
284 l += [x for x in files if x.startswith(t)]
278 return l
285 return l
279
286
280 parity = paritygen(self.stripecount)
287 parity = paritygen(self.stripecount)
281 def diffblock(diff, f, fn):
288 def diffblock(diff, f, fn):
282 yield tmpl("diffblock",
289 yield tmpl("diffblock",
283 lines=prettyprintlines(diff),
290 lines=prettyprintlines(diff),
284 parity=parity.next(),
291 parity=parity.next(),
285 file=f,
292 file=f,
286 filenode=hex(fn or nullid))
293 filenode=hex(fn or nullid))
287
294
288 blockcount = countgen()
295 blockcount = countgen()
289 def prettyprintlines(diff):
296 def prettyprintlines(diff):
290 blockno = blockcount.next()
297 blockno = blockcount.next()
291 for lineno, l in enumerate(diff.splitlines(1)):
298 for lineno, l in enumerate(diff.splitlines(1)):
292 if blockno == 0:
299 if blockno == 0:
293 lineno = lineno + 1
300 lineno = lineno + 1
294 else:
301 else:
295 lineno = "%d.%d" % (blockno, lineno + 1)
302 lineno = "%d.%d" % (blockno, lineno + 1)
296 if l.startswith('+'):
303 if l.startswith('+'):
297 ltype = "difflineplus"
304 ltype = "difflineplus"
298 elif l.startswith('-'):
305 elif l.startswith('-'):
299 ltype = "difflineminus"
306 ltype = "difflineminus"
300 elif l.startswith('@'):
307 elif l.startswith('@'):
301 ltype = "difflineat"
308 ltype = "difflineat"
302 else:
309 else:
303 ltype = "diffline"
310 ltype = "diffline"
304 yield tmpl(ltype,
311 yield tmpl(ltype,
305 line=l,
312 line=l,
306 lineid="l%s" % lineno,
313 lineid="l%s" % lineno,
307 linenumber="% 8s" % lineno)
314 linenumber="% 8s" % lineno)
308
315
309 r = self.repo
316 r = self.repo
310 c1 = r[node1]
317 c1 = r[node1]
311 c2 = r[node2]
318 c2 = r[node2]
312 date1 = util.datestr(c1.date())
319 date1 = util.datestr(c1.date())
313 date2 = util.datestr(c2.date())
320 date2 = util.datestr(c2.date())
314
321
315 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
322 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
316 if files:
323 if files:
317 modified, added, removed = map(lambda x: filterfiles(files, x),
324 modified, added, removed = map(lambda x: filterfiles(files, x),
318 (modified, added, removed))
325 (modified, added, removed))
319
326
320 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
327 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
321 for f in modified:
328 for f in modified:
322 to = c1.filectx(f).data()
329 to = c1.filectx(f).data()
323 tn = c2.filectx(f).data()
330 tn = c2.filectx(f).data()
324 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
331 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
325 opts=diffopts), f, tn)
332 opts=diffopts), f, tn)
326 for f in added:
333 for f in added:
327 to = None
334 to = None
328 tn = c2.filectx(f).data()
335 tn = c2.filectx(f).data()
329 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
336 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
330 opts=diffopts), f, tn)
337 opts=diffopts), f, tn)
331 for f in removed:
338 for f in removed:
332 to = c1.filectx(f).data()
339 to = c1.filectx(f).data()
333 tn = None
340 tn = None
334 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
341 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f, f,
335 opts=diffopts), f, tn)
342 opts=diffopts), f, tn)
336
343
337 archive_specs = {
344 archive_specs = {
338 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
345 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
339 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
346 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
340 'zip': ('application/zip', 'zip', '.zip', None),
347 'zip': ('application/zip', 'zip', '.zip', None),
341 }
348 }
342
349
343 def check_perm(self, req, op):
350 def check_perm(self, req, op):
344 '''Check permission for operation based on request data (including
351 '''Check permission for operation based on request data (including
345 authentication info. Return true if op allowed, else false.'''
352 authentication info. Return true if op allowed, else false.'''
346
353
347 def error(status, message):
354 if op == 'pull' and not self.allowpull:
348 req.respond(status, protocol.HGTYPE)
355 raise ErrorResponse(HTTP_OK, '')
349 req.write('0\n%s\n' % message)
356 elif op == 'pull':
350
357 return
351 if op == 'pull':
352 return self.allowpull
353
358
354 # enforce that you can only push using POST requests
359 # enforce that you can only push using POST requests
355 if req.env['REQUEST_METHOD'] != 'POST':
360 if req.env['REQUEST_METHOD'] != 'POST':
356 error('405 Method Not Allowed', 'push requires POST request')
361 msg = 'push requires POST request'
357 return False
362 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
358
363
359 # require ssl by default for pushing, auth info cannot be sniffed
364 # require ssl by default for pushing, auth info cannot be sniffed
360 # and replayed
365 # and replayed
361 scheme = req.env.get('wsgi.url_scheme')
366 scheme = req.env.get('wsgi.url_scheme')
362 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
367 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
363 error(HTTP_OK, 'ssl required')
368 raise ErrorResponse(HTTP_OK, 'ssl required')
364 return False
365
369
366 user = req.env.get('REMOTE_USER')
370 user = req.env.get('REMOTE_USER')
367
371
368 deny = self.configlist('web', 'deny_push')
372 deny = self.configlist('web', 'deny_push')
369 if deny and (not user or deny == ['*'] or user in deny):
373 if deny and (not user or deny == ['*'] or user in deny):
370 error('401 Unauthorized', 'push not authorized')
374 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
371 return False
372
375
373 allow = self.configlist('web', 'allow_push')
376 allow = self.configlist('web', 'allow_push')
374 result = allow and (allow == ['*'] or user in allow)
377 result = allow and (allow == ['*'] or user in allow)
375 if not result:
378 if not result:
376 error('401 Unauthorized', 'push not authorized')
379 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
377
378 return result
@@ -1,200 +1,196 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 import cStringIO, zlib, tempfile, errno, os, sys
8 import cStringIO, zlib, tempfile, errno, os, sys
9 from mercurial import util, streamclone
9 from mercurial import util, streamclone
10 from mercurial.node import bin, hex
10 from mercurial.node import bin, hex
11 from mercurial import changegroup as changegroupmod
11 from mercurial import changegroup as changegroupmod
12 from common import HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
12 from common import HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13
13
14 # __all__ is populated with the allowed commands. Be sure to add to it if
14 # __all__ is populated with the allowed commands. Be sure to add to it if
15 # you're adding a new command, or the new command won't work.
15 # you're adding a new command, or the new command won't work.
16
16
17 __all__ = [
17 __all__ = [
18 'lookup', 'heads', 'branches', 'between', 'changegroup',
18 'lookup', 'heads', 'branches', 'between', 'changegroup',
19 'changegroupsubset', 'capabilities', 'unbundle', 'stream_out',
19 'changegroupsubset', 'capabilities', 'unbundle', 'stream_out',
20 ]
20 ]
21
21
22 HGTYPE = 'application/mercurial-0.1'
22 HGTYPE = 'application/mercurial-0.1'
23
23
24 def lookup(repo, req):
24 def lookup(repo, req):
25 try:
25 try:
26 r = hex(repo.lookup(req.form['key'][0]))
26 r = hex(repo.lookup(req.form['key'][0]))
27 success = 1
27 success = 1
28 except Exception,inst:
28 except Exception,inst:
29 r = str(inst)
29 r = str(inst)
30 success = 0
30 success = 0
31 resp = "%s %s\n" % (success, r)
31 resp = "%s %s\n" % (success, r)
32 req.respond(HTTP_OK, HGTYPE, length=len(resp))
32 req.respond(HTTP_OK, HGTYPE, length=len(resp))
33 yield resp
33 yield resp
34
34
35 def heads(repo, req):
35 def heads(repo, req):
36 resp = " ".join(map(hex, repo.heads())) + "\n"
36 resp = " ".join(map(hex, repo.heads())) + "\n"
37 req.respond(HTTP_OK, HGTYPE, length=len(resp))
37 req.respond(HTTP_OK, HGTYPE, length=len(resp))
38 yield resp
38 yield resp
39
39
40 def branches(repo, req):
40 def branches(repo, req):
41 nodes = []
41 nodes = []
42 if 'nodes' in req.form:
42 if 'nodes' in req.form:
43 nodes = map(bin, req.form['nodes'][0].split(" "))
43 nodes = map(bin, req.form['nodes'][0].split(" "))
44 resp = cStringIO.StringIO()
44 resp = cStringIO.StringIO()
45 for b in repo.branches(nodes):
45 for b in repo.branches(nodes):
46 resp.write(" ".join(map(hex, b)) + "\n")
46 resp.write(" ".join(map(hex, b)) + "\n")
47 resp = resp.getvalue()
47 resp = resp.getvalue()
48 req.respond(HTTP_OK, HGTYPE, length=len(resp))
48 req.respond(HTTP_OK, HGTYPE, length=len(resp))
49 yield resp
49 yield resp
50
50
51 def between(repo, req):
51 def between(repo, req):
52 if 'pairs' in req.form:
52 if 'pairs' in req.form:
53 pairs = [map(bin, p.split("-"))
53 pairs = [map(bin, p.split("-"))
54 for p in req.form['pairs'][0].split(" ")]
54 for p in req.form['pairs'][0].split(" ")]
55 resp = cStringIO.StringIO()
55 resp = cStringIO.StringIO()
56 for b in repo.between(pairs):
56 for b in repo.between(pairs):
57 resp.write(" ".join(map(hex, b)) + "\n")
57 resp.write(" ".join(map(hex, b)) + "\n")
58 resp = resp.getvalue()
58 resp = resp.getvalue()
59 req.respond(HTTP_OK, HGTYPE, length=len(resp))
59 req.respond(HTTP_OK, HGTYPE, length=len(resp))
60 yield resp
60 yield resp
61
61
62 def changegroup(repo, req):
62 def changegroup(repo, req):
63 req.respond(HTTP_OK, HGTYPE)
63 req.respond(HTTP_OK, HGTYPE)
64 nodes = []
64 nodes = []
65
65
66 if 'roots' in req.form:
66 if 'roots' in req.form:
67 nodes = map(bin, req.form['roots'][0].split(" "))
67 nodes = map(bin, req.form['roots'][0].split(" "))
68
68
69 z = zlib.compressobj()
69 z = zlib.compressobj()
70 f = repo.changegroup(nodes, 'serve')
70 f = repo.changegroup(nodes, 'serve')
71 while 1:
71 while 1:
72 chunk = f.read(4096)
72 chunk = f.read(4096)
73 if not chunk:
73 if not chunk:
74 break
74 break
75 yield z.compress(chunk)
75 yield z.compress(chunk)
76
76
77 yield z.flush()
77 yield z.flush()
78
78
79 def changegroupsubset(repo, req):
79 def changegroupsubset(repo, req):
80 req.respond(HTTP_OK, HGTYPE)
80 req.respond(HTTP_OK, HGTYPE)
81 bases = []
81 bases = []
82 heads = []
82 heads = []
83
83
84 if 'bases' in req.form:
84 if 'bases' in req.form:
85 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
85 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
86 if 'heads' in req.form:
86 if 'heads' in req.form:
87 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
87 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
88
88
89 z = zlib.compressobj()
89 z = zlib.compressobj()
90 f = repo.changegroupsubset(bases, heads, 'serve')
90 f = repo.changegroupsubset(bases, heads, 'serve')
91 while 1:
91 while 1:
92 chunk = f.read(4096)
92 chunk = f.read(4096)
93 if not chunk:
93 if not chunk:
94 break
94 break
95 yield z.compress(chunk)
95 yield z.compress(chunk)
96
96
97 yield z.flush()
97 yield z.flush()
98
98
99 def capabilities(repo, req):
99 def capabilities(repo, req):
100 caps = ['lookup', 'changegroupsubset']
100 caps = ['lookup', 'changegroupsubset']
101 if repo.ui.configbool('server', 'uncompressed', untrusted=True):
101 if repo.ui.configbool('server', 'uncompressed', untrusted=True):
102 caps.append('stream=%d' % repo.changelog.version)
102 caps.append('stream=%d' % repo.changelog.version)
103 if changegroupmod.bundlepriority:
103 if changegroupmod.bundlepriority:
104 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
104 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
105 rsp = ' '.join(caps)
105 rsp = ' '.join(caps)
106 req.respond(HTTP_OK, HGTYPE, length=len(rsp))
106 req.respond(HTTP_OK, HGTYPE, length=len(rsp))
107 yield rsp
107 yield rsp
108
108
109 def unbundle(repo, req):
109 def unbundle(repo, req):
110
110
111 errorfmt = '0\n%s\n'
112 proto = req.env.get('wsgi.url_scheme') or 'http'
111 proto = req.env.get('wsgi.url_scheme') or 'http'
113 their_heads = req.form['heads'][0].split(' ')
112 their_heads = req.form['heads'][0].split(' ')
114
113
115 def check_heads():
114 def check_heads():
116 heads = map(hex, repo.heads())
115 heads = map(hex, repo.heads())
117 return their_heads == [hex('force')] or their_heads == heads
116 return their_heads == [hex('force')] or their_heads == heads
118
117
119 # fail early if possible
118 # fail early if possible
120 if not check_heads():
119 if not check_heads():
121 length = int(req.env.get('CONTENT_LENGTH', 0))
120 length = int(req.env.get('CONTENT_LENGTH', 0))
122 for s in util.filechunkiter(req, limit=length):
121 for s in util.filechunkiter(req, limit=length):
123 # drain incoming bundle, else client will not see
122 # drain incoming bundle, else client will not see
124 # response when run outside cgi script
123 # response when run outside cgi script
125 pass
124 pass
126 req.respond(HTTP_OK, HGTYPE)
125 raise ErrorResponse(HTTP_OK, 'unsynced changes')
127 return errorfmt % 'unsynced changes',
128
129 req.respond(HTTP_OK, HGTYPE)
130
126
131 # do not lock repo until all changegroup data is
127 # do not lock repo until all changegroup data is
132 # streamed. save to temporary file.
128 # streamed. save to temporary file.
133
129
134 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
130 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
135 fp = os.fdopen(fd, 'wb+')
131 fp = os.fdopen(fd, 'wb+')
136 try:
132 try:
137 length = int(req.env['CONTENT_LENGTH'])
133 length = int(req.env['CONTENT_LENGTH'])
138 for s in util.filechunkiter(req, limit=length):
134 for s in util.filechunkiter(req, limit=length):
139 fp.write(s)
135 fp.write(s)
140
136
141 try:
137 try:
142 lock = repo.lock()
138 lock = repo.lock()
143 try:
139 try:
144 if not check_heads():
140 if not check_heads():
145 return errorfmt % 'unsynced changes',
141 raise ErrorResponse(HTTP_OK, 'unsynced changes')
146
142
147 fp.seek(0)
143 fp.seek(0)
148 header = fp.read(6)
144 header = fp.read(6)
149 if header.startswith('HG') and not header.startswith('HG10'):
145 if header.startswith('HG') and not header.startswith('HG10'):
150 raise ValueError('unknown bundle version')
146 raise ValueError('unknown bundle version')
151 elif header not in changegroupmod.bundletypes:
147 elif header not in changegroupmod.bundletypes:
152 raise ValueError('unknown bundle compression type')
148 raise ValueError('unknown bundle compression type')
153 gen = changegroupmod.unbundle(header, fp)
149 gen = changegroupmod.unbundle(header, fp)
154
150
155 # send addchangegroup output to client
151 # send addchangegroup output to client
156
152
157 oldio = sys.stdout, sys.stderr
153 oldio = sys.stdout, sys.stderr
158 sys.stderr = sys.stdout = cStringIO.StringIO()
154 sys.stderr = sys.stdout = cStringIO.StringIO()
159
155
160 try:
156 try:
161 url = 'remote:%s:%s' % (proto,
157 url = 'remote:%s:%s' % (proto,
162 req.env.get('REMOTE_HOST', ''))
158 req.env.get('REMOTE_HOST', ''))
163 try:
159 try:
164 ret = repo.addchangegroup(gen, 'serve', url)
160 ret = repo.addchangegroup(gen, 'serve', url)
165 except util.Abort, inst:
161 except util.Abort, inst:
166 sys.stdout.write("abort: %s\n" % inst)
162 sys.stdout.write("abort: %s\n" % inst)
167 ret = 0
163 ret = 0
168 finally:
164 finally:
169 val = sys.stdout.getvalue()
165 val = sys.stdout.getvalue()
170 sys.stdout, sys.stderr = oldio
166 sys.stdout, sys.stderr = oldio
167 req.respond(HTTP_OK, HGTYPE)
171 return '%d\n%s' % (ret, val),
168 return '%d\n%s' % (ret, val),
172 finally:
169 finally:
173 del lock
170 del lock
174 except ValueError, inst:
171 except ValueError, inst:
175 return errorfmt % inst,
172 raise ErrorResponse(HTTP_OK, inst)
176 except (OSError, IOError), inst:
173 except (OSError, IOError), inst:
177 filename = getattr(inst, 'filename', '')
174 filename = getattr(inst, 'filename', '')
178 # Don't send our filesystem layout to the client
175 # Don't send our filesystem layout to the client
179 if filename.startswith(repo.root):
176 if filename.startswith(repo.root):
180 filename = filename[len(repo.root)+1:]
177 filename = filename[len(repo.root)+1:]
181 else:
178 else:
182 filename = ''
179 filename = ''
183 error = getattr(inst, 'strerror', 'Unknown error')
180 error = getattr(inst, 'strerror', 'Unknown error')
184 if inst.errno == errno.ENOENT:
181 if inst.errno == errno.ENOENT:
185 code = HTTP_NOT_FOUND
182 code = HTTP_NOT_FOUND
186 else:
183 else:
187 code = HTTP_SERVER_ERROR
184 code = HTTP_SERVER_ERROR
188 req.respond(code)
185 raise ErrorResponse(code, '%s: %s' % (error, filename))
189 return '0\n%s: %s\n' % (error, filename),
190 finally:
186 finally:
191 fp.close()
187 fp.close()
192 os.unlink(tempname)
188 os.unlink(tempname)
193
189
194 def stream_out(repo, req):
190 def stream_out(repo, req):
195 req.respond(HTTP_OK, HGTYPE)
191 req.respond(HTTP_OK, HGTYPE)
196 try:
192 try:
197 for chunk in streamclone.stream_out(repo, untrusted=True):
193 for chunk in streamclone.stream_out(repo, untrusted=True):
198 yield chunk
194 yield chunk
199 except streamclone.StreamException, inst:
195 except streamclone.StreamException, inst:
200 yield str(inst)
196 yield str(inst)
General Comments 0
You need to be logged in to leave comments. Login now