mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-19 21:03:21 -07:00
Bump cherrypy from 18.6.1 to 18.8.0 (#1796)
* Bump cherrypy from 18.6.1 to 18.8.0 Bumps [cherrypy](https://github.com/cherrypy/cherrypy) from 18.6.1 to 18.8.0. - [Release notes](https://github.com/cherrypy/cherrypy/releases) - [Changelog](https://github.com/cherrypy/cherrypy/blob/main/CHANGES.rst) - [Commits](https://github.com/cherrypy/cherrypy/compare/v18.6.1...v18.8.0) --- updated-dependencies: - dependency-name: cherrypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * Update cherrypy==18.8.0 Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
This commit is contained in:
parent
e79da07973
commit
76cc56a215
75 changed files with 19150 additions and 1339 deletions
|
@ -206,12 +206,8 @@ except ImportError:
|
|||
def test_callable_spec(callable, args, kwargs): # noqa: F811
|
||||
return None
|
||||
else:
|
||||
getargspec = inspect.getargspec
|
||||
# Python 3 requires using getfullargspec if
|
||||
# keyword-only arguments are present
|
||||
if hasattr(inspect, 'getfullargspec'):
|
||||
def getargspec(callable):
|
||||
return inspect.getfullargspec(callable)[:4]
|
||||
def getargspec(callable):
|
||||
return inspect.getfullargspec(callable)[:4]
|
||||
|
||||
|
||||
class LateParamPageHandler(PageHandler):
|
||||
|
|
|
@ -466,7 +466,7 @@ _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
|
|||
<pre id="traceback">%(traceback)s</pre>
|
||||
<div id="powered_by">
|
||||
<span>
|
||||
Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a>
|
||||
Powered by <a href="http://www.cherrypy.dev">CherryPy %(version)s</a>
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -532,7 +532,8 @@ def get_error_page(status, **kwargs):
|
|||
return result
|
||||
else:
|
||||
# Load the template from this path.
|
||||
template = io.open(error_page, newline='').read()
|
||||
with io.open(error_page, newline='') as f:
|
||||
template = f.read()
|
||||
except Exception:
|
||||
e = _format_exception(*_exc_info())[-1]
|
||||
m = kwargs['message']
|
||||
|
|
|
@ -339,11 +339,8 @@ LoadModule python_module modules/mod_python.so
|
|||
}
|
||||
|
||||
mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf')
|
||||
f = open(mpconf, 'wb')
|
||||
try:
|
||||
with open(mpconf, 'wb') as f:
|
||||
f.write(conf_data)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
response = read_process(self.apache_path, '-k start -f %s' % mpconf)
|
||||
self.ready = True
|
||||
|
|
|
@ -169,7 +169,7 @@ def request_namespace(k, v):
|
|||
def response_namespace(k, v):
|
||||
"""Attach response attributes declared in config."""
|
||||
# Provides config entries to set default response headers
|
||||
# http://cherrypy.org/ticket/889
|
||||
# http://cherrypy.dev/ticket/889
|
||||
if k[:8] == 'headers.':
|
||||
cherrypy.serving.response.headers[k.split('.', 1)[1]] = v
|
||||
else:
|
||||
|
@ -252,7 +252,7 @@ class Request(object):
|
|||
The query component of the Request-URI, a string of information to be
|
||||
interpreted by the resource. The query portion of a URI follows the
|
||||
path component, and is separated by a '?'. For example, the URI
|
||||
'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
|
||||
'http://www.cherrypy.dev/wiki?a=3&b=4' has the query component,
|
||||
'a=3&b=4'."""
|
||||
|
||||
query_string_encoding = 'utf8'
|
||||
|
@ -742,6 +742,9 @@ class Request(object):
|
|||
if self.protocol >= (1, 1):
|
||||
msg = "HTTP/1.1 requires a 'Host' request header."
|
||||
raise cherrypy.HTTPError(400, msg)
|
||||
else:
|
||||
headers['Host'] = httputil.SanitizedHost(dict.get(headers, 'Host'))
|
||||
|
||||
host = dict.get(headers, 'Host')
|
||||
if not host:
|
||||
host = self.local.name or self.local.ip
|
||||
|
|
|
@ -101,13 +101,12 @@ def get_ha1_file_htdigest(filename):
|
|||
"""
|
||||
def get_ha1(realm, username):
|
||||
result = None
|
||||
f = open(filename, 'r')
|
||||
for line in f:
|
||||
u, r, ha1 = line.rstrip().split(':')
|
||||
if u == username and r == realm:
|
||||
result = ha1
|
||||
break
|
||||
f.close()
|
||||
with open(filename, 'r') as f:
|
||||
for line in f:
|
||||
u, r, ha1 = line.rstrip().split(':')
|
||||
if u == username and r == realm:
|
||||
result = ha1
|
||||
break
|
||||
return result
|
||||
|
||||
return get_ha1
|
||||
|
|
|
@ -334,9 +334,10 @@ class CoverStats(object):
|
|||
yield '</body></html>'
|
||||
|
||||
def annotated_file(self, filename, statements, excluded, missing):
|
||||
source = open(filename, 'r')
|
||||
with open(filename, 'r') as source:
|
||||
lines = source.readlines()
|
||||
buffer = []
|
||||
for lineno, line in enumerate(source.readlines()):
|
||||
for lineno, line in enumerate(lines):
|
||||
lineno += 1
|
||||
line = line.strip('\n\r')
|
||||
empty_the_buffer = True
|
||||
|
|
|
@ -516,3 +516,33 @@ class Host(object):
|
|||
|
||||
def __repr__(self):
|
||||
return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name)
|
||||
|
||||
|
||||
class SanitizedHost(str):
|
||||
r"""
|
||||
Wraps a raw host header received from the network in
|
||||
a sanitized version that elides dangerous characters.
|
||||
|
||||
>>> SanitizedHost('foo\nbar')
|
||||
'foobar'
|
||||
>>> SanitizedHost('foo\nbar').raw
|
||||
'foo\nbar'
|
||||
|
||||
A SanitizedInstance is only returned if sanitization was performed.
|
||||
|
||||
>>> isinstance(SanitizedHost('foobar'), SanitizedHost)
|
||||
False
|
||||
"""
|
||||
dangerous = re.compile(r'[\n\r]')
|
||||
|
||||
def __new__(cls, raw):
|
||||
sanitized = cls._sanitize(raw)
|
||||
if sanitized == raw:
|
||||
return raw
|
||||
instance = super().__new__(cls, sanitized)
|
||||
instance.raw = raw
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def _sanitize(cls, raw):
|
||||
return cls.dangerous.sub('', raw)
|
||||
|
|
|
@ -163,11 +163,8 @@ class Parser(configparser.ConfigParser):
|
|||
# fp = open(filename)
|
||||
# except IOError:
|
||||
# continue
|
||||
fp = open(filename)
|
||||
try:
|
||||
with open(filename) as fp:
|
||||
self._read(fp, filename)
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
def as_dict(self, raw=False, vars=None):
|
||||
"""Convert an INI file to a dictionary"""
|
||||
|
|
|
@ -516,11 +516,8 @@ class FileSession(Session):
|
|||
if path is None:
|
||||
path = self._get_file_path()
|
||||
try:
|
||||
f = open(path, 'rb')
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
return pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
except (IOError, EOFError):
|
||||
e = sys.exc_info()[1]
|
||||
if self.debug:
|
||||
|
@ -531,11 +528,8 @@ class FileSession(Session):
|
|||
def _save(self, expiration_time):
|
||||
assert self.locked, ('The session was saved without being locked. '
|
||||
"Check your tools' priority levels.")
|
||||
f = open(self._get_file_path(), 'wb')
|
||||
try:
|
||||
with open(self._get_file_path(), 'wb') as f:
|
||||
pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def _delete(self):
|
||||
assert self.locked, ('The session deletion without being locked. '
|
||||
|
|
|
@ -436,7 +436,8 @@ class PIDFile(SimplePlugin):
|
|||
if self.finalized:
|
||||
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
|
||||
else:
|
||||
open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8'))
|
||||
with open(self.pidfile, 'wb') as f:
|
||||
f.write(ntob('%s\n' % pid, 'utf8'))
|
||||
self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
|
||||
self.finalized = True
|
||||
start.priority = 70
|
||||
|
|
|
@ -505,7 +505,8 @@ server.ssl_private_key: r'%s'
|
|||
|
||||
def get_pid(self):
|
||||
if self.daemonize:
|
||||
return int(open(self.pid_file, 'rb').read())
|
||||
with open(self.pid_file, 'rb') as f:
|
||||
return int(f.read())
|
||||
return self._proc.pid
|
||||
|
||||
def join(self):
|
||||
|
|
|
@ -97,7 +97,8 @@ class LogCase(object):
|
|||
|
||||
def emptyLog(self):
|
||||
"""Overwrite self.logfile with 0 bytes."""
|
||||
open(self.logfile, 'wb').write('')
|
||||
with open(self.logfile, 'wb') as f:
|
||||
f.write('')
|
||||
|
||||
def markLog(self, key=None):
|
||||
"""Insert a marker line into the log and set self.lastmarker."""
|
||||
|
@ -105,10 +106,11 @@ class LogCase(object):
|
|||
key = str(time.time())
|
||||
self.lastmarker = key
|
||||
|
||||
open(self.logfile, 'ab+').write(
|
||||
b'%s%s\n'
|
||||
% (self.markerPrefix, key.encode('utf-8'))
|
||||
)
|
||||
with open(self.logfile, 'ab+') as f:
|
||||
f.write(
|
||||
b'%s%s\n'
|
||||
% (self.markerPrefix, key.encode('utf-8'))
|
||||
)
|
||||
|
||||
def _read_marked_region(self, marker=None):
|
||||
"""Return lines from self.logfile in the marked region.
|
||||
|
@ -122,20 +124,23 @@ class LogCase(object):
|
|||
logfile = self.logfile
|
||||
marker = marker or self.lastmarker
|
||||
if marker is None:
|
||||
return open(logfile, 'rb').readlines()
|
||||
with open(logfile, 'rb') as f:
|
||||
return f.readlines()
|
||||
|
||||
if isinstance(marker, str):
|
||||
marker = marker.encode('utf-8')
|
||||
data = []
|
||||
in_region = False
|
||||
for line in open(logfile, 'rb'):
|
||||
if in_region:
|
||||
if line.startswith(self.markerPrefix) and marker not in line:
|
||||
break
|
||||
else:
|
||||
data.append(line)
|
||||
elif marker in line:
|
||||
in_region = True
|
||||
with open(logfile, 'rb') as f:
|
||||
for line in f:
|
||||
if in_region:
|
||||
if (line.startswith(self.markerPrefix)
|
||||
and marker not in line):
|
||||
break
|
||||
else:
|
||||
data.append(line)
|
||||
elif marker in line:
|
||||
in_region = True
|
||||
return data
|
||||
|
||||
def assertInLog(self, line, marker=None):
|
||||
|
|
|
@ -14,7 +14,7 @@ KNOWN BUGS
|
|||
|
||||
1. Apache processes Range headers automatically; CherryPy's truncated
|
||||
output is then truncated again by Apache. See test_core.testRanges.
|
||||
This was worked around in http://www.cherrypy.org/changeset/1319.
|
||||
This was worked around in http://www.cherrypy.dev/changeset/1319.
|
||||
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
||||
See test_core.testHTTPMethods.
|
||||
3. Max request header and body settings do not work with Apache.
|
||||
|
@ -112,15 +112,12 @@ class ModFCGISupervisor(helper.LocalWSGISupervisor):
|
|||
fcgiconf = os.path.join(curdir, fcgiconf)
|
||||
|
||||
# Write the Apache conf file.
|
||||
f = open(fcgiconf, 'wb')
|
||||
try:
|
||||
with open(fcgiconf, 'wb') as f:
|
||||
server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
|
||||
output = self.template % {'port': self.port, 'root': curdir,
|
||||
'server': server}
|
||||
output = output.replace('\r\n', '\n')
|
||||
f.write(output)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
|
||||
if result:
|
||||
|
|
|
@ -14,7 +14,7 @@ KNOWN BUGS
|
|||
|
||||
1. Apache processes Range headers automatically; CherryPy's truncated
|
||||
output is then truncated again by Apache. See test_core.testRanges.
|
||||
This was worked around in http://www.cherrypy.org/changeset/1319.
|
||||
This was worked around in http://www.cherrypy.dev/changeset/1319.
|
||||
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
||||
See test_core.testHTTPMethods.
|
||||
3. Max request header and body settings do not work with Apache.
|
||||
|
@ -101,15 +101,12 @@ class ModFCGISupervisor(helper.LocalSupervisor):
|
|||
fcgiconf = os.path.join(curdir, fcgiconf)
|
||||
|
||||
# Write the Apache conf file.
|
||||
f = open(fcgiconf, 'wb')
|
||||
try:
|
||||
with open(fcgiconf, 'wb') as f:
|
||||
server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
|
||||
output = self.template % {'port': self.port, 'root': curdir,
|
||||
'server': server}
|
||||
output = ntob(output.replace('\r\n', '\n'))
|
||||
f.write(output)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
|
||||
if result:
|
||||
|
|
|
@ -15,7 +15,7 @@ KNOWN BUGS
|
|||
|
||||
1. Apache processes Range headers automatically; CherryPy's truncated
|
||||
output is then truncated again by Apache. See test_core.testRanges.
|
||||
This was worked around in http://www.cherrypy.org/changeset/1319.
|
||||
This was worked around in http://www.cherrypy.dev/changeset/1319.
|
||||
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
||||
See test_core.testHTTPMethods.
|
||||
3. Max request header and body settings do not work with Apache.
|
||||
|
@ -107,13 +107,10 @@ class ModPythonSupervisor(helper.Supervisor):
|
|||
if not os.path.isabs(mpconf):
|
||||
mpconf = os.path.join(curdir, mpconf)
|
||||
|
||||
f = open(mpconf, 'wb')
|
||||
try:
|
||||
with open(mpconf, 'wb') as f:
|
||||
f.write(self.template %
|
||||
{'port': self.port, 'modulename': modulename,
|
||||
'host': self.host})
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
|
||||
if result:
|
||||
|
|
|
@ -11,7 +11,7 @@ KNOWN BUGS
|
|||
|
||||
1. Apache processes Range headers automatically; CherryPy's truncated
|
||||
output is then truncated again by Apache. See test_core.testRanges.
|
||||
This was worked around in http://www.cherrypy.org/changeset/1319.
|
||||
This was worked around in http://www.cherrypy.dev/changeset/1319.
|
||||
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
||||
See test_core.testHTTPMethods.
|
||||
3. Max request header and body settings do not work with Apache.
|
||||
|
@ -109,14 +109,11 @@ class ModWSGISupervisor(helper.Supervisor):
|
|||
if not os.path.isabs(mpconf):
|
||||
mpconf = os.path.join(curdir, mpconf)
|
||||
|
||||
f = open(mpconf, 'wb')
|
||||
try:
|
||||
with open(mpconf, 'wb') as f:
|
||||
output = (self.template %
|
||||
{'port': self.port, 'testmod': modulename,
|
||||
'curdir': curdir})
|
||||
f.write(output)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
|
||||
if result:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This file is part of CherryPy <http://www.cherrypy.org/>
|
||||
# This file is part of CherryPy <http://www.cherrypy.dev/>
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This file is part of CherryPy <http://www.cherrypy.org/>
|
||||
# This file is part of CherryPy <http://www.cherrypy.dev/>
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
|
||||
|
||||
|
|
|
@ -586,9 +586,8 @@ class CoreRequestHandlingTest(helper.CPWebCase):
|
|||
def testFavicon(self):
|
||||
# favicon.ico is served by staticfile.
|
||||
icofilename = os.path.join(localDir, '../favicon.ico')
|
||||
icofile = open(icofilename, 'rb')
|
||||
data = icofile.read()
|
||||
icofile.close()
|
||||
with open(icofilename, 'rb') as icofile:
|
||||
data = icofile.read()
|
||||
|
||||
self.getPage('/favicon.ico')
|
||||
self.assertBody(data)
|
||||
|
|
|
@ -46,7 +46,7 @@ class EncodingTests(helper.CPWebCase):
|
|||
# any part which is unicode (even ascii), the response
|
||||
# should not fail.
|
||||
cherrypy.response.cookie['candy'] = 'bar'
|
||||
cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org'
|
||||
cherrypy.response.cookie['candy']['domain'] = 'cherrypy.dev'
|
||||
cherrypy.response.headers[
|
||||
'Some-Header'] = 'My d\xc3\xb6g has fleas'
|
||||
cherrypy.response.headers[
|
||||
|
|
|
@ -113,7 +113,7 @@ def test_normal_return(log_tracker, server):
|
|||
resp = requests.get(
|
||||
'http://%s:%s/as_string' % (host, port),
|
||||
headers={
|
||||
'Referer': 'http://www.cherrypy.org/',
|
||||
'Referer': 'http://www.cherrypy.dev/',
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
},
|
||||
)
|
||||
|
@ -135,7 +135,7 @@ def test_normal_return(log_tracker, server):
|
|||
log_tracker.assertLog(
|
||||
-1,
|
||||
'] "GET /as_string HTTP/1.1" 200 %s '
|
||||
'"http://www.cherrypy.org/" "Mozilla/5.0"'
|
||||
'"http://www.cherrypy.dev/" "Mozilla/5.0"'
|
||||
% content_length,
|
||||
)
|
||||
|
||||
|
|
|
@ -342,7 +342,7 @@ class RequestObjectTests(helper.CPWebCase):
|
|||
self.assertBody('/pathinfo/foo/bar')
|
||||
|
||||
def testAbsoluteURIPathInfo(self):
|
||||
# http://cherrypy.org/ticket/1061
|
||||
# http://cherrypy.dev/ticket/1061
|
||||
self.getPage('http://localhost/pathinfo/foo/bar')
|
||||
self.assertBody('/pathinfo/foo/bar')
|
||||
|
||||
|
@ -375,10 +375,10 @@ class RequestObjectTests(helper.CPWebCase):
|
|||
|
||||
# Make sure that encoded = and & get parsed correctly
|
||||
self.getPage(
|
||||
'/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2')
|
||||
'/params/code?url=http%3A//cherrypy.dev/index%3Fa%3D1%26b%3D2')
|
||||
self.assertBody('args: %s kwargs: %s' %
|
||||
(('code',),
|
||||
[('url', ntou('http://cherrypy.org/index?a=1&b=2'))]))
|
||||
[('url', ntou('http://cherrypy.dev/index?a=1&b=2'))]))
|
||||
|
||||
# Test coordinates sent by <img ismap>
|
||||
self.getPage('/params/ismap?223,114')
|
||||
|
@ -756,6 +756,16 @@ class RequestObjectTests(helper.CPWebCase):
|
|||
headers=[('Content-type', 'application/json')])
|
||||
self.assertBody('application/json')
|
||||
|
||||
def test_dangerous_host(self):
|
||||
"""
|
||||
Dangerous characters like newlines should be elided.
|
||||
Ref #1974.
|
||||
"""
|
||||
# foo\nbar
|
||||
encoded = '=?iso-8859-1?q?foo=0Abar?='
|
||||
self.getPage('/headers/Host', headers=[('Host', encoded)])
|
||||
self.assertBody('foobar')
|
||||
|
||||
def test_basic_HTTPMethods(self):
|
||||
helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND',
|
||||
'PATCH')
|
||||
|
|
|
@ -424,11 +424,12 @@ test_case_name: "test_signal_handler_unsubscribe"
|
|||
p.join()
|
||||
|
||||
# Assert the old handler ran.
|
||||
log_lines = list(open(p.error_log, 'rb'))
|
||||
assert any(
|
||||
line.endswith(b'I am an old SIGTERM handler.\n')
|
||||
for line in log_lines
|
||||
)
|
||||
with open(p.error_log, 'rb') as f:
|
||||
log_lines = list(f)
|
||||
assert any(
|
||||
line.endswith(b'I am an old SIGTERM handler.\n')
|
||||
for line in log_lines
|
||||
)
|
||||
|
||||
|
||||
def test_safe_wait_INADDR_ANY(): # pylint: disable=invalid-name
|
||||
|
|
|
@ -78,7 +78,7 @@ class TutorialTest(helper.CPWebCase):
|
|||
|
||||
<ul>
|
||||
<li><a href="http://del.icio.us">del.icio.us</a></li>
|
||||
<li><a href="http://www.cherrypy.org">CherryPy</a></li>
|
||||
<li><a href="http://www.cherrypy.dev">CherryPy</a></li>
|
||||
</ul>
|
||||
|
||||
<p>[<a href="../">Return to links page</a>]</p>'''
|
||||
|
@ -166,7 +166,7 @@ class TutorialTest(helper.CPWebCase):
|
|||
self.assertHeader('Content-Disposition',
|
||||
# Make sure the filename is quoted.
|
||||
'attachment; filename="pdf_file.pdf"')
|
||||
self.assertEqual(len(self.body), 85698)
|
||||
self.assertEqual(len(self.body), 11961)
|
||||
|
||||
def test10HTTPErrors(self):
|
||||
self.setup_tutorial('tut10_http_errors', 'HTTPErrorDemo')
|
||||
|
|
Binary file not shown.
|
@ -53,7 +53,7 @@ class LinksPage:
|
|||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="http://www.cherrypy.org">The CherryPy Homepage</a>
|
||||
<a href="http://www.cherrypy.dev">The CherryPy Homepage</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="http://www.python.org">The Python Homepage</a>
|
||||
|
@ -77,7 +77,7 @@ class ExtraLinksPage:
|
|||
|
||||
<ul>
|
||||
<li><a href="http://del.icio.us">del.icio.us</a></li>
|
||||
<li><a href="http://www.cherrypy.org">CherryPy</a></li>
|
||||
<li><a href="http://www.cherrypy.dev">CherryPy</a></li>
|
||||
</ul>
|
||||
|
||||
<p>[<a href="../">Return to links page</a>]</p>'''
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue