##// END OF EJS Templates
hgweb: abort if config file isn't found
Matt Mackall -
r13214:5bcb6c9d stable
parent child Browse files
Show More
@@ -1,357 +1,359 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
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 os, re, time, urlparse
9 import os, re, time, urlparse
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import ui, hg, util, templater
11 from mercurial import ui, hg, util, templater
12 from mercurial import error, encoding
12 from mercurial import error, encoding
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from hgweb_mod import hgweb
15 from hgweb_mod import hgweb
16 from request import wsgirequest
16 from request import wsgirequest
17 import webutil
17 import webutil
18
18
19 def cleannames(items):
19 def cleannames(items):
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21
21
22 def findrepos(paths):
22 def findrepos(paths):
23 repos = []
23 repos = []
24 for prefix, root in cleannames(paths):
24 for prefix, root in cleannames(paths):
25 roothead, roottail = os.path.split(root)
25 roothead, roottail = os.path.split(root)
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
27 # mounted as foo/subrepo
27 # mounted as foo/subrepo
28 # and "foo = /bar/**" also recurses into the subdirectories,
28 # and "foo = /bar/**" also recurses into the subdirectories,
29 # remember to use it without working dir.
29 # remember to use it without working dir.
30 try:
30 try:
31 recurse = {'*': False, '**': True}[roottail]
31 recurse = {'*': False, '**': True}[roottail]
32 except KeyError:
32 except KeyError:
33 repos.append((prefix, root))
33 repos.append((prefix, root))
34 continue
34 continue
35 roothead = os.path.normpath(os.path.abspath(roothead))
35 roothead = os.path.normpath(os.path.abspath(roothead))
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
37 path = os.path.normpath(path)
37 path = os.path.normpath(path)
38 name = util.pconvert(path[len(roothead):]).strip('/')
38 name = util.pconvert(path[len(roothead):]).strip('/')
39 if prefix:
39 if prefix:
40 name = prefix + '/' + name
40 name = prefix + '/' + name
41 repos.append((name, path))
41 repos.append((name, path))
42 return repos
42 return repos
43
43
44 class hgwebdir(object):
44 class hgwebdir(object):
45 refreshinterval = 20
45 refreshinterval = 20
46
46
47 def __init__(self, conf, baseui=None):
47 def __init__(self, conf, baseui=None):
48 self.conf = conf
48 self.conf = conf
49 self.baseui = baseui
49 self.baseui = baseui
50 self.lastrefresh = 0
50 self.lastrefresh = 0
51 self.motd = None
51 self.motd = None
52 self.refresh()
52 self.refresh()
53
53
54 def refresh(self):
54 def refresh(self):
55 if self.lastrefresh + self.refreshinterval > time.time():
55 if self.lastrefresh + self.refreshinterval > time.time():
56 return
56 return
57
57
58 if self.baseui:
58 if self.baseui:
59 u = self.baseui.copy()
59 u = self.baseui.copy()
60 else:
60 else:
61 u = ui.ui()
61 u = ui.ui()
62 u.setconfig('ui', 'report_untrusted', 'off')
62 u.setconfig('ui', 'report_untrusted', 'off')
63 u.setconfig('ui', 'interactive', 'off')
63 u.setconfig('ui', 'interactive', 'off')
64
64
65 if not isinstance(self.conf, (dict, list, tuple)):
65 if not isinstance(self.conf, (dict, list, tuple)):
66 map = {'paths': 'hgweb-paths'}
66 map = {'paths': 'hgweb-paths'}
67 if not os.path.exists(self.conf):
68 raise util.Abort(_('config file %s not found!') % self.conf)
67 u.readconfig(self.conf, remap=map, trust=True)
69 u.readconfig(self.conf, remap=map, trust=True)
68 paths = u.configitems('hgweb-paths')
70 paths = u.configitems('hgweb-paths')
69 elif isinstance(self.conf, (list, tuple)):
71 elif isinstance(self.conf, (list, tuple)):
70 paths = self.conf
72 paths = self.conf
71 elif isinstance(self.conf, dict):
73 elif isinstance(self.conf, dict):
72 paths = self.conf.items()
74 paths = self.conf.items()
73
75
74 repos = findrepos(paths)
76 repos = findrepos(paths)
75 for prefix, root in u.configitems('collections'):
77 for prefix, root in u.configitems('collections'):
76 prefix = util.pconvert(prefix)
78 prefix = util.pconvert(prefix)
77 for path in util.walkrepos(root, followsym=True):
79 for path in util.walkrepos(root, followsym=True):
78 repo = os.path.normpath(path)
80 repo = os.path.normpath(path)
79 name = util.pconvert(repo)
81 name = util.pconvert(repo)
80 if name.startswith(prefix):
82 if name.startswith(prefix):
81 name = name[len(prefix):]
83 name = name[len(prefix):]
82 repos.append((name.lstrip('/'), repo))
84 repos.append((name.lstrip('/'), repo))
83
85
84 self.repos = repos
86 self.repos = repos
85 self.ui = u
87 self.ui = u
86 encoding.encoding = self.ui.config('web', 'encoding',
88 encoding.encoding = self.ui.config('web', 'encoding',
87 encoding.encoding)
89 encoding.encoding)
88 self.style = self.ui.config('web', 'style', 'paper')
90 self.style = self.ui.config('web', 'style', 'paper')
89 self.templatepath = self.ui.config('web', 'templates', None)
91 self.templatepath = self.ui.config('web', 'templates', None)
90 self.stripecount = self.ui.config('web', 'stripes', 1)
92 self.stripecount = self.ui.config('web', 'stripes', 1)
91 if self.stripecount:
93 if self.stripecount:
92 self.stripecount = int(self.stripecount)
94 self.stripecount = int(self.stripecount)
93 self._baseurl = self.ui.config('web', 'baseurl')
95 self._baseurl = self.ui.config('web', 'baseurl')
94 self.lastrefresh = time.time()
96 self.lastrefresh = time.time()
95
97
96 def run(self):
98 def run(self):
97 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
99 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
98 raise RuntimeError("This function is only intended to be "
100 raise RuntimeError("This function is only intended to be "
99 "called while running as a CGI script.")
101 "called while running as a CGI script.")
100 import mercurial.hgweb.wsgicgi as wsgicgi
102 import mercurial.hgweb.wsgicgi as wsgicgi
101 wsgicgi.launch(self)
103 wsgicgi.launch(self)
102
104
103 def __call__(self, env, respond):
105 def __call__(self, env, respond):
104 req = wsgirequest(env, respond)
106 req = wsgirequest(env, respond)
105 return self.run_wsgi(req)
107 return self.run_wsgi(req)
106
108
107 def read_allowed(self, ui, req):
109 def read_allowed(self, ui, req):
108 """Check allow_read and deny_read config options of a repo's ui object
110 """Check allow_read and deny_read config options of a repo's ui object
109 to determine user permissions. By default, with neither option set (or
111 to determine user permissions. By default, with neither option set (or
110 both empty), allow all users to read the repo. There are two ways a
112 both empty), allow all users to read the repo. There are two ways a
111 user can be denied read access: (1) deny_read is not empty, and the
113 user can be denied read access: (1) deny_read is not empty, and the
112 user is unauthenticated or deny_read contains user (or *), and (2)
114 user is unauthenticated or deny_read contains user (or *), and (2)
113 allow_read is not empty and the user is not in allow_read. Return True
115 allow_read is not empty and the user is not in allow_read. Return True
114 if user is allowed to read the repo, else return False."""
116 if user is allowed to read the repo, else return False."""
115
117
116 user = req.env.get('REMOTE_USER')
118 user = req.env.get('REMOTE_USER')
117
119
118 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
120 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
119 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
121 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
120 return False
122 return False
121
123
122 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
124 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
123 # by default, allow reading if no allow_read option has been set
125 # by default, allow reading if no allow_read option has been set
124 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
126 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
125 return True
127 return True
126
128
127 return False
129 return False
128
130
129 def run_wsgi(self, req):
131 def run_wsgi(self, req):
130 try:
132 try:
131 try:
133 try:
132 self.refresh()
134 self.refresh()
133
135
134 virtual = req.env.get("PATH_INFO", "").strip('/')
136 virtual = req.env.get("PATH_INFO", "").strip('/')
135 tmpl = self.templater(req)
137 tmpl = self.templater(req)
136 ctype = tmpl('mimetype', encoding=encoding.encoding)
138 ctype = tmpl('mimetype', encoding=encoding.encoding)
137 ctype = templater.stringify(ctype)
139 ctype = templater.stringify(ctype)
138
140
139 # a static file
141 # a static file
140 if virtual.startswith('static/') or 'static' in req.form:
142 if virtual.startswith('static/') or 'static' in req.form:
141 if virtual.startswith('static/'):
143 if virtual.startswith('static/'):
142 fname = virtual[7:]
144 fname = virtual[7:]
143 else:
145 else:
144 fname = req.form['static'][0]
146 fname = req.form['static'][0]
145 static = templater.templatepath('static')
147 static = templater.templatepath('static')
146 return (staticfile(static, fname, req),)
148 return (staticfile(static, fname, req),)
147
149
148 # top-level index
150 # top-level index
149 elif not virtual:
151 elif not virtual:
150 req.respond(HTTP_OK, ctype)
152 req.respond(HTTP_OK, ctype)
151 return self.makeindex(req, tmpl)
153 return self.makeindex(req, tmpl)
152
154
153 # nested indexes and hgwebs
155 # nested indexes and hgwebs
154
156
155 repos = dict(self.repos)
157 repos = dict(self.repos)
156 virtualrepo = virtual
158 virtualrepo = virtual
157 while virtualrepo:
159 while virtualrepo:
158 real = repos.get(virtualrepo)
160 real = repos.get(virtualrepo)
159 if real:
161 if real:
160 req.env['REPO_NAME'] = virtualrepo
162 req.env['REPO_NAME'] = virtualrepo
161 try:
163 try:
162 repo = hg.repository(self.ui, real)
164 repo = hg.repository(self.ui, real)
163 return hgweb(repo).run_wsgi(req)
165 return hgweb(repo).run_wsgi(req)
164 except IOError, inst:
166 except IOError, inst:
165 msg = inst.strerror
167 msg = inst.strerror
166 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
168 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
167 except error.RepoError, inst:
169 except error.RepoError, inst:
168 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
170 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
169
171
170 up = virtualrepo.rfind('/')
172 up = virtualrepo.rfind('/')
171 if up < 0:
173 if up < 0:
172 break
174 break
173 virtualrepo = virtualrepo[:up]
175 virtualrepo = virtualrepo[:up]
174
176
175 # browse subdirectories
177 # browse subdirectories
176 subdir = virtual + '/'
178 subdir = virtual + '/'
177 if [r for r in repos if r.startswith(subdir)]:
179 if [r for r in repos if r.startswith(subdir)]:
178 req.respond(HTTP_OK, ctype)
180 req.respond(HTTP_OK, ctype)
179 return self.makeindex(req, tmpl, subdir)
181 return self.makeindex(req, tmpl, subdir)
180
182
181 # prefixes not found
183 # prefixes not found
182 req.respond(HTTP_NOT_FOUND, ctype)
184 req.respond(HTTP_NOT_FOUND, ctype)
183 return tmpl("notfound", repo=virtual)
185 return tmpl("notfound", repo=virtual)
184
186
185 except ErrorResponse, err:
187 except ErrorResponse, err:
186 req.respond(err, ctype)
188 req.respond(err, ctype)
187 return tmpl('error', error=err.message or '')
189 return tmpl('error', error=err.message or '')
188 finally:
190 finally:
189 tmpl = None
191 tmpl = None
190
192
191 def makeindex(self, req, tmpl, subdir=""):
193 def makeindex(self, req, tmpl, subdir=""):
192
194
193 def archivelist(ui, nodeid, url):
195 def archivelist(ui, nodeid, url):
194 allowed = ui.configlist("web", "allow_archive", untrusted=True)
196 allowed = ui.configlist("web", "allow_archive", untrusted=True)
195 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
197 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
196 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
198 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
197 untrusted=True):
199 untrusted=True):
198 yield {"type" : i[0], "extension": i[1],
200 yield {"type" : i[0], "extension": i[1],
199 "node": nodeid, "url": url}
201 "node": nodeid, "url": url}
200
202
201 def rawentries(subdir="", **map):
203 def rawentries(subdir="", **map):
202
204
203 descend = self.ui.configbool('web', 'descend', True)
205 descend = self.ui.configbool('web', 'descend', True)
204 for name, path in self.repos:
206 for name, path in self.repos:
205
207
206 if not name.startswith(subdir):
208 if not name.startswith(subdir):
207 continue
209 continue
208 name = name[len(subdir):]
210 name = name[len(subdir):]
209 if not descend and '/' in name:
211 if not descend and '/' in name:
210 continue
212 continue
211
213
212 u = self.ui.copy()
214 u = self.ui.copy()
213 try:
215 try:
214 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
216 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
215 except Exception, e:
217 except Exception, e:
216 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
218 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
217 continue
219 continue
218 def get(section, name, default=None):
220 def get(section, name, default=None):
219 return u.config(section, name, default, untrusted=True)
221 return u.config(section, name, default, untrusted=True)
220
222
221 if u.configbool("web", "hidden", untrusted=True):
223 if u.configbool("web", "hidden", untrusted=True):
222 continue
224 continue
223
225
224 if not self.read_allowed(u, req):
226 if not self.read_allowed(u, req):
225 continue
227 continue
226
228
227 parts = [name]
229 parts = [name]
228 if 'PATH_INFO' in req.env:
230 if 'PATH_INFO' in req.env:
229 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
231 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
230 if req.env['SCRIPT_NAME']:
232 if req.env['SCRIPT_NAME']:
231 parts.insert(0, req.env['SCRIPT_NAME'])
233 parts.insert(0, req.env['SCRIPT_NAME'])
232 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
234 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
233
235
234 # update time with local timezone
236 # update time with local timezone
235 try:
237 try:
236 r = hg.repository(self.ui, path)
238 r = hg.repository(self.ui, path)
237 except error.RepoError:
239 except error.RepoError:
238 u.warn(_('error accessing repository at %s\n') % path)
240 u.warn(_('error accessing repository at %s\n') % path)
239 continue
241 continue
240 try:
242 try:
241 d = (get_mtime(r.spath), util.makedate()[1])
243 d = (get_mtime(r.spath), util.makedate()[1])
242 except OSError:
244 except OSError:
243 continue
245 continue
244
246
245 contact = get_contact(get)
247 contact = get_contact(get)
246 description = get("web", "description", "")
248 description = get("web", "description", "")
247 name = get("web", "name", name)
249 name = get("web", "name", name)
248 row = dict(contact=contact or "unknown",
250 row = dict(contact=contact or "unknown",
249 contact_sort=contact.upper() or "unknown",
251 contact_sort=contact.upper() or "unknown",
250 name=name,
252 name=name,
251 name_sort=name,
253 name_sort=name,
252 url=url,
254 url=url,
253 description=description or "unknown",
255 description=description or "unknown",
254 description_sort=description.upper() or "unknown",
256 description_sort=description.upper() or "unknown",
255 lastchange=d,
257 lastchange=d,
256 lastchange_sort=d[1]-d[0],
258 lastchange_sort=d[1]-d[0],
257 archives=archivelist(u, "tip", url))
259 archives=archivelist(u, "tip", url))
258 yield row
260 yield row
259
261
260 sortdefault = None, False
262 sortdefault = None, False
261 def entries(sortcolumn="", descending=False, subdir="", **map):
263 def entries(sortcolumn="", descending=False, subdir="", **map):
262 rows = rawentries(subdir=subdir, **map)
264 rows = rawentries(subdir=subdir, **map)
263
265
264 if sortcolumn and sortdefault != (sortcolumn, descending):
266 if sortcolumn and sortdefault != (sortcolumn, descending):
265 sortkey = '%s_sort' % sortcolumn
267 sortkey = '%s_sort' % sortcolumn
266 rows = sorted(rows, key=lambda x: x[sortkey],
268 rows = sorted(rows, key=lambda x: x[sortkey],
267 reverse=descending)
269 reverse=descending)
268 for row, parity in zip(rows, paritygen(self.stripecount)):
270 for row, parity in zip(rows, paritygen(self.stripecount)):
269 row['parity'] = parity
271 row['parity'] = parity
270 yield row
272 yield row
271
273
272 self.refresh()
274 self.refresh()
273 sortable = ["name", "description", "contact", "lastchange"]
275 sortable = ["name", "description", "contact", "lastchange"]
274 sortcolumn, descending = sortdefault
276 sortcolumn, descending = sortdefault
275 if 'sort' in req.form:
277 if 'sort' in req.form:
276 sortcolumn = req.form['sort'][0]
278 sortcolumn = req.form['sort'][0]
277 descending = sortcolumn.startswith('-')
279 descending = sortcolumn.startswith('-')
278 if descending:
280 if descending:
279 sortcolumn = sortcolumn[1:]
281 sortcolumn = sortcolumn[1:]
280 if sortcolumn not in sortable:
282 if sortcolumn not in sortable:
281 sortcolumn = ""
283 sortcolumn = ""
282
284
283 sort = [("sort_%s" % column,
285 sort = [("sort_%s" % column,
284 "%s%s" % ((not descending and column == sortcolumn)
286 "%s%s" % ((not descending and column == sortcolumn)
285 and "-" or "", column))
287 and "-" or "", column))
286 for column in sortable]
288 for column in sortable]
287
289
288 self.refresh()
290 self.refresh()
289 self.updatereqenv(req.env)
291 self.updatereqenv(req.env)
290
292
291 return tmpl("index", entries=entries, subdir=subdir,
293 return tmpl("index", entries=entries, subdir=subdir,
292 sortcolumn=sortcolumn, descending=descending,
294 sortcolumn=sortcolumn, descending=descending,
293 **dict(sort))
295 **dict(sort))
294
296
295 def templater(self, req):
297 def templater(self, req):
296
298
297 def header(**map):
299 def header(**map):
298 yield tmpl('header', encoding=encoding.encoding, **map)
300 yield tmpl('header', encoding=encoding.encoding, **map)
299
301
300 def footer(**map):
302 def footer(**map):
301 yield tmpl("footer", **map)
303 yield tmpl("footer", **map)
302
304
303 def motd(**map):
305 def motd(**map):
304 if self.motd is not None:
306 if self.motd is not None:
305 yield self.motd
307 yield self.motd
306 else:
308 else:
307 yield config('web', 'motd', '')
309 yield config('web', 'motd', '')
308
310
309 def config(section, name, default=None, untrusted=True):
311 def config(section, name, default=None, untrusted=True):
310 return self.ui.config(section, name, default, untrusted)
312 return self.ui.config(section, name, default, untrusted)
311
313
312 self.updatereqenv(req.env)
314 self.updatereqenv(req.env)
313
315
314 url = req.env.get('SCRIPT_NAME', '')
316 url = req.env.get('SCRIPT_NAME', '')
315 if not url.endswith('/'):
317 if not url.endswith('/'):
316 url += '/'
318 url += '/'
317
319
318 vars = {}
320 vars = {}
319 styles = (
321 styles = (
320 req.form.get('style', [None])[0],
322 req.form.get('style', [None])[0],
321 config('web', 'style'),
323 config('web', 'style'),
322 'paper'
324 'paper'
323 )
325 )
324 style, mapfile = templater.stylemap(styles, self.templatepath)
326 style, mapfile = templater.stylemap(styles, self.templatepath)
325 if style == styles[0]:
327 if style == styles[0]:
326 vars['style'] = style
328 vars['style'] = style
327
329
328 start = url[-1] == '?' and '&' or '?'
330 start = url[-1] == '?' and '&' or '?'
329 sessionvars = webutil.sessionvars(vars, start)
331 sessionvars = webutil.sessionvars(vars, start)
330 staticurl = config('web', 'staticurl') or url + 'static/'
332 staticurl = config('web', 'staticurl') or url + 'static/'
331 if not staticurl.endswith('/'):
333 if not staticurl.endswith('/'):
332 staticurl += '/'
334 staticurl += '/'
333
335
334 tmpl = templater.templater(mapfile,
336 tmpl = templater.templater(mapfile,
335 defaults={"header": header,
337 defaults={"header": header,
336 "footer": footer,
338 "footer": footer,
337 "motd": motd,
339 "motd": motd,
338 "url": url,
340 "url": url,
339 "staticurl": staticurl,
341 "staticurl": staticurl,
340 "sessionvars": sessionvars})
342 "sessionvars": sessionvars})
341 return tmpl
343 return tmpl
342
344
343 def updatereqenv(self, env):
345 def updatereqenv(self, env):
344 def splitnetloc(netloc):
346 def splitnetloc(netloc):
345 if ':' in netloc:
347 if ':' in netloc:
346 return netloc.split(':', 1)
348 return netloc.split(':', 1)
347 else:
349 else:
348 return (netloc, None)
350 return (netloc, None)
349
351
350 if self._baseurl is not None:
352 if self._baseurl is not None:
351 urlcomp = urlparse.urlparse(self._baseurl)
353 urlcomp = urlparse.urlparse(self._baseurl)
352 host, port = splitnetloc(urlcomp[1])
354 host, port = splitnetloc(urlcomp[1])
353 path = urlcomp[2]
355 path = urlcomp[2]
354 env['SERVER_NAME'] = host
356 env['SERVER_NAME'] = host
355 if port:
357 if port:
356 env['SERVER_PORT'] = port
358 env['SERVER_PORT'] = port
357 env['SCRIPT_NAME'] = path
359 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now