Merge branch 'development'
138
Mylar.py
|
@ -25,7 +25,7 @@ sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'lib'))
|
|||
|
||||
import mylar
|
||||
|
||||
from mylar import webstart, logger, filechecker, versioncheck
|
||||
from mylar import webstart, logger, filechecker, versioncheck, maintenance
|
||||
|
||||
import argparse
|
||||
|
||||
|
@ -62,9 +62,17 @@ def main():
|
|||
if not mylar.SYS_ENCODING or mylar.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
||||
mylar.SYS_ENCODING = 'UTF-8'
|
||||
|
||||
if not logger.LOG_LANG.startswith('en'):
|
||||
print 'language detected as non-English (%s). Forcing specific logging module - errors WILL NOT be captured in the logs' % logger.LOG_LANG
|
||||
else:
|
||||
print 'log language set to %s' % logger.LOG_LANG
|
||||
|
||||
# Set up and gather command line arguments
|
||||
parser = argparse.ArgumentParser(description='Automated Comic Book Downloader')
|
||||
subparsers = parser.add_subparsers(title='Subcommands', dest='maintenance')
|
||||
parser_maintenance = subparsers.add_parser('maintenance', help='Enter maintenance mode (no GUI). Additional commands are available (maintenance --help)')
|
||||
|
||||
#main parser
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='Increase console logging verbosity')
|
||||
parser.add_argument('-q', '--quiet', action='store_true', help='Turn off console logging')
|
||||
parser.add_argument('-d', '--daemon', action='store_true', help='Run as a daemon')
|
||||
|
@ -76,24 +84,32 @@ def main():
|
|||
parser.add_argument('--nolaunch', action='store_true', help='Prevent browser from launching on startup')
|
||||
parser.add_argument('--pidfile', help='Create a pid file (only relevant when running as a daemon)')
|
||||
parser.add_argument('--safe', action='store_true', help='redirect the startup page to point to the Manage Comics screen on startup')
|
||||
#parser.add_argument('-u', '--update', action='store_true', help='force mylar to perform an update as if in GUI')
|
||||
parser_maintenance.add_argument('-xj', '--exportjson', action='store', help='Export existing mylar.db to json file')
|
||||
parser_maintenance.add_argument('-id', '--importdatabase', action='store', help='Import a mylar.db into current db')
|
||||
parser_maintenance.add_argument('-ij', '--importjson', action='store', help='Import a specified json file containing just {"ComicID": "XXXXX"} into current db')
|
||||
parser_maintenance.add_argument('-st', '--importstatus', action='store_true', help='Provide current maintenance status')
|
||||
parser_maintenance.add_argument('-u', '--update', action='store_true', help='force mylar to perform an update as if in GUI')
|
||||
#parser_maintenance.add_argument('-it', '--importtext', action='store', help='Import a specified text file into current db')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.maintenance:
|
||||
if all([args.exportjson is None, args.importdatabase is None, args.importjson is None, args.importstatus is False, args.update is False]):
|
||||
print 'Expecting subcommand with the maintenance positional argumeent'
|
||||
sys.exit()
|
||||
mylar.MAINTENANCE = True
|
||||
else:
|
||||
mylar.MAINTENANCE = False
|
||||
|
||||
if args.verbose:
|
||||
mylar.VERBOSE = True
|
||||
if args.quiet:
|
||||
print 'Verbose/Debugging mode enabled...'
|
||||
mylar.LOG_LEVEL = 2
|
||||
elif args.quiet:
|
||||
mylar.QUIET = True
|
||||
|
||||
# Do an intial setup of the logger.
|
||||
logger.initLogger(console=not mylar.QUIET, log_dir=False, init=True, verbose=mylar.VERBOSE)
|
||||
|
||||
#if args.update:
|
||||
# print('Attempting to update Mylar so things can work again...')
|
||||
# try:
|
||||
# versioncheck.update()
|
||||
# except Exception, e:
|
||||
# sys.exit('Mylar failed to update.')
|
||||
print 'Quiet logging mode enabled...'
|
||||
mylar.LOG_LEVEL = 0
|
||||
else:
|
||||
mylar.LOG_LEVEL = 1
|
||||
|
||||
if args.daemon:
|
||||
if sys.platform == 'win32':
|
||||
|
@ -116,7 +132,7 @@ def main():
|
|||
except IOError, e:
|
||||
raise SystemExit("Unable to write PID file: %s [%d]" % (e.strerror, e.errno))
|
||||
else:
|
||||
logger.warn("Not running in daemon mode. PID file creation disabled.")
|
||||
print("Not running in daemon mode. PID file creation disabled.")
|
||||
|
||||
if args.datadir:
|
||||
mylar.DATA_DIR = args.datadir
|
||||
|
@ -138,22 +154,26 @@ def main():
|
|||
else:
|
||||
mylar.NOWEEKLY = False
|
||||
|
||||
# Try to create the DATA_DIR if it doesn't exist
|
||||
#if not os.path.exists(mylar.DATA_DIR):
|
||||
# try:
|
||||
# os.makedirs(mylar.DATA_DIR)
|
||||
# except OSError:
|
||||
# raise SystemExit('Could not create data directory: ' + mylar.DATA_DIR + '. Exiting....')
|
||||
|
||||
filechecker.validateAndCreateDirectory(mylar.DATA_DIR, True)
|
||||
|
||||
# Make sure the DATA_DIR is writeable
|
||||
if not os.access(mylar.DATA_DIR, os.W_OK):
|
||||
raise SystemExit('Cannot write to the data directory: ' + mylar.DATA_DIR + '. Exiting...')
|
||||
|
||||
# Put the database in the DATA_DIR
|
||||
mylar.DB_FILE = os.path.join(mylar.DATA_DIR, 'mylar.db')
|
||||
|
||||
# Read config and start logging
|
||||
if mylar.MAINTENANCE is False:
|
||||
print('Initializing startup sequence....')
|
||||
|
||||
#try:
|
||||
mylar.initialize(mylar.CONFIG_FILE)
|
||||
#except Exception as e:
|
||||
# print e
|
||||
# raise SystemExit('FATAL ERROR')
|
||||
|
||||
if mylar.MAINTENANCE is False:
|
||||
filechecker.validateAndCreateDirectory(mylar.DATA_DIR, True)
|
||||
|
||||
# Make sure the DATA_DIR is writeable
|
||||
if not os.access(mylar.DATA_DIR, os.W_OK):
|
||||
raise SystemExit('Cannot write to the data directory: ' + mylar.DATA_DIR + '. Exiting...')
|
||||
|
||||
# backup the db and configs before they load.
|
||||
if args.backup:
|
||||
print '[AUTO-BACKUP] Backing up .db and config.ini files for safety.'
|
||||
|
@ -195,23 +215,59 @@ def main():
|
|||
|
||||
i += 1
|
||||
|
||||
#from configobj import ConfigObj
|
||||
#mylar.CFG = ConfigObj(mylar.CONFIG_FILE, encoding='utf-8')
|
||||
|
||||
# Read config and start logging
|
||||
try:
|
||||
logger.info('Initializing startup sequence....')
|
||||
mylar.initialize(mylar.CONFIG_FILE)
|
||||
except Exception as e:
|
||||
print e
|
||||
raise SystemExit('FATAL ERROR')
|
||||
|
||||
# Rename the main thread
|
||||
threading.currentThread().name = "MAIN"
|
||||
|
||||
if mylar.DAEMON:
|
||||
mylar.daemonize()
|
||||
|
||||
if mylar.MAINTENANCE is True and any([args.exportjson, args.importjson, args.update is True, args.importstatus is True]):
|
||||
loggermode = '[MAINTENANCE-MODE]'
|
||||
if args.importstatus: #mylar.MAINTENANCE is True:
|
||||
cs = maintenance.Maintenance('status')
|
||||
cstat = cs.check_status()
|
||||
else:
|
||||
logger.info('%s Initializing maintenance mode' % loggermode)
|
||||
|
||||
if args.update is True:
|
||||
logger.info('%s Attempting to update Mylar so things can work again...' % loggermode)
|
||||
try:
|
||||
mylar.shutdown(restart=True, update=True, maintenance=True)
|
||||
except Exception as e:
|
||||
sys.exit('%s Mylar failed to update: %s' % (loggermode, e))
|
||||
|
||||
elif args.importdatabase:
|
||||
#for attempted db import.
|
||||
maintenance_path = args.importdatabase
|
||||
logger.info('%s db path accepted as %s' % (loggermode, maintenance_path))
|
||||
di = maintenance.Maintenance('database-import', file=maintenance_path)
|
||||
d = di.database_import()
|
||||
elif args.importjson:
|
||||
#for attempted file re-import (json format)
|
||||
maintenance_path = args.importjson
|
||||
logger.info('%s file indicated as being in json format - path accepted as %s' % (loggermode, maintenance_path))
|
||||
ij = maintenance.Maintenance('json-import', file=maintenance_path)
|
||||
j = ij.json_import()
|
||||
#elif args.importtext:
|
||||
# #for attempted file re-import (list format)
|
||||
# maintenance_path = args.importtext
|
||||
# logger.info('%s file indicated as being in list format - path accepted as %s' % (loggermode, maintenance_path))
|
||||
# it = maintenance.Maintenance('list-import', file=maintenance_path)
|
||||
# t = it.list_import()
|
||||
elif args.exportjson:
|
||||
#for export of db comicid's in json format
|
||||
maintenance_path = args.exportjson
|
||||
logger.info('%s file indicated as being written to json format - destination accepted as %s' % (loggermode, maintenance_path))
|
||||
ej = maintenance.Maintenance('json-export', output=maintenance_path)
|
||||
j = ej.json_export()
|
||||
else:
|
||||
logger.info('%s Not a valid command: %s' % (loggermode, maintenance_info))
|
||||
sys.exit()
|
||||
logger.info('%s Exiting Maintenance mode' % (loggermode))
|
||||
|
||||
#possible option to restart automatically after maintenance has completed...
|
||||
sys.exit()
|
||||
|
||||
# Force the http port if neccessary
|
||||
if args.port:
|
||||
http_port = args.port
|
||||
|
@ -251,7 +307,9 @@ def main():
|
|||
# Try to start the server.
|
||||
webstart.initialize(web_config)
|
||||
|
||||
#logger.info('Starting Mylar on port: %i' % http_port)
|
||||
#check for version here after web server initialized so it doesn't try to repeatidly hit github
|
||||
#for version info if it's already running
|
||||
versioncheck.versionload()
|
||||
|
||||
if mylar.CONFIG.LAUNCH_BROWSER and not args.nolaunch:
|
||||
mylar.launch_browser(mylar.CONFIG.HTTP_HOST, http_port, mylar.CONFIG.HTTP_ROOT)
|
||||
|
|
|
@ -259,6 +259,16 @@ table#searchresults_table td#name { vertical-align: middle; text-align: left; mi
|
|||
table#searchresults_table td#comicyear { vertical-align: middle; text-align: left; min-width: 50px; }
|
||||
table#searchresults_table td#issues { vertical-align: middle; text-align: center; min-width: 50px; }
|
||||
|
||||
table#downloads_table th#title { text-align: center; min-width: 100px; }
|
||||
table#downloads_table th#provider { text-align: center; min-width: 30px; }
|
||||
table#downloads_table th#size { text-align: center; min-width: 20px; }
|
||||
table#downloads_table th#kind { text-align: center; min-width: 20px; }
|
||||
|
||||
table#downloads_table td#title { vertical-align: middle; text-align: left; min-width: 100px; }
|
||||
table#downloads_table td#provider { vertical-align: middle; text-align: left; min-width: 30px; }
|
||||
table#downloads_table td#size { vertical-align: middle; text-align: left; min-width: 20px; }
|
||||
table#downloads_table td#kind { vertical-align: middle; text-align: left; min-width: 20px; }
|
||||
|
||||
.havetracks { font-size: 13px; margin-left: 36px; padding-bottom: 3px; vertical-align: middle; }
|
||||
|
||||
footer { margin: 20px auto 20px auto; }
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
mylar.DONATEBUTTON = True
|
||||
%>
|
||||
<div id="container">
|
||||
<div id="ajaxMsg"></div>
|
||||
<div id="ajaxMsg" class="ajaxMsg"></div>
|
||||
% if not mylar.CURRENT_VERSION:
|
||||
<div id="updatebar">
|
||||
You're running an unknown version of Mylar. <a href="update">Update</a> or
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('refreshSeries?ComicID=${comic['ComicID']}', $(this),'table')" href="#" data-success="${comic['ComicName']} is being refreshed">Refresh Comic</a>
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('refreshSeries?ComicID=${comic['ComicID']}', $(this), 'tabs');" href="#" data-success="${comic['ComicName']} is being refreshed">Refresh Comic</a>
|
||||
<a id="menu_link_delete" href="#">Delete Comic</a>
|
||||
<div id="dialog" title="Delete Series Confirmation" style="display:none" class="configtable">
|
||||
<form action="deleteSeries" method="GET" style="vertical-align: middle; text-align: center">
|
||||
|
@ -24,9 +24,9 @@
|
|||
%if mylar.CONFIG.RENAME_FILES:
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('manualRename?comicid=${comic['ComicID']}', $(this),'table')" data-success="Renaming files.">Rename Files</a>
|
||||
%endif
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('forceRescan?ComicID=${comic['ComicID']}', $(this),true);return true;" data-success="${comic['ComicName']} is being rescanned">Recheck Files</a>
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('forceRescan?ComicID=${comic['ComicID']}', $(this), 'table');" data-success="${comic['ComicName']} is being rescanned">Recheck Files</a>
|
||||
%if mylar.CONFIG.ENABLE_META:
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('group_metatag?ComicID=${comic['ComicID']}&dirName=${comic['ComicLocation'] |u}', $(this),'table')" data-success="(re)tagging every issue present for '${comic['ComicName']}'">Manual MetaTagging</a>
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('group_metatag?ComicID=${comic['ComicID']}&dirName=${comic['ComicLocation'] |u}', $(this),'table');refreshLoadSeries();" data-success="(re)tagging every issue present for '${comic['ComicName']}'">Manual MetaTagging</a>
|
||||
%endif
|
||||
%if comic['Status'] == 'Paused':
|
||||
<a id="menu_link_resume" href="#" onclick="doAjaxCall('resumeSeries?ComicID=${comic['ComicID']}',$(this),true)" data-success="${comic['ComicName']} resumed">Resume Comic</a>
|
||||
|
@ -81,55 +81,22 @@
|
|||
<td id="mainimg">
|
||||
<fieldset>
|
||||
<div id="artistImg">
|
||||
<img src="${comic['ComicImage']}" alt="" height="400" width="263" />
|
||||
<img src="${comicConfig['ComicImage']}" alt="" height="400" width="263" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<%
|
||||
if comicConfig['percent'] == 101:
|
||||
css = '<div class=\"progress-container warning\">'
|
||||
if comicConfig['percent'] == 100:
|
||||
css = '<div class=\"progress-container complete\">'
|
||||
if comicConfig['percent'] < 100:
|
||||
css = '<div class=\"progress-container missing\">'
|
||||
%>
|
||||
<div style="display:table;position:relative;margin:auto;top:0px;"><span title="${comicConfig['percent']}"></span>${css}<div style="width:${comicConfig['percent']}%"><span class="progressbar-front-text">${comicConfig['haveissues']}/${comicConfig['totalissues']}</span></div></div></div>
|
||||
</td>
|
||||
<td width="100%" padding="10">
|
||||
%if comic['ComicPublisher'] == 'DC Comics':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-dccomics.png" align="right" alt="DC" height="50" width="50" />
|
||||
%elif comic['ComicPublisher'] == 'Marvel':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-marvel.jpg" align="right" alt="Marvel" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Image':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-imagecomics.png" align="right" alt="Image" height="100" width="50" />
|
||||
%elif comic['ComicPublisher'] == 'Dark Horse Comics' or comic['ComicPublisher'] == 'Dark Horse':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-darkhorse.png" align="right" alt="Darkhorse" height="100" width="75" />
|
||||
%elif comic['ComicPublisher'] == 'IDW Publishing':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-idwpublish.png" align="right" alt="IDW" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Icon':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-iconcomics.png" align="right" alt="Icon" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Red5':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-red5comics.png" align="right" alt="Red5" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Vertigo':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-vertigo.jpg" align="right" alt="Vertigo" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'ShadowLine':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-shadowline.png" align="right" alt="Shadowline" height="75" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Archie Comics':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-archiecomics.jpg" align="right" alt="Archie" height="75" width="75"/>
|
||||
%elif comic['ComicPublisher'] == 'Oni Press':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-onipress.png" align="right" alt="Oni Press" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Tokyopop':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-tokyopop.jpg" align="right" alt="Tokyopop" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Midtown Comics':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-midtowncomics.jpg" align="right" alt="Midtown" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Boom! Studios':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-boom.jpg" align="right" alt="Boom!" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Skybound':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-skybound.jpg" align="right" alt="Skybound" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Vertigo':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-dynamite.jpg" align="right" alt="Dynamite" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Top Cow':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-topcow.gif" align="right" alt="Top Cow" height="75" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Dynamite Entertainment':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-dynamite.png" align="right" alt="Dynamite" height="50" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Cartoon Books':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-cartoonbooks.jpg" align="right" alt="Cartoon Books" height="75" width="90"/>
|
||||
%elif comic['ComicPublisher'] == 'Valiant':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-valiant.png" align="right" alt="Valiant" height="100" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Action Lab':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-actionlabs.png" align="right" alt="Action Lab" height="100" width="100"/>
|
||||
%elif comic['ComicPublisher'] == 'Zenescope Entertainment':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-zenescope.png" align="right" alt="Zenescope" height="100" width="160"/>
|
||||
%if comicConfig['publisher_image'] is not None:
|
||||
<img src="${comicConfig['publisher_image']}" align="right" alt="${comicConfig['publisher_image_alt']}" height="${comicConfig['publisher_imageH']}" width="${comicConfig['publisher_imageW']}" />
|
||||
%endif
|
||||
<fieldset>
|
||||
<div>
|
||||
|
@ -198,29 +165,21 @@
|
|||
<td id="mainimg">
|
||||
<fieldset>
|
||||
<div class="row" id="artistImg">
|
||||
<img src="${comic['ComicImage']}" alt="" height="400" width="263" />
|
||||
<img src="${comicConfig['ComicImage']}" alt="" height="400" width="263" />
|
||||
</div>
|
||||
</fieldset>
|
||||
<div style="display:table;position:relative;margin:auto;top:0px;"><span title="${comicConfig['percent']}"></span>${css}<div style="width:${comicConfig['percent']}%"><span class="progressbar-front-text">${comicConfig['haveissues']}/${comicConfig['totalissues']}</span></div></div></div>
|
||||
</td>
|
||||
<td width="100%" padding="10">
|
||||
%if comic['ComicPublisher'] == 'DC Comics':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-dccomics.png" align="right" alt="DC" height="50" width="50"/>
|
||||
%elif comic['ComicPublisher'] == 'Marvel':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-marvel.jpg" align="right" alt="Marvel" height="50" width="100" border="1"/>
|
||||
%elif comic['ComicPublisher'] == 'Image':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-imagecomics.png" align="right" alt="Image" height="100" width="50"/>
|
||||
%elif comic['ComicPublisher'] == 'Dark Horse Comics':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-darkhorse.png" align="right" alt="Darkhorse" height="75" width="50"/>
|
||||
%elif comic['ComicPublisher'] == 'IDW Publishing':
|
||||
<img src="interfaces/default/images/publisherlogos/logo-idwpublish.png" align="right" alt="IDW" height="50" width="100"/>
|
||||
%endif
|
||||
|
||||
<form action="comic_config" method="GET">
|
||||
%if comicConfig['publisher_image'] is not None:
|
||||
<img src="${comicConfig['publisher_image']}" align="right" alt="${comicConfig['publisher_image_alt']}" height="${comicConfig['publisher_imageH']}" width="${comicConfig['publisher_imageW']}" />
|
||||
%endif
|
||||
<form action="comic_config" method="GET">
|
||||
<input type="hidden" name="ComicID" value=${comic['ComicID']}>
|
||||
<fieldset>
|
||||
<br/><br/>
|
||||
<div class="row checkbox left clearfix">
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="force_continuing" value="2" ${comicConfig['force_continuing']} /><label>Forcibly Mark as Continuing Series<a href="#" title="Will forcibly mark this series as 'Continuing' regardless of actual status"><img src="interfaces/default/images/info32.png" height="16" alt="" /></label></a>
|
||||
<br/>
|
||||
<div class="row checkbox right clearfix">
|
||||
<input type="checkbox" style="vertical-align: bottom; margin: 3px; margin-top: -3px;" name="force_continuing" value="2" ${comicConfig['force_continuing']} /><label>Forcibly Mark as Continuing<a href="#" title="Will forcibly mark this series as 'Continuing' regardless of actual status"><img src="interfaces/default/images/info32.png" height="16" alt="" /></label></a>
|
||||
</div>
|
||||
%if any([comic['ComicYear'] == '2099',comic['ComicYear'] == '0000', comic['ComicYear'] == '', comic['Corrected_SeriesYear']]):
|
||||
<div class="row">
|
||||
|
@ -257,18 +216,17 @@
|
|||
<%
|
||||
year_options = "Default - Keep the Year as is\nYear Removal - Remove issue publication year from searches (dangerous)\nFuzzy the Year - Increase & Decrease the issue publication year by one"
|
||||
%>
|
||||
<label>Year Options<a href="#" title="${year_options}"><img src="interfaces/default/images/info32.png" height="16" alt="" /></a></label>
|
||||
<input type="radio" name="fuzzy_year" value="0" ${comicConfig['fuzzy_year0']} /> Default <input type="radio" name="fuzzy_year" value="1" ${comicConfig['fuzzy_year1']} /> Year Removal <input type="radio" name="fuzzy_year" value="2" ${comicConfig['fuzzy_year2']} /> Fuzzy the Year
|
||||
<label>Year Options<a href="#" title="${year_options}"><img src="interfaces/default/images/info32.png" valign="bottom" height="16" alt="" /></a></label>
|
||||
<input type="radio" style="vertical-align: bottom; margin: 3px; margin-top: -3px;" name="fuzzy_year" value="0" ${comicConfig['fuzzy_year0']} /> Default <input type="radio" style="vertical-align: bottom; margin: 3px; margin-top: -3px;" name="fuzzy_year" value="1" ${comicConfig['fuzzy_year1']} /> Year Removal <input type="radio" style="vertical-align: bottom; margin: 3px; margin-top: -3px;" name="fuzzy_year" value="2" ${comicConfig['fuzzy_year2']} /> Fuzzy the Year
|
||||
</div>
|
||||
|
||||
%if mylar.CONFIG.ENABLE_32P and mylar.CONFIG.MODE_32P == 1:
|
||||
<div class="row">
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="allow_packs" value="1" ${comicConfig['allow_packs']} /><label>Enable Pack Downloads<a href="#" title="Will allow downloading of multiple issues in one file (packs), but will search individual issues first"><img src="interfaces/default/images/info32.png" height="16" alt="" /></a></label>
|
||||
<div class="row checkbox right clearfix">
|
||||
<input type="checkbox" style="vertical-align: bottom; margin: 3px; margin-top: -3px;" name="allow_packs" value="1" ${comicConfig['allow_packs']} /><label>Enable Pack Downloads<a href="#" title="Will allow downloading of multiple issues in one file (packs), but will search individual issues first"><img src="interfaces/default/images/info32.png" height="16" alt="" /></a></label>
|
||||
</div>
|
||||
%endif
|
||||
|
||||
</fieldset>
|
||||
<input type="submit" value="Update"/>
|
||||
<input type="submit" style="float:right;" value="Update"/>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -355,7 +313,7 @@
|
|||
<td id="issuenumber">${issue['Issue_Number']}</td>
|
||||
<%
|
||||
if len(issue['IssueName']) > 70:
|
||||
issuename = issue['IssueName'][:70] + '...'
|
||||
issuename = "%s..." % issue['IssueName'][:70]
|
||||
else:
|
||||
issuename = issue['IssueName']
|
||||
endif
|
||||
|
@ -370,6 +328,23 @@
|
|||
</td>
|
||||
<td id="options">
|
||||
<a href="#" title="Manual Search" onclick="doAjaxCall('queueit?ComicID=${issue['ComicID']}&IssueID=${issue['IssueID']}&ComicIssue=${issue['Issue_Number']}&ComicYear=${issue['IssueDate']}&mode=want&manualsearch=True',$(this),'table')" data-success="Manually searching for ${comic['ComicName']} #${issue['Issue_Number']}"><img src="interfaces/default/images/search.png" height="25" width="25" /></a>
|
||||
<!--
|
||||
<a class="menu_link_edit" id="choose_specific_download" title="Choose Specific Download" href="javascript:void(0)" onclick="getAvailableDownloads('${issue['IssueID']}')"><i class="fa fa-search"></i><img src="interfaces/default/images/magnifier.png" height="25" width="25" class="highqual" /></a>
|
||||
<div id="choose_specific_download_dialog" title="Choose a specific download for this issue" style="display:none" class="configtable">
|
||||
<table class="display" id="downloads_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="title">Title</th>
|
||||
<th id="provider">Provider</th>
|
||||
<th id="size">Size</th>
|
||||
<th id="kind">Kind</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="downloads_table_body" value="Now searching....this might take up to 90 seconds.">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
-->
|
||||
%if issue['Status'] == 'Skipped' or issue['Status'] == 'Ignored':
|
||||
<a href="#" title="Mark issue as Wanted" onclick="doAjaxCall('queueit?ComicID=${issue['ComicID']}&IssueID=${issue['IssueID']}&ComicIssue=${issue['Issue_Number']}&ComicYear=${issue['IssueDate']}&mode=want',$(this),'table')" data-success="Issue Marked as Wanted"><img src="interfaces/default/images/wanted_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
%elif (issue['Status'] == 'Wanted'):
|
||||
|
@ -408,10 +383,11 @@
|
|||
%endif
|
||||
|
||||
%if mylar.CONFIG.ENABLE_META:
|
||||
<a href="#" title="Manually meta-tag issue" onclick="doAjaxCall('manual_metatag?dirName=${comic['ComicLocation'] |u}&issueid=${issue['IssueID']}&filename=${linky |u}&comicid=${issue['ComicID']}&comversion=${comic['ComicVersion']}&seriesyear=${comic['ComicYear']}',$(this),'table')" data-success="${comic['ComicName']} #${issue['Issue_Number']} successfully tagged."><img src="interfaces/default/images/comictagger.png" height="25" width="25" class="highqual" /></a>
|
||||
<a href="#" title="Manually meta-tag issue" onclick="doAjaxCall('manual_metatag?dirName=${comic['ComicLocation'] |u}&issueid=${issue['IssueID']}&filename=${linky |u}&comicid=${issue['ComicID']}&comversion=${comic['ComicVersion']}&seriesyear=${comic['ComicYear']}',$(this));refreshLoadSeries();" data-success="${comic['ComicName']} #${issue['Issue_Number']} successfully tagged."><img src="interfaces/default/images/comictagger.png" height="25" width="25" class="highqual" /></a>
|
||||
%endif
|
||||
%endif
|
||||
<a href="#" title="Add to Reading List" onclick="doAjaxCall('addtoreadlist?IssueID=${issue['IssueID']}',$(this),'table')" data-success="${comic['ComicName']} #${issue['Issue_Number']} added to Reading List"><img src="interfaces/default/images/glasses-icon.png" height="25" width="25" class="highqual" /></a>
|
||||
<a href="#" title="Mark issue as Skipped" onclick="doAjaxCall('unqueueissue?IssueID=${issue['IssueID']}&ComicID=${issue['ComicID']}',$(this),'table')" data-success="'${issue['Issue_Number']}' has been marked as skipped"><img src="interfaces/default/images/skipped_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
%else:
|
||||
<a href="#" title="Retry the same download again" onclick="doAjaxCall('queueit?ComicID=${issue['ComicID']}&IssueID=${issue['IssueID']}&ComicIssue=${issue['Issue_Number']}&mode=want', $(this),'table')" data-success="Retrying the same version of '${issue['ComicName']}' '${issue['Issue_Number']}'"><img src="interfaces/default/images/retry_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
<a href="#" title="Mark issue as Skipped" onclick="doAjaxCall('unqueueissue?IssueID=${issue['IssueID']}&ComicID=${issue['ComicID']}',$(this),'table',true);" data-success="'${issue['Issue_Number']}' has been marked as skipped"><img src="interfaces/default/images/skipped_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
|
@ -507,6 +483,23 @@
|
|||
<td id="aoptions">
|
||||
<% amode = 'want_ann' %>
|
||||
<a href="#" title="Manual Search" onclick="doAjaxCall('queueit?ComicID=${annual['ComicID']}&IssueID=${annual['IssueID']}&ComicIssue=${annual['Issue_Number']}&ComicYear=${annual['IssueDate']}&mode=${amode}&manualsearch=True',$(this),'table')" data-success="Manually searching for ${annual['ComicName']} #${annual['Issue_Number']}"><img src="interfaces/default/images/search.png" height="25" width="25" class="highqual" /></a>
|
||||
<!--
|
||||
<a class="menu_link_edit" id="choose_specific_download" title="Choose Specific Download" href="javascript:void(0)" onclick="getAvailableDownloads('${annual['IssueID']}')"><i class="fa fa-search"></i><img src="interfaces/default/images/magnifier.png" height="25" width="25" class="highqual" /></a>
|
||||
<div id="choose_specific_download_dialog" title="Choose a specific download for this issue" style="display:none" class="configtable">
|
||||
<table class="display" id="downloads_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="title">Title</th>
|
||||
<th id="provider">Provider</th>
|
||||
<th id="size">Size</th>
|
||||
<th id="kind">Kind</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="downloads_table_body" value="Now searching....this might take up to 90 seconds.">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
-->
|
||||
%if annual['Status'] == 'Skipped' or annual['Status'] == 'Ignored':
|
||||
<a href="#" title="Mark annual as Wanted" onclick="doAjaxCall('queueit?ComicID=${annual['ComicID']}&IssueID=${annual['IssueID']}&ComicIssue=${annual['Issue_Number']}&ComicYear=${annual['IssueDate']}&mode=${amode}',$(this),'table')" data-success="Marked ${annual['ReleaseComicName']} #${annual['Issue_Number']} as Wanted"><img src="interfaces/default/images/wanted_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
%elif (annual['Status'] == 'Wanted'):
|
||||
|
@ -519,15 +512,18 @@
|
|||
%endif
|
||||
%elif (annual['Status'] == 'Downloaded'):
|
||||
<%
|
||||
if annual['Location'] is not None:
|
||||
linky = os.path.join(comic['ComicLocation'],annual['Location'])
|
||||
if not os.path.isfile(linky):
|
||||
if mylar.CONFIG.MULTIPLE_DEST_DIRS is not None and mylar.CONFIG.MULTIPLE_DEST_DIRS != 'None':
|
||||
pathdir = os.path.join(mylar.CONFIG.MULTIPLE_DEST_DIRS, os.path.basename(comic['ComicLocation']))
|
||||
linky = os.path.join(pathdir,issue['Location'])
|
||||
else:
|
||||
linky = None
|
||||
else:
|
||||
try:
|
||||
if annual['Location'] is not None:
|
||||
linky = os.path.join(comic['ComicLocation'],annual['Location'])
|
||||
if not os.path.isfile(linky):
|
||||
if mylar.CONFIG.MULTIPLE_DEST_DIRS is not None and mylar.CONFIG.MULTIPLE_DEST_DIRS != 'None':
|
||||
pathdir = os.path.join(mylar.CONFIG.MULTIPLE_DEST_DIRS, os.path.basename(comic['ComicLocation']))
|
||||
linky = os.path.join(pathdir,issue['Location'])
|
||||
else:
|
||||
linky = None
|
||||
else:
|
||||
linky = None
|
||||
except:
|
||||
linky = None
|
||||
%>
|
||||
%if linky:
|
||||
|
@ -554,12 +550,11 @@
|
|||
-->
|
||||
<a href="#" title="Add to Reading List" onclick="doAjaxCall('addtoreadlist?IssueID=${annual['IssueID']}',$(this),'table')" data-success="${aninfo['annualComicName']} #${annual['Issue_Number']} added to Reading List"><img src="interfaces/default/images/glasses-icon.png" height="25" width="25" class="highqual" /></a>
|
||||
|
||||
<a href="#" onclick="doAjaxCall('retryit?ComicName=${annual['ComicName'] |u}&ComicID=${annual['ComicID']}&IssueID=${annual['IssueID']}&IssueNumber=${annual['Issue_Number']}&ComicYear=${annual['IssueDate']}&ReleaseComicID=${annual['ReleaseComicID']}', $(this),'table')" data-success="Retrying the same version of '${annual['ComicName']}' '${annual['Issue_Number']}'" title="Retry the same download again"><img src="interfaces/default/images/retry_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
<a href="#" title="Mark annual as Skipped" onclick="doAjaxCall('unqueueissue?IssueID=${annual['IssueID']}&ComicID=${annual['ComicID']}',$(this),'table')" data-success="'${annual['Issue_Number']}' has been marked as skipped"><img src="interfaces/default/images/skipped_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
<a href="#" title="Mark annual as Skipped" onclick="doAjaxCall('unqueueissue?IssueID=${annual['IssueID']}&ComicID=${annual['ComicID']}&ReleaseComicID=${annual['ReleaseComicID']}',$(this),'table')" data-success="'${annual['Issue_Number']}' has been marked as skipped"><img src="interfaces/default/images/skipped_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
|
||||
%else:
|
||||
<a href="#" title="Retry the same download again" onclick="doAjaxCall('queueit?ComicID=${annual['ComicID']}&IssueID=${annual['IssueID']}&ComicIssue=${annual['Issue_Number']}&mode=want', $(this),'table')" data-success="Retrying the same version of '${annual['ComicName']}' '${annual['Issue_Number']}'"><img src="interfaces/default/images/retry_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
<a href="#" title="Mark annual as Skipped" onclick="doAjaxCall('unqueueissue?IssueID=${annual['IssueID']}&ComicID=${annual['ComicID']}',$(this),'table')" data-success="'${annual['Issue_Number']}' has been marked as skipped"><img src="interfaces/default/images/skipped_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
<a href="#" title="Mark annual as Skipped" onclick="doAjaxCall('unqueueissue?IssueID=${annual['IssueID']}&ComicID=${annual['ComicID']}&ReleaseComicID=${annual['ReleaseComicID']}',$(this),'table')" data-success="'${annual['Issue_Number']}' has been marked as skipped"><img src="interfaces/default/images/skipped_icon.png" height="25" width="25" class="highqual" /></a>
|
||||
%endif
|
||||
</td>
|
||||
|
||||
|
@ -756,6 +751,69 @@
|
|||
});
|
||||
}
|
||||
|
||||
function getAvailableDownloads(issueid) {
|
||||
ShowSpinner();
|
||||
$.getJSON("choose_specific_download", {issueid: issueid}, function(data) {
|
||||
loader.remove();
|
||||
feedback.fadeOut();
|
||||
search_results = data
|
||||
for( var i = 0, len = data.length; i < len; i++ ) {
|
||||
$('#downloads_table_body').append('<tr><td id="title"><a href="javascript:void(0)" onclick="downloadSpecificRelease('+i+')">'+data[i].nzbtitle+'</a></td><td id="provider">'+data[i].provider+'</td><td id="size">'+data[i].size+'</td><td id="kind">'+data[i].kind+'</td></tr>');
|
||||
}
|
||||
$('#downloads_table').dataTable({
|
||||
"aoColumns": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"aaSorting": [[ 1, 'desc']],
|
||||
"bFilter": false,
|
||||
"bInfo": false,
|
||||
"bPaginate": false,
|
||||
"bDestroy": true
|
||||
});
|
||||
$("#choose_specific_download_dialog").dialog({
|
||||
width: "60%",
|
||||
maxHeight: 500
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function downloadSpecificRelease(i){
|
||||
|
||||
name = search_results[i].nzbtitle
|
||||
prov = search_results[i].tmpprov
|
||||
nzbid = search_results[i].nzbid
|
||||
ShowSpinner();
|
||||
$.getJSON("download_specific_release", {nzbid: nzbid, provider: prov, name: name}, function(data) {
|
||||
loader.remove();
|
||||
feedback.fadeOut();
|
||||
refreshSubmenu();
|
||||
$("#choose_specific_download_dialog").dialog("close");
|
||||
});
|
||||
}
|
||||
|
||||
function ShowSpinner() {
|
||||
feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
if ( update.is(":visible") ) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom",height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
}
|
||||
loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
||||
feedback.prepend(loader);
|
||||
feedback.fadeIn();
|
||||
}
|
||||
|
||||
var loadingMessage = false;
|
||||
var spinner_active = false;
|
||||
var loadingtext_active = false;
|
||||
var refreshInterval;
|
||||
|
||||
function initThisPage(){
|
||||
|
||||
$(function() {
|
||||
|
|
|
@ -40,7 +40,8 @@
|
|||
<legend>Configuration Options</legend>
|
||||
<div>
|
||||
<label><strong>Mylar Version: </strong> ${config['branch']}</br> -- ${config['br_type']} build ${config['br_version']}.</label></br>
|
||||
<label><strong>Python Version :</strong> ${config['py_version']}</label></br></br>
|
||||
<label><strong>Python Version :</strong> ${config['py_version']}</label></br>
|
||||
<label><strong>Language :</strong> ${config['lang']}</label></br></br>
|
||||
<legend>MYLAR PROGRAM OPTIONS</legend>
|
||||
<label><strong>Mylar Data Directory :</strong> ${config['data_dir']}</label></br>
|
||||
<label><strong>Mylar Program Directory :</strong> ${config['prog_dir']}</label></br>
|
||||
|
@ -385,11 +386,17 @@
|
|||
</div>
|
||||
|
||||
<div class="row checkbox left clearfix">
|
||||
<input type="checkbox" id="sab_client_post_processing" onclick="initConfigCheckbox($this));" name="sab_client_post_processing" value="1" ${config['sab_client_post_processing']} /><label>Enable Completed Download Handling<label>
|
||||
<small>The category label above is used to when completed download handling is enabled</small>
|
||||
<input type="checkbox" id="sab_client_post_processing" onclick="initConfigCheckbox($this));" name="sab_client_post_processing" value="1" ${config['sab_client_post_processing']} /><label>Enable Completed Download Handling</label>
|
||||
<div id="completedinfo">
|
||||
<div class="row">
|
||||
<small class="heading"><span style="float: left; margin-right: .3em; margin-top: 4px;" class="ui-icon ui-icon-info"></span>
|
||||
ComicRN script cannot be used with the same category / Category above has to be set within SABnzbd</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div align="center" class="row">
|
||||
<img name="sabnzbd_statusicon" id="sabnzbd_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test SABnzbd" id="test_sab" style="float:center" /></br>
|
||||
<input type="text" name="sabstatus" style="text-align:center; font-size:11px;" id="sabstatus" size="50" DISABLED />
|
||||
</div>
|
||||
|
@ -398,20 +405,20 @@
|
|||
<fieldset id="nzbget_options">
|
||||
<div class="row">
|
||||
<label>NZBGet Host:</label>
|
||||
<input type="text" name="nzbget_host" value="${config['nzbget_host']}" size="30">
|
||||
<input type="text" id="nzbget_host" name="nzbget_host" value="${config['nzbget_host']}" size="30">
|
||||
<small>usually http://localhost</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>NZBGet Port:</label>
|
||||
<input type="text" name="nzbget_port" value="${config['nzbget_port']}" size="36">
|
||||
<input type="text" id="nzbget_port" name="nzbget_port" value="${config['nzbget_port']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>NZBGet Username:</label>
|
||||
<input type="text" name="nzbget_username" value="${config['nzbget_user']}" size="20">
|
||||
<input type="text" id="nzbget_username" name="nzbget_username" value="${config['nzbget_user']}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>NZBGet Password:</label>
|
||||
<input type="password" name="nzbget_password" value="${config['nzbget_pass']| h}" size="20">
|
||||
<input type="password" id="nzbget_password" name="nzbget_password" value="${config['nzbget_pass']| h}" size="20">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>NZBGet Download Directory</label>
|
||||
|
@ -441,6 +448,12 @@
|
|||
<input type="checkbox" id="nzbget_client_post_processing" onclick="initConfigCheckbox($this));" name="nzbget_client_post_processing" value="1" ${config['nzbget_client_post_processing']} /><label>Enable Completed Download Handling<label>
|
||||
<small>The category label above is used to when completed download handling is enabled</small>
|
||||
</div>
|
||||
|
||||
<div align="center" class="row">
|
||||
<img name="nzbget_statusicon" id="nzbget_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test NZBGet" id="test_nzbget" style="float:center" /></br>
|
||||
<input type="text" name="nzbgetstatus" style="text-align:center; font-size:11px;" id="nzbgetstatus" size="50" DISABLED />
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
<fieldset id="blackhole_options">
|
||||
|
@ -590,6 +603,7 @@
|
|||
<small>Automatically start torrent on successful loading within rtorrent client</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<img name="rtorrent_statusicon" id="rtorrent_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test Connection" id="rtorrent_test" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
@ -687,7 +701,7 @@
|
|||
</div>
|
||||
<div class="config">
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="nzbsu_verify" value="1" ${config['nzbsu_verify']} /><label>Verify SSL</label>
|
||||
<input type="checkbox" name="nzbsu_verify" id="nzbsu_verify" value="1" ${config['nzbsu_verify']} /><label>Verify SSL</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>NZB.SU UID</label>
|
||||
|
@ -696,8 +710,12 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<label>NZB.SU API</label>
|
||||
<input type="text" name="nzbsu_apikey" value="${config['nzbsu_api']}" size="36">
|
||||
<input type="text" name="nzbsu_apikey" id="nzbsu_apikey" value="${config['nzbsu_api']}" size="36">
|
||||
</div>
|
||||
<div>
|
||||
<img name="nzbsu_statusicon" id="nzbsu_statusicon" src="interfaces/default/images/success.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" class="newznabtest" value="Test Connection" id="test_nzbsu" name="test_nzbsu" style="float:right;margin-right:10px;" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
@ -707,12 +725,16 @@
|
|||
</div>
|
||||
<div class="config">
|
||||
<div class="row checkbox">
|
||||
<input id="dognzb_verify" type="checkbox" name="dognzb_verify" value="1" ${config['dognzb_verify']} /><label>Verify SSL</label>
|
||||
<input id="dognzb_verify" type="checkbox" name="dognzb_verify" id="dognzb_verify" value="1" ${config['dognzb_verify']} /><label>Verify SSL</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>DOGNZB API</label>
|
||||
<input type="text" name="dognzb_apikey" value="${config['dognzb_api']}" size="36">
|
||||
</div>
|
||||
<input type="text" name="dognzb_apikey" id="dognzb_apikey" value="${config['dognzb_api']}" size="36">
|
||||
</div>
|
||||
<div>
|
||||
<img name="dognzb_statusicon" id="dognzb_statusicon" src="interfaces/default/images/success.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" class="newznabtest" value="Test Connection" id="test_dognzb" name="test_dognzb" style="float:right;margin-right:10px;" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
@ -732,9 +754,9 @@
|
|||
</div>
|
||||
<div class="config">
|
||||
<div class="row checkbox left clearfix">
|
||||
<input id="enable_tpse" title="Will Use TPSE for searches, Demonoid & WWT for RSS feeds" type="checkbox" name="enable_tpse" value=1 ${config['enable_tpse']} /><label>Enable Public Torrent Search</label>
|
||||
<input id="enable_public" title="Use Public Torrents" type="checkbox" name="enable_public" value=1 ${config['enable_public']} /><label>Enable Public Torrent Search</label>
|
||||
<div align="left">
|
||||
<small class="heading"><span style="float: left; margin-left: .3em; margin-top: 4px;" class="ui-icon ui-icon-info"></span>Search: TPSE / RSS: DEM & WWT</small>
|
||||
<small class="heading"><span style="float: left; margin-left: .3em; margin-top: 4px;" class="ui-icon ui-icon-info"></span>Search: WWT / RSS: DEM & WWT</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row checkbox left clearfix">
|
||||
|
@ -775,6 +797,7 @@
|
|||
<small>( monitor the NEW releases feed & your personal notifications )</small>
|
||||
</div>
|
||||
<div align="center" class="row">
|
||||
<img name="test32p_statusicon" id="test32p_statusicon" src="interfaces/default/images/success.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test Connection" id="test_32p" style="float:center" /></br>
|
||||
<input type="text" name="status32p" style="text-align:center; font-size:11px;" id="status32p" size="50" DISABLED />
|
||||
</div>
|
||||
|
@ -792,24 +815,51 @@
|
|||
<div class="row checkbox left clearfix">
|
||||
<input type="checkbox" id="enable_torznab" onclick="initConfigCheckbox($(this));" name="enable_torznab" value=1 ${config['enable_torznab']} /><label>Enable Torznab</label>
|
||||
</div>
|
||||
<div class="config">
|
||||
<div class="row">
|
||||
<label>Torznab Name</label>
|
||||
<input type="text" name="torznab_name" value="${config['torznab_name']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab Host</label>
|
||||
<input type="text" name="torznab_host" value="${config['torznab_host']}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab API</label>
|
||||
<input type="text" name="torznab_apikey" value="${config['torznab_apikey']}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab Category</label>
|
||||
<input type="text" name="torznab_category" value="${config['torznab_category']}" size="12">
|
||||
</div>
|
||||
</div>
|
||||
<div id="torznab_providers">
|
||||
<%
|
||||
torznab_number = 1
|
||||
%>
|
||||
%for torznab in config['extra_torznabs']:
|
||||
<%
|
||||
if torznab[4] == '1' or torznab[4] == 1:
|
||||
torznab_enabled = "checked"
|
||||
else:
|
||||
torznab_enabled = ""
|
||||
|
||||
%>
|
||||
|
||||
<div class="config" id="torznab${torznab_number}">
|
||||
<div class="row">
|
||||
<label>Torznab Name</label>
|
||||
<input type="text" name="torznab_name${torznab_number}" id="torznab_name${torznab_number}" value="${torznab[0]}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab Host</label>
|
||||
<input type="text" name="torznab_host${torznab_number}" id="torznab_host${torznab_number}" value="${torznab[1]}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab API</label>
|
||||
<input type="text" name="torznab_apikey${torznab_number}" id="torznab_apikey${torznab_number}" value="${torznab[2]}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Torznab Category</label>
|
||||
<input type="text" name="torznab_category${torznab_number}" id="torznab_category${torznab_number}" value="${torznab[3]}" size="12">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input id="torznab_enabled${torznab_number}" type="checkbox" name="torznab_enabled${torznab_number}" value="1" ${torznab_enabled} /><label>Enabled</label>
|
||||
<img name="torznabstatus${torznab_number}" id="torznabstatus${torznab_number}" src="interfaces/default/images/success.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" class="torznabtest" value="Test ${torznab[0]}" id="torznab_test${torznab_number}" name="torznab_test${torznab_number}" style="float:right;margin-right:10px;" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="button" class="remove" id="torznab${torznab_number}" value="Remove ${torznab[0]}">
|
||||
</div>
|
||||
</div>
|
||||
<%
|
||||
torznab_number += 1
|
||||
%>
|
||||
%endfor
|
||||
<input type="button" value="Add Torznab" class="add_torznab" id="add_torznab" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
@ -840,18 +890,18 @@
|
|||
<div class="config" id="newznab${newznab_number}">
|
||||
<div class="row">
|
||||
<label>Newznab Name</label>
|
||||
<input type="text" name="newznab_name${newznab_number}" value="${newznab[0]}" size="30">
|
||||
<input type="text" name="newznab_name${newznab_number}" id="newznab_name${newznab_number}" value="${newznab[0]}" size="30">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Newznab Host</label>
|
||||
<input type="text" name="newznab_host${newznab_number}" value="${newznab[1]}" size="30">
|
||||
<input type="text" name="newznab_host${newznab_number}" id="newznab_host${newznab_number}" value="${newznab[1]}" size="30">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input id="newznab_verify" type="checkbox" name="newznab_verify${newznab_number}" value="1" ${newznab_verify} /><label>Verify SSL</label>
|
||||
<input id="newznab_verify${newznab_number}" type="checkbox" name="newznab_verify${newznab_number}" value="1" ${newznab_verify} /><label>Verify SSL</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Newznab API</label>
|
||||
<input type="text" name="newznab_api${newznab_number}" value="${newznab[3]}" size="36">
|
||||
<input type="text" name="newznab_api${newznab_number}" id="newznab_api${newznab_number}" value="${newznab[3]}" size="36">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Newznab UID</label>
|
||||
|
@ -859,11 +909,9 @@
|
|||
<small>( only needed for RSS feed )</small>
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input id="newznab_enabled" type="checkbox" name="newznab_enabled${newznab_number}" value="1" ${newznab_enabled} /><label>Enabled</label>
|
||||
<!--
|
||||
<input type="button" value="Test ${newznab[0]}" id="newznab_test(newznab${newznab_number})" style="float:center" /></br>
|
||||
<img name="newznabstatus${newznab_number}" id="newznabstatus${newznab_number}" src="interfaces/default/images/newznab_success.png" style="vertical-align: middle; margin: 3px; margin-top: -1px;" height="10" width="10" DISABLED />
|
||||
-->
|
||||
<input id="newznab_enabled${newznab_number}" type="checkbox" name="newznab_enabled${newznab_number}" value="1" ${newznab_enabled} /><label>Enabled</label>
|
||||
<img name="newznabstatus${newznab_number}" id="newznabstatus${newznab_number}" src="interfaces/default/images/success.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" class="newznabtest" value="Test ${newznab[0]}" id="newznab_test${newznab_number}" name="newznab_test${newznab_number}" style="float:right;margin-right:10px;" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="button" class="remove" id="newznab${newznab_number}" value="Remove ${newznab[0]}">
|
||||
|
@ -879,6 +927,7 @@
|
|||
|
||||
</td>
|
||||
|
||||
|
||||
</tr>
|
||||
<tr>
|
||||
<%
|
||||
|
@ -899,9 +948,9 @@
|
|||
<fieldset>
|
||||
<legend>Quality</legend>
|
||||
<div class="row radio left clearfix">
|
||||
<input type="radio" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="preferred_quality" value="1" ${config['pref_qual_1']} /><label>cbr</label>
|
||||
<input type="radio" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="preferred_quality" value="2" ${config['pref_qual_2']} /><label>cbz</label>
|
||||
<input type="radio" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="preferred_quality" value="0" ${config['pref_qual_0']} /><label>Whichever - just get it</label>
|
||||
<input type="radio" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="preferred_quality" value="1" ${config['pref_qual_1']} /><label>cbr</label>
|
||||
<input type="radio" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="preferred_quality" value="2" ${config['pref_qual_2']} /><label>cbz</label>
|
||||
<input type="radio" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="preferred_quality" value="0" ${config['pref_qual_0']} /><label>Whichever - just get it</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
@ -1046,7 +1095,7 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Metadata Tagging</legend><small class="heading"><span style="float: left; margin-right: .3em; margin-top: 4px;" class="ui-icon ui-icon-info"></span>ComicTagger is included but configparser is required</small>
|
||||
<legend>Metadata Tagging</legend><small class="heading"><span style="float: left; margin-right: .3em; margin-top: 4px;" class="ui-icon ui-icon-info"></span>ComicTagger is included</small>
|
||||
<div class="row checkbox left clearfix">
|
||||
<input id="enable_meta" type="checkbox" onclick="initConfigCheckbox($this));" name="enable_meta" value="1" ${config['enable_meta']} /><label>Enable Metadata Tagging</label>
|
||||
</div>
|
||||
|
@ -1163,6 +1212,7 @@
|
|||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="autowant_all" value="1" ${config['autowant_all']} /><label>Automatically Mark All Issues as Wanted</label>
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="comic_cover_local" value="1" ${config['comic_cover_local']} /><label>Place cover.jpg into Comic Directory for each comic</label>
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="cvinfo" value="1" ${config['cvinfo']} /><label>Write cvinfo into each comic directory</label>
|
||||
<input type="checkbox" style="vertical-align: bottom; margin: 3px; margin-top: -1px;" name="alternate_latest_series_covers" value="1" ${config['alternate_latest_series_covers']} /><label>Alternate Series Covers</label>
|
||||
%if mylar.CONFIG.ENABLE_TORRENTS:
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="snatchedtorrent_notify" value="1" ${config['snatchedtorrent_notify']} /><label>Manual Post-Processing - Notify on Completed Torrents</label>
|
||||
%endif
|
||||
|
@ -1204,7 +1254,7 @@
|
|||
</div>
|
||||
<div id="prowloptions">
|
||||
<div class="row">
|
||||
<label>API key</label><input type="text" name="prowl_keys" value="${config['prowl_keys']}" size="50">
|
||||
<label>API key</label><input type="text" id="prowl_keys" name="prowl_keys" value="${config['prowl_keys']}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="prowl_onsnatch" value="1" ${config['prowl_onsnatch']} /><label>Notify on snatch?</label>
|
||||
|
@ -1214,6 +1264,7 @@
|
|||
<input type="text" name="prowl_priority" value="${config['prowl_priority']}" size="2">
|
||||
</div>
|
||||
<div class="row">
|
||||
<img name="prowl_statusicon" id="prowl_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test PROWL" id="prowl_test" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1258,6 +1309,7 @@
|
|||
</select>
|
||||
</div>
|
||||
<div align="center" class="row">
|
||||
<img name="nma_statusicon" id="nma_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test NMA" id="nma_test" style="float:center" /></br>
|
||||
<input type="text" name="nmastatus" style="text-align:center; font-size:11px;" id="nmastatus" size="55" DISABLED />
|
||||
</div>
|
||||
|
@ -1276,6 +1328,9 @@
|
|||
<div class="row">
|
||||
<label>User key</label><input type="text" name="pushover_userkey" id="pushover_userkey" value="${config['pushover_userkey']}" size="50">
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Device</label><input type="text" title="Specific Device ID to push notifications to" name="pushover_device" id="pushover_device" value="${config['pushover_device']}" size="50">
|
||||
</div>
|
||||
<div class="row checkbox">
|
||||
<input type="checkbox" name="pushover_onsnatch" value="1" ${config['pushover_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
|
@ -1284,6 +1339,7 @@
|
|||
<input type="text" name="pushover_priority" value="${config['pushover_priority']}" size="2">
|
||||
</div>
|
||||
<div align="center" class="row">
|
||||
<img name="pushover_statusicon" id="pushover_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test Pushover" id="pushover_test" style="float:center" /></br>
|
||||
<input type="text" name="pushoverstatus" style="text-align:center; font-size:11px;" id="pushoverstatus" size="55" DISABLED />
|
||||
</div>
|
||||
|
@ -1303,6 +1359,7 @@
|
|||
<label>Boxcar Token</label>
|
||||
<input type="text" name="boxcar_token" value="${config['boxcar_token']}" size="30">
|
||||
<div class="row">
|
||||
<img name="boxcar_statusicon" id="boxcar_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test Boxcar" id="boxcar_test" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1334,6 +1391,7 @@
|
|||
<small>Send to all subscribers of the channel with this tag (Optional)</small>
|
||||
</div>
|
||||
<div align="center" class="row">
|
||||
<img name="pushbullet_statusicon" id="pushbullet_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test Pushbullet" id="pushbullet_test" style="float:center" /></br>
|
||||
<input type="text" name="pbstatus" style="text-align:center; font-size:11px;" id="pbstatus" size="55" DISABLED />
|
||||
</div>
|
||||
|
@ -1356,6 +1414,7 @@
|
|||
<input type="checkbox" name="telegram_onsnatch" value="1" ${config['telegram_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
<div align="center" class="row">
|
||||
<img name="telegram_statusicon" id="telegram_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test Telegram" id="telegram_test" style="float:center" /></br>
|
||||
<input type="text" name="telegramstatus" style="text-align:center; font-size:11px;" id="telegramstatus" size="55" DISABLED />
|
||||
</div>
|
||||
|
@ -1375,6 +1434,7 @@
|
|||
<input type="checkbox" name="slack_onsnatch" value="1" ${config['slack_onsnatch']} /><label>Notify on snatch?</label>
|
||||
</div>
|
||||
<div align="center" class="row">
|
||||
<img name="slack_statusicon" id="slack_statusicon" src="interfaces/default/images/successs.png" style="float:right;visibility:hidden;" height="20" width="20" />
|
||||
<input type="button" value="Test Slack" id="slack_test" style="float:center" /></br>
|
||||
<input type="text" name="slackstatus" style="text-align:center; font-size:11px;" id="slackstatus" size="55" DISABLED />
|
||||
</div>
|
||||
|
@ -1447,6 +1507,25 @@
|
|||
$("#apioptions").slideUp();
|
||||
}
|
||||
});
|
||||
if ($("#sab_client_post_processing").is(":checked"))
|
||||
{
|
||||
$("#completedinfo").show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#completedinfo").hide();
|
||||
}
|
||||
|
||||
$("#sab_client_post_processing").click(function(){
|
||||
if ($("#sab_client_post_processing").is(":checked"))
|
||||
{
|
||||
$("#completedinfo").slideDown();
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#completedinfo").slideUp();
|
||||
}
|
||||
});
|
||||
if ($("#opds_enable").is(":checked"))
|
||||
{
|
||||
$("#opdsoptions").show();
|
||||
|
@ -1796,6 +1875,7 @@
|
|||
});
|
||||
|
||||
var deletedNewznabs = 0;
|
||||
var deletedTorznabs = 0;
|
||||
|
||||
$(".remove").click(function() {
|
||||
$(this).parent().parent().remove();
|
||||
|
@ -1819,6 +1899,7 @@
|
|||
};
|
||||
|
||||
$("#test_32p").click(function(){
|
||||
var imagechk = document.getElementById("test32p_statusicon");
|
||||
$.get('test_32p',
|
||||
function(data){
|
||||
if (data.error != undefined) {
|
||||
|
@ -1830,14 +1911,26 @@
|
|||
inkd = numberWithCommas(inkd);
|
||||
$('#status32p').val(obj['status']);
|
||||
$('#inkdrops32p span').text('Inkdrops Available: '+inkd);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#test_sab').click(function () {
|
||||
var sabhost = document.getElementById('sab_host').value;
|
||||
var sabuser = document.getElementById('sab_username').value;
|
||||
var sabpass = document.getElementById('sab_password').value;
|
||||
var sabapi = document.getElementById('sab_apikey').value;
|
||||
var imagechk = document.getElementById("sabnzbd_statusicon");
|
||||
var sabhost = document.getElementById("sab_host").value;
|
||||
var sabuser = document.getElementById("sab_username").value;
|
||||
var sabpass = document.getElementById("sab_password").value;
|
||||
var sabapi = document.getElementById("sab_apikey").value;
|
||||
$.get("SABtest",
|
||||
{ sabhost: sabhost, sabusername: sabuser, sabpassword: sabpass, sabapikey: sabapi },
|
||||
function(data){
|
||||
|
@ -1846,8 +1939,44 @@
|
|||
return;
|
||||
}
|
||||
$('#sabstatus').val(data);
|
||||
// $('#sab_apikey').val(data);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#test_nzbget').click(function () {
|
||||
var imagechk = document.getElementById("nzbget_statusicon");
|
||||
var nzbhost = document.getElementById("nzbget_host").value;
|
||||
var nzbport = document.getElementById("nzbget_port").value;
|
||||
var nzbuser = document.getElementById("nzbget_username").value;
|
||||
var nzbpass = document.getElementById("nzbget_password").value;
|
||||
$.get("NZBGet_test",
|
||||
{ nzbhost: nzbhost, nzbport: nzbport, nzbusername: nzbuser, nzbpassword: nzbpass },
|
||||
function(data){
|
||||
if (data.error != undefined) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
$('#nzbgetstatus').val(data);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
@ -1905,16 +2034,32 @@
|
|||
|
||||
$("#add_newznab").click(function() {
|
||||
var intId = $("#newznab_providers > div").size() + deletedNewznabs + 1;
|
||||
var formfields = $("<div class=\"config\" id=\"newznab" + intId + "\"><div class=\"row\"><label>Newznab Name</label><input type=\"text\" name=\"newznab_name" + intId + "\" size=\"36\"></div><div class=\"row\"><label>Newznab Host</label><input type=\"text\" name=\"newznab_host" + intId + "\" + value=\"http://\" + size=\"30\"></div><div class=\"row checkbox\"><input type=\"checkbox\" name=\"newznab_verify" + intId + "\" value=\"0\" checked /><label>Verify SSL</label></div><div class=\"row\"><label>Newznab API</label><input type=\"text\" name=\"newznab_api" + intId + "\" size=\"36\"></div><div class=\"row\"><label>Newznab UID</label><input type=\"text\" name=\"newznab_uid" + intId + "\" size=\"15\"></div><div class=\"row checkbox\"><input type=\"checkbox\" name=\"newznab_enabled" + intId + "\" value=\"1\" checked /><label>Enabled</label></div>");
|
||||
var removeButton = $("<div class=\"row\"><input type=\"button\" class=\"remove\" value=\"Remove\" /></div>");
|
||||
removeButton.click(function() {
|
||||
var newzformfields = $("<div class=\"config\" id=\"newznab" + intId + "\"><div class=\"row\"><label>Newznab Name</label><input type=\"text\" id=\"newznab_name" + intId + "\" name=\"newznab_name" + intId + "\" size=\"36\"></div><div class=\"row\"><label>Newznab Host</label><input type=\"text\" id=\"newznab_host" + intId + "\" name=\"newznab_host" + intId + "\" + value=\"http://\" + size=\"30\"></div><div class=\"row checkbox\"><input type=\"checkbox\" id=\"newznab_verify" + intId + "\" name=\"newznab_verify" + intId + "\" value=\"0\" checked /><label>Verify SSL</label></div><div class=\"row\"><label>Newznab API</label><input type=\"text\" id=\"newznab_api" + intId + "\" name=\"newznab_api" + intId + "\" size=\"36\"></div><div class=\"row\"><label>Newznab UID</label><input type=\"text\" id=\"newznab_uid" + intId + "\" name=\"newznab_uid" + intId + "\" size=\"15\"></div><div class=\"row checkbox\"><input type=\"checkbox\" name=\"newznab_enabled" + intId + "\" value=\"1\" checked /><label>Enabled</label></div>");
|
||||
var newztestButton = $("<div class=\"row\"><img name=\"newznabstatus" + intId + "\" id=\"newznabstatus" + intId + "\" src=\"interfaces/default/images/success.png\" style=\"float:right;visibility:hidden;\" height=\"20\" width=\"20\" /><input type=\"button\" class=\"newznabtest\" value=\"Test\" id=\"newznab_test" + intId + "\" name=\"newznab_test" + intId + "\" style=\"float:right;margin-right:10px;\" /></div>");
|
||||
var newzremoveButton = $("<div class=\"row\"><input type=\"button\" class=\"remove\" value=\"Remove\" /></div>");
|
||||
newzremoveButton.click(function() {
|
||||
$(this).parent().remove();
|
||||
deletedNewznabs = deletedNewznabs + 1;
|
||||
|
||||
});
|
||||
formfields.append(removeButton);
|
||||
formfields.append("</div>");
|
||||
$("#add_newznab").before(formfields);
|
||||
newzformfields.append(newztestButton);
|
||||
newzformfields.append(newzremoveButton);
|
||||
newzformfields.append("</div>");
|
||||
$("#add_newznab").before(newzformfields);
|
||||
});
|
||||
|
||||
$("#add_torznab").click(function() {
|
||||
var intId = $("#torznab_providers > div").size() + deletedTorznabs + 1;
|
||||
var torformfields = $("<div class=\"config\" id=\"torznab" + intId + "\"><div class=\"row\"><label>Torznab Name</label><input type=\"text\" id=\"torznab_name" + intId + "\" name=\"torznab_name" + intId + "\" size=\"36\"></div><div class=\"row\"><label>Torznab Host</label><input type=\"text\" id=\"torznab_host" + intId + "\" name=\"torznab_host" + intId + "\" + value=\"http://\" + size=\"30\"></div><div class=\"row\"><label>Torznab API</label><input type=\"text\" id=\"torznab_apikey" + intId + "\" name=\"torznab_apikey" + intId + "\" size=\"36\"></div><div class=\"row\"><label>Torznab Category</label><input type=\"text\" id=\"torznab_category" + intId + "\" name=\"torznab_category" + intId + "\" size=\"36\"></div><div class=\"row checkbox\"><input type=\"checkbox\" name=\"torznab_enabled" + intId + "\" value=\"1\" checked /><label>Enabled</label></div>");
|
||||
var tortestButton = $("<div class=\"row\"><img name=\"torznabstatus" + intId + "\" id=\"torznabstatus" + intId + "\" src=\"interfaces/default/images/success.png\" style=\"float:right;visibility:hidden;\" height=\"20\" width=\"20\" /><input type=\"button\" class=\"torznabtest\" value=\"Test\" id=\"torznab_test" + intId + "\" name=\"torznab_test" + intId + "\" style=\"float:right;margin-right:10px;\" /></div>");
|
||||
var torremoveButton = $("<div class=\"row\"><input type=\"button\" class=\"remove\" value=\"Remove\" /></div>");
|
||||
torremoveButton.click(function() {
|
||||
$(this).parent().remove();
|
||||
deletedTorznabs = deletedTorznabs + 1;
|
||||
});
|
||||
torformfields.append(tortestButton);
|
||||
torformfields.append(torremoveButton);
|
||||
torformfields.append("</div>");
|
||||
$("#add_torznab").before(torformfields);
|
||||
});
|
||||
|
||||
function addAction() {
|
||||
|
@ -1922,14 +2067,14 @@
|
|||
};
|
||||
|
||||
$('#rtorrent_test').click(function () {
|
||||
var host = document.getElementById('rtorrent_host').value;
|
||||
var username = document.getElementById('rtorrent_username').value;
|
||||
var password = document.getElementById('rtorrent_password').value;
|
||||
var auth = document.getElementById('rtorrent_authentication').value;
|
||||
var verify = document.getElementById('rtorrent_verify').value;
|
||||
var ssl = document.getElementById('rtorrent_ssl').value;
|
||||
var rpc_url = document.getElementById('rtorrent_rpc_url').value;
|
||||
alert(host);
|
||||
var imagechk = document.getElementById("rtorrent_statusicon");
|
||||
var host = document.getElementById("rtorrent_host").value;
|
||||
var username = document.getElementById("rtorrent_username").value;
|
||||
var password = document.getElementById("rtorrent_password").value;
|
||||
var auth = document.getElementById("rtorrent_authentication").value;
|
||||
var verify = document.getElementById("rtorrent_verify").value;
|
||||
var ssl = document.getElementById("rtorrent_ssl").value;
|
||||
var rpc_url = document.getElementById("rtorrent_rpc_url").value;
|
||||
$.get("testrtorrent",
|
||||
{ host: host, username: username, password: password, auth: auth, verify: verify, ssl: ssl, rpc_url: rpc_url },
|
||||
function(data){
|
||||
|
@ -1939,13 +2084,40 @@
|
|||
}
|
||||
//$('#rtorrentstatus').val(data);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#newznab_test').click(function (x) {
|
||||
// var name = document.getElementById("newznab_name").value;
|
||||
alert(x);
|
||||
$(".newznabtest").click(function () {
|
||||
var newznab = this.attributes["name"].value.replace('newznab_test', '');
|
||||
if ( newznab.indexOf("test_dognzb") > -1) {
|
||||
var imagechk = document.getElementById("dognzb_statusicon");
|
||||
var name = 'DOGnzb';
|
||||
var host = 'https://api.dognzb.cr';
|
||||
var ssl = document.getElementById("dognzb_verify").value;
|
||||
var apikey = document.getElementById("dognzb_apikey").value;
|
||||
} else if ( newznab.indexOf("test_nzbsu") > -1) {
|
||||
var imagechk = document.getElementById("nzbsu_statusicon");
|
||||
var name = 'nzb.su';
|
||||
var host = 'https://api.nzb.su';
|
||||
var ssl = document.getElementById("nzbsu_verify").value;
|
||||
var apikey = document.getElementById("nzbsu_apikey").value;
|
||||
} else {
|
||||
var imagechk = document.getElementById("newznabstatus"+newznab);
|
||||
var name = document.getElementById("newznab_name"+newznab).value;
|
||||
var host = document.getElementById("newznab_host"+newznab).value;;
|
||||
var ssl = document.getElementById("newznab_verify"+newznab).value;
|
||||
var apikey = document.getElementById("newznab_api"+newznab).value;
|
||||
}
|
||||
$.get("testnewznab",
|
||||
{ name: name, host: host, ssl: ssl, apikey: apikey },
|
||||
function(data){
|
||||
|
@ -1953,14 +2125,50 @@
|
|||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
$('#newznabstatus').show();
|
||||
$('#ajaxMsg').html("Successfully connected to newnzab");
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$(".torznabtest").click(function () {
|
||||
var torznab = this.attributes["name"].value.replace('torznab_test', '');
|
||||
var imagechk = document.getElementById("tornabstatus"+torznab);
|
||||
var name = document.getElementById("torznab_name"+torznab).value;
|
||||
var host = document.getElementById("torznab_host"+torznab).value;
|
||||
var apikey = document.getElementById("torznab_api"+torznab).value;
|
||||
$.get("testtorznab",
|
||||
{ name: name, host: host, ssl: ssl, apikey: apikey },
|
||||
function(data){
|
||||
if (data.error != undefined) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#nma_test').click(function () {
|
||||
var apikey = document.getElementById('nma_apikey').value;
|
||||
var imagechk = document.getElementById("nma_statusicon");
|
||||
var apikey = document.getElementById("nma_apikey").value;
|
||||
$.get("testNMA",
|
||||
{ apikey: apikey },
|
||||
function(data){
|
||||
|
@ -1970,21 +2178,50 @@
|
|||
}
|
||||
$('#nmastatus').val(data);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#prowl_test').click(function () {
|
||||
var imagechk = document.getElementById("prowl_statusicon");
|
||||
var apikey = document.getElementById("prowl_keys");
|
||||
$.get("testprowl",
|
||||
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
|
||||
{ apikey: apikey },
|
||||
function (data) {
|
||||
if (data.error != undefined) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#pushover_test').click(function () {
|
||||
var apikey = document.getElementById('pushover_apikey').value;
|
||||
var userkey = document.getElementById('pushover_userkey').value;
|
||||
var imagechk = document.getElementById("pushover_statusicon");
|
||||
var apikey = document.getElementById("pushover_apikey").value;
|
||||
var userkey = document.getElementById("pushover_userkey").value;
|
||||
var device = document.getElementById("pushover_device").value;
|
||||
$.get("testpushover",
|
||||
{ apikey: apikey, userkey: userkey },
|
||||
{ apikey: apikey, userkey: userkey , device: device},
|
||||
function(data){
|
||||
if (data.error != undefined) {
|
||||
alert(data.error);
|
||||
|
@ -1992,18 +2229,47 @@
|
|||
}
|
||||
$('#pushoverstatus').val(data);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#boxcar_test').click(function () {
|
||||
var imagechk = document.getElementById("boxcar_statusicon");
|
||||
var token = document.getElementById("boxcar_token");
|
||||
$.get("testboxcar",
|
||||
function (data) { $('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>"); });
|
||||
{ token: token },
|
||||
function (data) {
|
||||
if (data.error != undefined) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
$('#pushoverstatus').val(data);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#pushbullet_test').click(function () {
|
||||
var apikey = document.getElementById('pushbullet_apikey').value;
|
||||
var imagechk = document.getElementById("pushbullet_statusicon");
|
||||
var apikey = document.getElementById("pushbullet_apikey").value;
|
||||
$.get("testpushbullet",
|
||||
{ apikey: apikey },
|
||||
function(data){
|
||||
|
@ -2013,13 +2279,23 @@
|
|||
}
|
||||
$('#pbstatus').val(data);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#telegram_test').click(function () {
|
||||
var userid = document.getElementById('telegram_userid').value;
|
||||
var token = document.getElementById('telegram_token').value;
|
||||
var imagechk = document.getElementById("telegram_statusicon");
|
||||
var userid = document.getElementById("telegram_userid").value;
|
||||
var token = document.getElementById("telegram_token").value;
|
||||
$.get("testtelegram",
|
||||
{ userid: userid, token: token },
|
||||
function(data){
|
||||
|
@ -2029,12 +2305,22 @@
|
|||
}
|
||||
$('#telegramstatus').val(data);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
$('#slack_test').click(function () {
|
||||
var webhook_url = document.getElementById('slack_webhook_url').value;
|
||||
var imagechk = document.getElementById("slack_statusicon");
|
||||
var webhook_url = document.getElementById("slack_webhook_url").value;
|
||||
$.get("testslack",
|
||||
{ webhook_url: webhook_url },
|
||||
function(data){
|
||||
|
@ -2044,7 +2330,16 @@
|
|||
}
|
||||
$('#slackstatus').val(data);
|
||||
$('#ajaxMsg').html("<div class='msg'><span class='ui-icon ui-icon-check'></span>"+data+"</div>");
|
||||
});
|
||||
if ( data.indexOf("Successfully") > -1){
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/success.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
} else {
|
||||
imagechk.src = "";
|
||||
imagechk.src = "interfaces/default/images/fail.png";
|
||||
imagechk.style.visibility = "visible";
|
||||
}
|
||||
});
|
||||
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
|
||||
});
|
||||
|
||||
|
|
|
@ -349,6 +349,14 @@ table.display tr.even.gradeT {
|
|||
background-color: #F9CBE6;
|
||||
}
|
||||
|
||||
table.display tr.odd.gradeF {
|
||||
background-color: #FF5858;
|
||||
}
|
||||
|
||||
table.display tr.even.gradeF {
|
||||
background-color: #FF5858;
|
||||
}
|
||||
|
||||
table.display tr.gradeL #status {
|
||||
background: url("../images/loader_black.gif") no-repeat scroll 15px center transparent;
|
||||
font-size: 11px;
|
||||
|
@ -364,6 +372,7 @@ table.display tr.gradeU td,
|
|||
table.display tr.gradeP td,
|
||||
table.display tr.gradeD td,
|
||||
table.display tr.gradeT td,
|
||||
table.display tr.gradeF td,
|
||||
table.display tr.gradeZ td {border-bottom: 1px solid #FFF;}
|
||||
table.display tr:last-child td {
|
||||
border-bottom: 1px solid #eee;
|
||||
|
@ -454,6 +463,14 @@ table.display_no_select tr.even.gradeT {
|
|||
background-color: #F9CBE6;
|
||||
}
|
||||
|
||||
table.display_no_select tr.odd.gradeF {
|
||||
background-color: #FF5858;
|
||||
}
|
||||
|
||||
table.display_no_select tr.even.gradeF {
|
||||
background-color: #FF5858;
|
||||
}
|
||||
|
||||
table.display_no_select tr.odd.gradeZ {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
@ -476,6 +493,7 @@ table.display_no_select tr.gradeU td,
|
|||
table.display_no_select tr.gradeP td,
|
||||
table.display_no_select tr.gradeD td,
|
||||
table.display_no_select tr.gradeT td,
|
||||
table.display_no_select tr.gradeF td,
|
||||
table.display_no_select tr.gradeZ td {border-bottom: 1px solid #FFF;}
|
||||
table.display_no_select tr:last-child td {
|
||||
border-bottom: 1px solid #eee;
|
||||
|
|
|
@ -1054,7 +1054,7 @@ div#artistheader h2 a {
|
|||
vertical-align: middle;
|
||||
}
|
||||
#arc_detail th#options {
|
||||
min-width: 150px;
|
||||
min-width: 130px;
|
||||
text-align: left;
|
||||
}
|
||||
#arc_detail th#comicname {
|
||||
|
@ -1066,11 +1066,11 @@ div#artistheader h2 a {
|
|||
text-align: center;
|
||||
}
|
||||
#arc_detail th#status {
|
||||
max-width: 75px;
|
||||
max-width: 85px;
|
||||
text-align: center;
|
||||
}
|
||||
#arc_detail th#issuedate {
|
||||
max-width: 45px;
|
||||
max-width: 58px;
|
||||
text-align: center;
|
||||
}
|
||||
#arc_detail th#readingorder {
|
||||
|
@ -1090,17 +1090,17 @@ div#artistheader h2 a {
|
|||
vertical-align: middle;
|
||||
}
|
||||
#arc_detail td#status {
|
||||
max-width: 75px;
|
||||
max-width: 85px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#arc_detail td#issuedate {
|
||||
max-width: 45px;
|
||||
max-width: 58px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#arc_detail td#options {
|
||||
min-width: 150px;
|
||||
min-width: 130px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
@ -1144,7 +1144,7 @@ div#artistheader h2 a {
|
|||
#pull_table th#publishdate {
|
||||
min-width: 50px;
|
||||
text-align: left;
|
||||
vertial-align: middle;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#pull_table th#publisher {
|
||||
min-width: 100px;
|
||||
|
@ -1162,14 +1162,19 @@ div#artistheader h2 a {
|
|||
vertical-align: middle;
|
||||
}
|
||||
#pull_table th#status {
|
||||
min-width: 120px;
|
||||
min-width: 30px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#pull_table th#options {
|
||||
min-width: 90px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#pull_table td#publishdate {
|
||||
max-width: 50px;
|
||||
text-align: left;
|
||||
vertial-align: middle;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#pull_table td#publisher {
|
||||
min-width: 100px;
|
||||
|
@ -1187,9 +1192,14 @@ div#artistheader h2 a {
|
|||
vertical-align: middle;
|
||||
}
|
||||
#pull_table td#status {
|
||||
min-width: 120px;
|
||||
min-width: 30px;
|
||||
text-align: left;
|
||||
vertial-align: middle;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#pull_table td#options {
|
||||
min-width: 90px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#manage_comic th#name {
|
||||
min-width: 275px;
|
||||
|
@ -1605,6 +1615,46 @@ div#artistheader h2 a {
|
|||
vertical-align: middle;
|
||||
text-align: left;
|
||||
}
|
||||
#downloads_table th#title {
|
||||
max-width: 150px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
#downloads_table th#size {
|
||||
max-width: 35px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
#downloads_table th#kind {
|
||||
max-width: 20px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
#downloads_table th#provider {
|
||||
max-width: 30px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
#downloads_table td#title {
|
||||
max-width: 150px;
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
}
|
||||
#downloads_table td#size {
|
||||
max-width: 35px;
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
}
|
||||
#downloads_table td#kind {
|
||||
max-width: 20px;
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
}
|
||||
#downloads_table td#provider {
|
||||
max-width: 30px;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
#searchmanage_table th#comicname {
|
||||
min-width: 325px;
|
||||
text-align: left;
|
||||
|
|
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 855 B After Width: | Height: | Size: 855 B |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 7.0 KiB |
0
data/interfaces/default/images/publisherlogos/logo-bluewatercomics.png
Executable file → Normal file
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 152 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
0
data/interfaces/default/images/publisherlogos/logo-midtowncomics.jpg
Executable file → Normal file
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 9.8 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 6.0 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 197 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -130,7 +130,7 @@
|
|||
"search" : ""},
|
||||
"stateSave": true,
|
||||
"pageLength": 25,
|
||||
"pagingType": "full_numbers",
|
||||
"pagingType": "full_numbers"
|
||||
});
|
||||
|
||||
resetFilters("comic");
|
||||
|
|
|
@ -171,7 +171,14 @@ function refreshTable() {
|
|||
initThisPage();
|
||||
});
|
||||
}
|
||||
function refreshLoadArtist() {
|
||||
function refreshTab() {
|
||||
var url = $(location).attr('href');
|
||||
var tabId = $('.ui-tabs-panel:visible').attr("id");
|
||||
$('.ui-tabs-panel:visible').load(url + " #"+ tabId, function() {
|
||||
initThisPage();
|
||||
});
|
||||
}
|
||||
function refreshLoadSeries() {
|
||||
if ( $(".gradeL").length > 0 ) {
|
||||
var url = $(location).attr('href');
|
||||
var loadingRow = $("table.display tr.gradeL")
|
||||
|
@ -186,7 +193,7 @@ function refreshLoadArtist() {
|
|||
} else {
|
||||
// Still loading
|
||||
setTimeout(function(){
|
||||
refreshLoadArtist();
|
||||
refreshLoadSeries();
|
||||
},3000);
|
||||
}
|
||||
});
|
||||
|
@ -194,14 +201,6 @@ function refreshLoadArtist() {
|
|||
}
|
||||
}
|
||||
|
||||
function refreshTab() {
|
||||
var url = $(location).attr('href');
|
||||
var tabId = $('.ui-tabs-panel:visible').attr("id");
|
||||
$('.ui-tabs-panel:visible').load(url + " #"+ tabId, function() {
|
||||
initThisPage();
|
||||
});
|
||||
}
|
||||
|
||||
function showMsg(msg,loader,timeout,ms) {
|
||||
var feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
|
|
|
@ -15,19 +15,19 @@
|
|||
|
||||
<body>
|
||||
<form action="${http_root}auth/login" method="post">
|
||||
<input type="hidden" name="from_page" value="${from_page}" />
|
||||
<div class="wrap">
|
||||
<div class="avatar">
|
||||
<img src="${http_root}images/mylarlogo.png" />
|
||||
</div>
|
||||
<input type="text" placeholder="username" name="username" required autofocus />
|
||||
<input type="text" placeholder="username" name="current_username" id="current_username" required autofocus autocomplete/>
|
||||
<div class="bar">
|
||||
<i></i>
|
||||
</div>
|
||||
<input type="password" placeholder="password" name="password" required />
|
||||
<button>Sign in</button>
|
||||
<input type="password" placeholder="password" name="current_password" id="current_password" required autocomplete/>
|
||||
<button>Sign in</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
<a id="menu_link_delete" href="#" onclick="doAjaxCall('clearLogs',$(this),'table')" data-success="All logs cleared">Clear Log</a>
|
||||
<a id="menu_link_edit" href="toggleVerbose">Toggle Debug Log
|
||||
%if mylar.VERBOSE:
|
||||
<a id="menu_link_edit" href="toggleVerbose">Toggle Debug Logging
|
||||
%if mylar.LOG_LEVEL < 2:
|
||||
ON
|
||||
%else:
|
||||
OFF
|
||||
%endif
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
<option value="pause">Pause Series</option>
|
||||
<option value="recheck">Recheck Files</option>
|
||||
<option value="refresh">Refresh Series</option>
|
||||
<option value="rename">Rename Series</option>
|
||||
<option value="resume">Resume Series</option>
|
||||
</select>
|
||||
selected comics
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
except:
|
||||
ahash = None
|
||||
%>
|
||||
%if ahash is not None:
|
||||
%if ahash is not None and mylar.CONFIG.AUTO_SNATCH is True:
|
||||
<a href="#" onclick="doAjaxCall('torrentit?torrent_hash=${ahash}&download=True',$(this),'table')" title="Auto-Snatch torrent file"><span class="ui-icon ui-icon-plus"></span>auto-snatch</a>
|
||||
%endif
|
||||
</td>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<label>Arc Folder Format</label>
|
||||
<input type="text" title="$publisher, $spanyears, $arc" name="arc_folderformat" value="${mylar.CONFIG.ARC_FOLDERFORMAT}" size="25">
|
||||
</div>
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="read2filename" id="read2filename" value="1" ${checked(mylar.CONFIG.READ2FILENAME)} /><label>Append Reading # to filename</label></br>
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="read2filename" id="read2filename" value="1" ${checked(mylar.CONFIG.READ2FILENAME)} /><label>Prepend Reading # to filename</label></br>
|
||||
</div>
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="copy2arcdir" id="copy2arcdir" value="1" ${checked(mylar.CONFIG.COPY2ARCDIR)} />
|
||||
<select name="arc_fileops" id="arc_fileops">
|
||||
|
|
|
@ -23,11 +23,14 @@
|
|||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
<div id="paddingheader">
|
||||
<h1 class="clearfix"><a href="storyarc_main"><img src="interfaces/default/images/ReadingList-icon.png" height="26" width="26" alt="Story Arc Management"/>Story Arc Management</a></h1>
|
||||
</div>
|
||||
<center><h1>${storyarcname}</h1></center>
|
||||
<div id="tabs">
|
||||
<h1 class="clearfix"><a href="storyarc_main"><img src="interfaces/default/images/ReadingList-icon.png" height="26" width="26" align="left" alt="Story Arc Management"/></a></h1>
|
||||
%if storyarcbanner is not None:
|
||||
<img src="${storyarcbanner}" onload="resizeimage('${bannerheight}')" width="960" height="${bannerheight}" id="banner" style="text-decoration: none; position: relative; top: 40px;"/>
|
||||
%else:
|
||||
</br>
|
||||
%endif
|
||||
|
||||
<div id="tabs" style="z-index:10">
|
||||
<ul>
|
||||
<li><a href="#tabs-1">Options</a></li>
|
||||
</ul>
|
||||
|
@ -37,17 +40,27 @@
|
|||
<table class="configtable">
|
||||
<tr>
|
||||
<form action="arcOptions" id="chkoptions" method="GET">
|
||||
<fieldset>
|
||||
<%
|
||||
if arcdetail['percent'] == 101:
|
||||
css = '<div class=\"progress-container warning\">'
|
||||
if arcdetail['percent'] == 100:
|
||||
css = '<div class=\"progress-container complete\">'
|
||||
if arcdetail['percent'] < 100:
|
||||
css = '<div class=\"progress-container missing\">'
|
||||
|
||||
%>
|
||||
<fieldset>
|
||||
<div class="row checkbox left clearfix">
|
||||
%if mylar.CONFIG.STORYARCDIR:
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="storyarcdir" id="storyarcdir" value="1" ${checked(mylar.CONFIG.STORYARCDIR)} /><label>Should I create a Story-Arc Directory?</label><br/>
|
||||
<small>Arcs in StoryArc Directory: ${sdir}</small><br/>
|
||||
</br>
|
||||
%if mylar.CONFIG.STORYARCDIR is True:
|
||||
<label>Location: ${sdir}</label>
|
||||
%endif
|
||||
</br>
|
||||
<!--
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" /><label>Show Downloaded Story Arc Issues on ReadingList tab</label><br/>
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" /><label>Enforce Renaming/MetaTagging options (if enabled)</label><br/>
|
||||
-->
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="read2filename" id="read2filename" value="1" ${checked(mylar.CONFIG.READ2FILENAME)} /><label>Append Reading# to filename</label><br/>
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="read2filename" id="read2filename" value="1" ${checked(mylar.CONFIG.READ2FILENAME)} /><label>Prepend Reading# to filename</label><br/>
|
||||
<%
|
||||
if mylar.CONFIG.STORYARCDIR:
|
||||
carcdir = 'StoryArc'
|
||||
|
@ -55,15 +68,44 @@
|
|||
carcdir = 'GrabBag'
|
||||
%>
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="copy2arcdir" id="copy2arcdir" value="1" ${checked(mylar.CONFIG.COPY2ARCDIR)} /><label>Copy watchlisted issues to ${carcdir} Directory</label>
|
||||
|
||||
<input type="hidden" name="StoryArcID" value="${storyarcid}">
|
||||
<input type="hidden" name="StoryArcName" value="${storyarcname}">
|
||||
|
||||
</div>
|
||||
</fieldset>
|
||||
<div>
|
||||
<input type="submit" value="Update"/>
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div style="display:block;position:relative;top:10px;">
|
||||
<input type="submit" value="Update"/>
|
||||
</div>
|
||||
<div style="display:block;float:right;position:relative;text-color:black;top:-130px;">
|
||||
<h1><p style="display:inline;float:right;">${storyarcname}</h1>
|
||||
<div style="display:inline;float:right;"><span title="${arcdetail['percent']}"></span>${css}<div style="width:${arcdetail['percent']}%"><span class="progressbar-front-text">${arcdetail['Have']}/${arcdetail['Total']}</span></div></div></div>
|
||||
</div>
|
||||
<%
|
||||
if storyarcbanner is None:
|
||||
optpos = '<div style=\"display:block;float:right;position:absolute;right:20px;top:120px;\">'
|
||||
else:
|
||||
optpos = '<div style=\"display:block;float:right;position:absolute;right:20px;top:100px;\">'
|
||||
%>
|
||||
${optpos}
|
||||
<form action="downloadBanner" method="GET">
|
||||
<label style="display:inline;float:right;">Banner Image Retrieval</label></br>
|
||||
<input style="display:inline;float:right;" type="text" title="enter full url where image resides" placeholder="full URL where image resides" onfocus="if(this.value==this.defaultValue) this.value='';" name="url" size="25">
|
||||
<input style="display:inline;float:right;" type="image" src="interfaces/default/images/download_icon.png" height="20" width="20" alt="submit"></br>
|
||||
<input type="hidden" name="storyarcid" value="${storyarcid}">
|
||||
<input type="hidden" name="storyarcname" value="${storyarcname}">
|
||||
%if storyarcbanner is not None:
|
||||
<p style="display:inline;float:right;">
|
||||
<input type="hidden" name="height" id="height" value="" />
|
||||
<button type="button" onclick="zoomin()">Zoom In</button>
|
||||
<button type="button" onclick="zoomout()">Zoom Out</button>
|
||||
<button type="button" onclick="getHeight('${storyarcid}', 'delete')" data-success="Successfully deleted banner image">Clear</button>
|
||||
<button type="button" onclick="getHeight('${storyarcid}', 'save')" data-success="Saved new height dimensions for banner">Save</button>
|
||||
</p>
|
||||
%endif
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -73,8 +115,8 @@
|
|||
<tr>
|
||||
<th id="readingorder"></th>
|
||||
<th id="comicname">ComicName</th>
|
||||
<th id="issue">Issue</th>
|
||||
<th id="issuedate">Pub Date</th>
|
||||
<th id="issue">#</th>
|
||||
<th id="issuedate">Date</th>
|
||||
<th id="status">Status</th>
|
||||
<th id="options">Options</th>
|
||||
</tr>
|
||||
|
@ -85,6 +127,7 @@
|
|||
%>
|
||||
%for item in readlist:
|
||||
<%
|
||||
|
||||
if item['Status'] == 'Skipped':
|
||||
grade = 'Z'
|
||||
elif item['Status'] == 'Wanted':
|
||||
|
@ -107,7 +150,8 @@
|
|||
grade = 'Z'
|
||||
|
||||
if item['ComicID'] in cids:
|
||||
haveit = cids[item['ComicID']]
|
||||
ht = cids[item['ComicID']]
|
||||
haveit = ht['comicid']
|
||||
else:
|
||||
haveit = "No"
|
||||
|
||||
|
@ -135,8 +179,8 @@
|
|||
if item['IssueDate'] != '0000-00-00' and item['IssueDate'] is not None:
|
||||
issuedate = item['IssueDate']
|
||||
else:
|
||||
if item['StoreDate'] != '0000-00-00' and item['StoreDate'] is not None:
|
||||
issuedate = item['StoreDate']
|
||||
if item['ReleaseDate'] != '0000-00-00' and item['ReleaseDate'] is not None:
|
||||
issuedate = item['ReleaseDate']
|
||||
else:
|
||||
# this is needed for imported cbl's
|
||||
try:
|
||||
|
@ -194,6 +238,7 @@
|
|||
console.log(this);
|
||||
console.log(value);
|
||||
console.log(settings);
|
||||
refreshTable();
|
||||
return(value);
|
||||
}
|
||||
});
|
||||
|
@ -205,6 +250,51 @@
|
|||
return true;
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
function zoomin(){
|
||||
var myImg = document.getElementById("banner");
|
||||
var currHeight = myImg.clientHeight;
|
||||
if(currHeight == 400){
|
||||
alert("Maximum zoom-in level reached.");
|
||||
} else{
|
||||
myImg.style.height = (currHeight + 50) + "px";
|
||||
}
|
||||
}
|
||||
function zoomout(){
|
||||
var myImg = document.getElementById("banner");
|
||||
var currHeight = myImg.clientHeight;
|
||||
if(currHeight == 50){
|
||||
alert("Maximum zoom-out level reached.");
|
||||
} else{
|
||||
myImg.style.height = (currHeight - 50) + "px";
|
||||
}
|
||||
}
|
||||
function resizeimage(height) {
|
||||
var myImg = document.getElementById("banner");
|
||||
myImg.style.height = height + "px";
|
||||
}
|
||||
function getHeight(comicid, action) {
|
||||
var myImg = document.getElementById("banner");
|
||||
var curHeight = myImg.clientHeight;
|
||||
//document.getElementById("#height").setAttribute("value", curHeight);
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "manageBanner",
|
||||
data: { comicid: comicid, action: action, height: curHeight },
|
||||
success: function(response) {
|
||||
if (action = "save") {
|
||||
myImg.style.height = curHeight + "px";
|
||||
} else{
|
||||
document.getElementById("#height").src="";
|
||||
}
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('ERROR'+data.responseText);
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function initThisPage() {
|
||||
$(function() {
|
||||
|
|
|
@ -0,0 +1,323 @@
|
|||
<%inherit file="base.html"/>
|
||||
<%!
|
||||
import os
|
||||
import mylar
|
||||
from mylar.helpers import checked, listLibrary
|
||||
%>
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
<div id="subhead_container">
|
||||
<div id="subhead_menu">
|
||||
%if mylar.CONFIG.TAB_ENABLE:
|
||||
<a id="menu_link_delete" onclick="doAjaxCall('syncfiles',$(this),'table')" data-success="Successfully sent issues to your device">Sync</a>
|
||||
%endif
|
||||
<a id="menu_link_delete" href="#">Remove Read</a>
|
||||
<a id="menu_link_delete" href="#">Clear File Cache</a>
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('SearchArcIssues?StoryArcID=${storyarcid}',$(this),'table')" data-success="Now searching for Missing StoryArc Issues">Search for Missing</a>
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('ArcWatchlist?StoryArcID=${storyarcid}',$(this),'table')" data-success="Now searching for matches on Watchlist">Search for Watchlist matches</a>
|
||||
%if cvarcid:
|
||||
<a id="menu_link_refresh" onclick="doAjaxCall('addStoryArc_thread?arcid=${storyarcid}&cvarcid=${cvarcid}&storyarcname=${storyarcname}&arcrefresh=True',$(this),'table')" data-success="Refreshed Story Arc">Refresh Story Arc</a>
|
||||
%endif
|
||||
</div>
|
||||
<h1 class="clearfix"><a href="storyarc_main"><img src="interfaces/default/images/ReadingList-icon.png" height="26" width="26" align="left" alt="Story Arc Management"/></a></h1>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
</br></br>
|
||||
<div id="tabs" width="100%">
|
||||
<ul>
|
||||
<li><a href="#tabs-1">Options</a></li>
|
||||
</ul>
|
||||
<div id="tabs-1">
|
||||
<table summary="Comic Details" border="1">
|
||||
<tr/><tr>
|
||||
<td id="mainimg">
|
||||
<fieldset>
|
||||
<div id="artistImg">
|
||||
<img src="${storyarcbanner}" onload="resizeimage('${bannerwidth}')" height="400" width="${bannerwidth}" id="banner" style="text-decoration: none;position: relative;top:0px;right:0px;left:0px;"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<%
|
||||
if arcdetail['percent'] == 101:
|
||||
css = '<div class=\"progress-container warning\">'
|
||||
if arcdetail['percent'] == 100:
|
||||
css = '<div class=\"progress-container complete\">'
|
||||
if arcdetail['percent'] < 100:
|
||||
css = '<div class=\"progress-container missing\">'
|
||||
|
||||
%>
|
||||
<div style="display:table;margin:auto;position:relative;top:0px;"><span title="${arcdetail['percent']}"></span>${css}<div style="width:${arcdetail['percent']}%"><span class="progressbar-front-text">${arcdetail['Have']}/${arcdetail['Total']}</span></div></div></div>
|
||||
</td>
|
||||
<td width="100%" padding="5px" style="vertical-align:bottom;">
|
||||
<div style="display:inline;position:relative;text-color:black;top:-200px;margin:0 auto;">
|
||||
<center><h1>${storyarcname}</h1>(${spanyears})</center>
|
||||
</div>
|
||||
|
||||
<div style="position:relative;left:20px;float:left;top:-30px;">
|
||||
<form action="arcOptions" id="chkoptions" method="GET">
|
||||
<div class="row checkbox left clearfix">
|
||||
%if mylar.CONFIG.STORYARCDIR is True:
|
||||
<label>Location: ${sdir}</label>
|
||||
%endif
|
||||
</div>
|
||||
<div class="row checkbox left clearfix">
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="read2filename" id="read2filename" value="1" ${checked(mylar.CONFIG.READ2FILENAME)} /><label>Prepend Reading# to filename</label>
|
||||
<%
|
||||
if mylar.CONFIG.STORYARCDIR:
|
||||
carcdir = 'StoryArc'
|
||||
else:
|
||||
carcdir = 'GrabBag'
|
||||
%>
|
||||
</div>
|
||||
<div class="row checkbox left clearfix">
|
||||
<input type="checkbox" style="vertical-align: middle; margin: 3px; margin-top: -1px;" name="copy2arcdir" id="copy2arcdir" value="1" ${checked(mylar.CONFIG.COPY2ARCDIR)} /><label>Copy watchlisted issues to ${carcdir} Directory</label>
|
||||
</div>
|
||||
<input type="hidden" name="StoryArcID" value="${storyarcid}">
|
||||
<input type="hidden" name="StoryArcName" value="${storyarcname}">
|
||||
<div style="display:inline;position:relative;top:0px;">
|
||||
<input type="submit" value="Update"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<%
|
||||
optpos = '<div style=\"display:block;float:right;position:relative;right:20px;top:50px;\">'
|
||||
%>
|
||||
${optpos}
|
||||
<form action="downloadBanner" method="GET">
|
||||
<label style="display:inline;float:right;">Banner Image Retrieval</label></br>
|
||||
<input style="display:inline;float:right;" type="text" title="enter full url where image resides" placeholder="full URL where image resides" onfocus="if(this.value==this.defaultValue) this.value='';" name="url" size="25">
|
||||
<input style="display:inline;float:right;" type="image" src="interfaces/default/images/download_icon.png" height="20" width="20" alt="submit"></br>
|
||||
<input type="hidden" name="storyarcid" value="${storyarcid}">
|
||||
<input type="hidden" name="storyarcname" value="${storyarcname}">
|
||||
%if storyarcbanner is not None:
|
||||
<p style="display:inline;float:right;">
|
||||
<input type="hidden" name="height" id="height" value="" />
|
||||
<button type="button" onclick="zoomin()">Zoom In</button>
|
||||
<button type="button" onclick="zoomout()">Zoom Out</button>
|
||||
<button type="button" onclick="getWidth('${storyarcid}', 'delete')" data-success="Successfully deleted banner image">Clear</button>
|
||||
<button type="button" onclick="getWidth('${storyarcid}', 'save')" data-success="Saved new height dimensions for banner">Save</button>
|
||||
</p>
|
||||
%endif
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</br>
|
||||
<table class="display" id="arc_detail">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="readingorder"></th>
|
||||
<th id="comicname">ComicName</th>
|
||||
<th id="issue">#</th>
|
||||
<th id="issuedate">Date</th>
|
||||
<th id="status">Status</th>
|
||||
<th id="options">Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%
|
||||
cids = listLibrary()
|
||||
%>
|
||||
%for item in readlist:
|
||||
<%
|
||||
|
||||
if item['Status'] == 'Skipped':
|
||||
grade = 'Z'
|
||||
elif item['Status'] == 'Wanted':
|
||||
grade = 'X'
|
||||
elif item['Status'] == 'Snatched':
|
||||
grade = 'C'
|
||||
elif item['Status'] == 'Downloaded':
|
||||
grade = 'A'
|
||||
elif item['Status'] == 'Archived':
|
||||
grade = 'A'
|
||||
elif item['Status'] == 'Ignored':
|
||||
grade = 'A'
|
||||
elif item['Status'] == 'Failed':
|
||||
grade = 'C'
|
||||
elif item['Status'] == 'Read':
|
||||
grade = 'C'
|
||||
elif item['Status'] == 'Not Watched':
|
||||
grade = 'X'
|
||||
else:
|
||||
grade = 'Z'
|
||||
|
||||
if item['ComicID'] in cids:
|
||||
ht = cids[item['ComicID']]
|
||||
haveit = ht['comicid']
|
||||
else:
|
||||
haveit = "No"
|
||||
|
||||
if all([item['Volume'] is not None, item['Volume'] != 'None']):
|
||||
volume = 'V' + item['Volume']
|
||||
else:
|
||||
volume = ''
|
||||
|
||||
%>
|
||||
|
||||
<tr id="${item['ReadingOrder']}" class="grade${grade}">
|
||||
<td class="edit" title="Change the order (click to edit)" id="${storyarcid}.${item['IssueArcID']}">${item['ReadingOrder']}</td>
|
||||
<td id="comicname" title="${item['IssueName']}">
|
||||
%if haveit == "No":
|
||||
${item['ComicName']} ${volume} (${item['SeriesYear']})
|
||||
%else:
|
||||
<a href="comicDetails?ComicID=${haveit}">${item['ComicName']} ${volume} (${item['SeriesYear']})</a>
|
||||
|
||||
%endif
|
||||
</td>
|
||||
|
||||
<td id="issue">${item['IssueNumber']}</td>
|
||||
<%
|
||||
try:
|
||||
if item['IssueDate'] != '0000-00-00' and item['IssueDate'] is not None:
|
||||
issuedate = item['IssueDate']
|
||||
else:
|
||||
if item['ReleaseDate'] != '0000-00-00' and item['ReleaseDate'] is not None:
|
||||
issuedate = item['ReleaseDate']
|
||||
else:
|
||||
# this is needed for imported cbl's
|
||||
try:
|
||||
issuedate = item['issueYEAR']
|
||||
except:
|
||||
issuedate = '0000-00-00'
|
||||
except:
|
||||
issuedate = '0000-00-00'
|
||||
%>
|
||||
<td id="issuedate">${issuedate}</td>
|
||||
<td id="status">${item['Status']}
|
||||
%if item['Status'] == 'Downloaded' or item['Status'] == 'Archived':
|
||||
<a href="#" title="${item['Location']}"><img src="interfaces/default/images/info32.png" height="16" alt="" class="highqual" /></a>
|
||||
%endif
|
||||
</td>
|
||||
<td id="options">
|
||||
%if any([item['Status'] is None, item['Status'] == None, item['Status'] == 'Skipped']):
|
||||
<a href="#" onclick="doAjaxCall('queueit?ComicName=${item['ComicName'] | u}&ComicIssue=${item['IssueNumber']}&ComicYear=${issuedate}&mode=readlist&SARC=${item['StoryArc']}&IssueArcID=${item['IssueArcID']}&SeriesYear=${item['SeriesYear']}',$(this),'table')" data-success="Now searching for ${item['ComicName']} #${item['IssueNumber']}"><span class="ui-icon ui-icon-plus"></span>Grab</a>
|
||||
%elif item['Status'] == 'Snatched':
|
||||
<a href="#" onclick="doAjaxCall('queueit?ComicName=${item['ComicName'] | u}&ComicIssue=${item['IssueNumber']}&ComicYear=${issuedate}&mode=readlist&SARC=${item['StoryArc']}&IssueArcID=${item['IssueArcID']}&SeriesYear=${item['SeriesYear']}',$(this),'table')" data-success="Trying to search again for issue"><span class="ui-icon ui-icon-plus"></span>Retry</a>
|
||||
%elif item['Status'] == 'Downloaded' and item['Location'] is not None:
|
||||
<a href="downloadthis?pathfile=${item['Location'] |u}"><span class="ui-icon ui-icon-plus"></span>Download</a>
|
||||
%endif
|
||||
<a href="#" title="Remove Issue from Story Arc" onclick="doAjaxCall('removefromreadlist?IssueArcID=${item['IssueArcID']}&manual=${item['Manual']}',$(this),'table')" data-success='Successfully deleted ${item['IssueArcID']}'><span class="ui-icon ui-icon-minus"></span>Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</br>
|
||||
<div style="position:relative; width:960px; height:10px; margin:10px auto;">
|
||||
<form action="manual_arc_add" method="GET">
|
||||
<input type="hidden" name="storyarcid" value=${storyarcid}>
|
||||
<div style="position:absolute; top:30px; right:10px;">
|
||||
<center><label><strong><a href="#" title="Enter the IssueID of the issue you want to add to the arc"/>Issue ID</a></strong></label>
|
||||
<input type="text" name="manual_issueid" size="10">
|
||||
<label><strong><a href="#" title="Enter the Reading Order # for the given issue"/>Reading Order</a></strong></label>
|
||||
<input type="text" name="manual_readingorder" size="2"><input type="image" src="interfaces/default/images/submit.png" height="25" width="25" class="highqual" /></center>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="headIncludes()">
|
||||
<link rel="stylesheet" href="interfaces/default/css/data_table.css">
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<script src="js/libs/jquery.dataTables.min.js"></script>
|
||||
<script src="js/libs/jquery.jeditable.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.edit').editable('order_edit', {
|
||||
callback : function(value, settings) {
|
||||
console.log(this);
|
||||
console.log(value);
|
||||
console.log(settings);
|
||||
refreshTable();
|
||||
return(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
$("#menu_link_scan").click(function() {
|
||||
$('#chkoptions').submit();
|
||||
return true;
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
function zoomin(){
|
||||
var myImg = document.getElementById("banner");
|
||||
var currWidth = myImg.clientWidth;
|
||||
if(currWidth == 400){
|
||||
alert("Maximum zoom-in level reached.");
|
||||
} else{
|
||||
myImg.style.width = (currWidth + 50) + "px";
|
||||
}
|
||||
}
|
||||
function zoomout(){
|
||||
var myImg = document.getElementById("banner");
|
||||
var currWidth = myImg.clientWidth;
|
||||
if(currWidth == 50){
|
||||
alert("Maximum zoom-out level reached.");
|
||||
} else{
|
||||
myImg.style.width = (currWidth - 50) + "px";
|
||||
}
|
||||
}
|
||||
function resizeimage(width) {
|
||||
var myImg = document.getElementById("banner");
|
||||
myImg.style.width = width + "px";
|
||||
}
|
||||
function getWidth(comicid, action) {
|
||||
var myImg = document.getElementById("banner");
|
||||
var curWidth = myImg.clientWidth;
|
||||
//document.getElementById("#Width").setAttribute("value", curWidth);
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "manageBanner",
|
||||
data: { comicid: comicid, action: action, width: curWidth },
|
||||
success: function(response) {
|
||||
if (action = "save") {
|
||||
myImg.style.width = curWidth + "px";
|
||||
} else{
|
||||
document.getElementById("#width").src="";
|
||||
}
|
||||
},
|
||||
error: function(data)
|
||||
{
|
||||
alert('ERROR'+data.responseText);
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
function initThisPage() {
|
||||
$(function() {
|
||||
$( "#tabs" ).tabs();
|
||||
});
|
||||
initActions();
|
||||
$('#arc_detail').dataTable(
|
||||
{
|
||||
"bDestroy": true,
|
||||
"oLanguage": {
|
||||
"sLengthMenu":"Show _MENU_ items per page",
|
||||
"sEmptyTable": "<em>No History to Display</em>",
|
||||
"sInfo":"Showing _START_ to _END_ of _TOTAL_ items",
|
||||
"sInfoEmpty":"Showing 0 to 0 of 0 items",
|
||||
"sInfoFiltered":"(filtered from _MAX_ total items)"},
|
||||
"iDisplayLength": 25,
|
||||
"sPaginationType": "full_numbers",
|
||||
"aaSorting": []
|
||||
})
|
||||
resetFilters("item");
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
initThisPage();
|
||||
initActions();
|
||||
});
|
||||
</script>
|
||||
</%def>
|
|
@ -21,12 +21,12 @@
|
|||
<div id="checkboxControls" style="float: right; vertical-align: middle; margin: 5px 3px 3px 3px;">
|
||||
<div style="padding-bottom: 5px;">
|
||||
<label for="Wanted" class="checkbox inline Wanted"><input type="checkbox" id="Wanted" checked="checked" /> Wanted: <b>${isCounts['Wanted']}</b></label>
|
||||
%if mylar.CONFIG.UPCOMING_SNATCHED:
|
||||
%if mylar.CONFIG.UPCOMING_SNATCHED is True:
|
||||
%if int(isCounts['Snatched']) > 0:
|
||||
<label for="Snatched" class="checkbox inline Snatched"><input type="checkbox" id="Snatched" checked="checked" /> Snatched: <b>${isCounts['Snatched']}</b></label>
|
||||
%endif
|
||||
%endif
|
||||
%if int(isCounts['Failed']) > 0 and mylar.CONFIG.FAILED_DOWNLOAD_HANDLING:
|
||||
%if int(isCounts['Failed']) > 0 and mylar.CONFIG.FAILED_DOWNLOAD_HANDLING is True:
|
||||
<label for="Failed" class="checkbox inline Failed"><input type="checkbox" id="Failed" checked="checked" /> Failed: <b>${isCounts['Failed']}</b></label>
|
||||
%endif
|
||||
</div>
|
||||
|
@ -54,6 +54,7 @@
|
|||
<th id="comicname">Comic</th>
|
||||
<th id="issuenumber">Issue</th>
|
||||
<th id="reldate">Release Date</th>
|
||||
<th id="options">Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -64,24 +65,65 @@
|
|||
elif issue['Status'] == 'Snatched':
|
||||
grade = 'C'
|
||||
elif issue['Status'] == 'Failed':
|
||||
grade = 'C'
|
||||
grade = 'F'
|
||||
else:
|
||||
grade = 'Z'
|
||||
%>
|
||||
|
||||
<tr class="${issue['Status']} grade${grade}">
|
||||
<td id="select"><input type="checkbox" name="${issue['IssueID']}" class="checkbox" value="${issue['IssueID']}"/></td>
|
||||
<td id="comicname"><a href="comicDetails?ComicID=${issue['ComicID']}">
|
||||
<%
|
||||
linkit = 'comicDetails?ComicID=%s' % issue['ComicID']
|
||||
try:
|
||||
if issue['StoryArcID'] is not None:
|
||||
lineheader = issue['StoryArc']
|
||||
linkit = 'detailStoryArc?StoryArcID=%s&StoryArcName=%s' % (issue['StoryArcID'],issue['StoryArc'])
|
||||
issuenumber = issue['IssueNumber']
|
||||
issueid = issue['IssueArcID']
|
||||
else:
|
||||
lineheader = None
|
||||
issuenumber = issue['Issue_Number']
|
||||
issueid = issue['IssueID']
|
||||
except:
|
||||
lineheader = None
|
||||
issuenumber = issue['Issue_Number']
|
||||
issueid = issue['IssueID']
|
||||
|
||||
if any(d['IssueID'] == str(issue['IssueID']) for d in ann_list):
|
||||
adjcomicname = issue['ComicName'] + ' Annual'
|
||||
else:
|
||||
adjcomicname = issue['ComicName']
|
||||
endif
|
||||
%>
|
||||
${adjcomicname}</a></td>
|
||||
<td id="issuenumber">${issue['Issue_Number']}</td>
|
||||
<td id="select"><input type="checkbox" name="${issueid}" class="checkbox" value="${issueid}"/></td>
|
||||
<td id="comicname">
|
||||
%if lineheader is not None:
|
||||
<b>[${lineheader}]</b><a href="${linkit}"> ${adjcomicname}</a>
|
||||
%else:
|
||||
<a href="${linkit}">${adjcomicname}</a>
|
||||
%endif
|
||||
</td>
|
||||
<td id="issuenumber">${issuenumber}</td>
|
||||
<td id="reldate">${issue['IssueDate']}</td>
|
||||
<td id="options">
|
||||
<!--
|
||||
<a class="menu_link_edit" id="choose_specific_download" title="Choose Specific Download" href="javascript:void(0)" onclick="getAvailableDownloads('${issueid}')"><i class="fa fa-search"></i><img src="interfaces/default/images/magnifier.png" height="25" width="25" class="highqual" /></a>
|
||||
<div id="choose_specific_download_dialog" title="Choose a specific download for ${adjcomicname} #${issuenumber}" style="display:none" class="configtable">
|
||||
<table class="display" id="downloads_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="title">Title</th>
|
||||
<th id="provider">Provider</th>
|
||||
<th id="size">Size</th>
|
||||
<th id="kind">Kind</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="downloads_table_body" value="Now searching....this might take up to 90 seconds.">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
-->
|
||||
</td>
|
||||
</div
|
||||
</tr>
|
||||
%endfor
|
||||
</tbody>
|
||||
|
@ -244,8 +286,70 @@
|
|||
$(this).hide();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function getAvailableDownloads(issueid) {
|
||||
ShowSpinner();
|
||||
$.getJSON("choose_specific_download", {issueid: issueid}, function(data) {
|
||||
loader.remove();
|
||||
feedback.fadeOut();
|
||||
search_results = data
|
||||
for( var i = 0, len = data.length; i < len; i++ ) {
|
||||
$('#downloads_table_body').append('<tr><td id="title"><a href="javascript:void(0)" onclick="downloadSpecificRelease('+i+')">'+data[i].nzbtitle+'</a></td><td id="provider">'+data[i].provider+'</td><td id="size">'+data[i].size+'</td><td id="kind">'+data[i].kind+'</td></tr>');
|
||||
}
|
||||
$('#downloads_table').dataTable({
|
||||
"aoColumns": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"aaSorting": [[ 1, 'desc']],
|
||||
"bFilter": false,
|
||||
"bInfo": false,
|
||||
"bPaginate": false,
|
||||
"bDestroy": true
|
||||
});
|
||||
$("#choose_specific_download_dialog").dialog({
|
||||
width: "70%",
|
||||
maxHeight: 500
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function downloadSpecificRelease(i){
|
||||
|
||||
name = search_results[i].nzbtitle
|
||||
prov = search_results[i].tmpprov
|
||||
nzbid = search_results[i].nzbid
|
||||
ShowSpinner();
|
||||
$.getJSON("download_specific_release", {nzbid: nzbid, provider: prov, name: name}, function(data) {
|
||||
loader.remove();
|
||||
feedback.fadeOut();
|
||||
refreshSubmenu();
|
||||
$("#choose_specific_download_dialog").dialog("close");
|
||||
});
|
||||
}
|
||||
|
||||
function ShowSpinner() {
|
||||
feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
if ( update.is(":visible") ) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom",height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
}
|
||||
loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
||||
feedback.prepend(loader);
|
||||
feedback.fadeIn();
|
||||
}
|
||||
|
||||
var loadingMessage = false;
|
||||
var spinner_active = false;
|
||||
var loadingtext_active = false;
|
||||
var refreshInterval;
|
||||
|
||||
function initThisPage() {
|
||||
$(function() {
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
-->
|
||||
<a id="menu_link_scan" class="button">Download</a>
|
||||
<a href="#" id="menu_link_refresh" onclick="doAjaxCall('pullSearch?week=${weekinfo['weeknumber']}&year=${weekinfo['year']}',$(this),'table')" data-success="Submitted background search request for new pull issues">Manually check for issues</a>
|
||||
%if all([mylar.CONFIG.ENABLE_TORRENT_SEARCH is True, mylar.CONFIG.ENABLE_32P is True, mylar.CONFIG.MODE_32P is True]):
|
||||
<a href="#" id="menu_link_refresh" onclick="doAjaxCall('download_0day?week=${weekinfo['midweek']}',$(this),'table')" data-success="Submitted background search request for 0-day pack for this week">Download 0-Day Pack</a>
|
||||
%endif
|
||||
</div>
|
||||
</div>
|
||||
<a href="home" class="back">« Back to overview</a>
|
||||
|
@ -63,6 +66,7 @@
|
|||
<th id="comicname">Comic</th>
|
||||
<th id="comicnumber">#</th>
|
||||
<th id="status">Status</th>
|
||||
<th id="options">Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -112,29 +116,110 @@
|
|||
</td>
|
||||
<td class="comicnumber">${weekly['ISSUE']}</td>
|
||||
%if weekly['AUTOWANT']:
|
||||
<td class="status">Auto-Want
|
||||
<td class="status">Auto-Want</td>
|
||||
%else:
|
||||
<td class="status">${weekly['STATUS']}
|
||||
%if weekly['HAVEIT'] == 'OneOff':
|
||||
%if weekly['STATUS'] == 'Snatched' or weekly['STATUS'] == 'Downloaded':
|
||||
<a href="#" onclick="doAjaxCall('queueit?ComicName=${weekly['COMIC'] | u}&ComicID=${weekly['COMICID']}&IssueID=${weekly['ISSUEID']}&ComicIssue=${weekly['ISSUE']}&mode=pullwant&Publisher=${weekly['PUBLISHER']}&pullinfo=${weekinfo['midweek']}&pullweek=${weekinfo['weeknumber']}&pullyear=${weekinfo['year']}',$(this),'table')" data-success="Successfully submitted search request for ${weekly['COMIC']} #${weekly['ISSUE']}" title="Snatch issue again as a One-Off"><span class="ui-icon ui-icon-plus"></span>Retry</a>
|
||||
<td class="status">${weekly['STATUS']}</td>
|
||||
%endif
|
||||
<td class="options">
|
||||
%if weekly['HAVEIT'] == 'OneOff':
|
||||
%if weekly['STATUS'] == 'Snatched' or weekly['STATUS'] == 'Downloaded':
|
||||
<a href="#" onclick="doAjaxCall('queueit?ComicName=${weekly['COMIC'] | u}&ComicID=${weekly['COMICID']}&IssueID=${weekly['ISSUEID']}&ComicIssue=${weekly['ISSUE']}&mode=pullwant&Publisher=${weekly['PUBLISHER']}&pullinfo=${weekinfo['midweek']}&pullweek=${weekinfo['weeknumber']}&pullyear=${weekinfo['year']}',$(this),'table')" data-success="Successfully submitted search request for ${weekly['COMIC']} #${weekly['ISSUE']}" title="Snatch issue again as a One-Off">
|
||||
%if mylar.CONFIG.SHOW_ICONS:
|
||||
<img style="margin: 0px 5px" src="interfaces/default/images/retry.png" height="25" width="25" class="highqual" />
|
||||
%else:
|
||||
<span class="ui-icon ui-icon-plus"></span>Retry
|
||||
%endif
|
||||
%if weekly['HASH'] is not None:
|
||||
<a href="#" onclick="doAjaxCall('torrentit?torrent_hash=${weekly['HASH']}&download=True',$(this),'table')" title="Auto-Snatch torrent file"><span class="ui-icon ui-icon-plus"></span>auto-snatch</a>
|
||||
</a>
|
||||
%endif
|
||||
%if weekly['HASH'] is not None and mylar.CONFIG.AUTO_SNATCH is True:
|
||||
<a href="#" onclick="doAjaxCall('torrentit?torrent_hash=${weekly['HASH']}&download=True',$(this),'table')" title="Auto-Snatch torrent file">
|
||||
%if mylar.CONFIG.SHOW_ICONS:
|
||||
<img style="margin: 0px 5px" src="interfaces/default/images/snatch.png" height="25" width="25" class="highqual" />
|
||||
%else:
|
||||
<span class="ui-icon ui-icon-plus"></span>Auto-Snatch
|
||||
%endif
|
||||
%elif weekly['STATUS'] == 'Skipped':
|
||||
</a>
|
||||
%endif
|
||||
%elif any([weekly['STATUS'] == 'Skipped', weekly['STATUS'] == 'Wanted']):
|
||||
%if weekly['STATUS'] == 'Skipped':
|
||||
%if weekly['COMICID'] != '' and weekly['COMICID'] is not None:
|
||||
<a href="#" title="auto-add by ID available for this series" onclick="doAjaxCall('addbyid?comicid=${weekly['COMICID']}&calledby=True',$(this),'table')" data-success="${weekly['COMIC']} is now being added to your wachlist."><span class="ui-icon ui-icon-plus"></span>Add</a>
|
||||
<a href="#" title="auto-add to Watchlist directly by ID available for this series" onclick="doAjaxCall('addbyid?comicid=${weekly['COMICID']}&calledby=True',$(this),'table')" data-success="${weekly['COMIC']} is now being added to your wachlist.">
|
||||
%if mylar.CONFIG.SHOW_ICONS:
|
||||
<img style="margin: 0px 5px" src="interfaces/default/images/add.png" height="25" width="25" class="highqual" />
|
||||
%else:
|
||||
<span class="ui-icon ui-icon-plus"></span>Add
|
||||
%endif
|
||||
</a>
|
||||
%else:
|
||||
%if weekly['ISSUE'] == '1' or weekly['ISSUE'] == '0':
|
||||
<a href="#" title="Watch for this series and auto-add when available" onclick="doAjaxCall('add2futurewatchlist?ComicName=${weekly['COMIC'] |u}&Issue=${weekly['ISSUE']}&Publisher=${weekly['PUBLISHER']}&ShipDate=${weekinfo['midweek']}&weeknumber=${weekinfo['weeknumber']}&year=${weekinfo['year']}',$(this),'table')" data-success="${weekly['COMIC']} is now on auto-watch/add."><span class="ui-icon ui-icon-plus"></span>Watch</a>
|
||||
%endif
|
||||
<a href="searchit?name=${weekly['COMIC'] | u}&issue=${weekly['ISSUE']}&mode=pullseries" title="Search for this series to add to your watchlist"><span class="ui-icon ui-icon-plus"></span>Search</a>
|
||||
<a href="#" title="Watch for this series and auto-add to Watchlist when available" onclick="doAjaxCall('add2futurewatchlist?ComicName=${weekly['COMIC'] |u}&Issue=${weekly['ISSUE']}&Publisher=${weekly['PUBLISHER']}&ShipDate=${weekinfo['midweek']}&weeknumber=${weekinfo['weeknumber']}&year=${weekinfo['year']}',$(this),'table')" data-success="${weekly['COMIC']} is now on auto-watch/add.">
|
||||
%if mylar.CONFIG.SHOW_ICONS:
|
||||
<img style="margin: 0px 5px" src="interfaces/default/images/watch.png" height="25" width="25" class="highqual" />
|
||||
%else:
|
||||
<span class="ui-icon ui-icon-plus"></span>Watch
|
||||
%endif
|
||||
</a>
|
||||
%endif
|
||||
<a href="searchit?name=${weekly['COMIC'] | u}&issue=${weekly['ISSUE']}&mode=pullseries" title="Search for this series to add to your watchlist">
|
||||
%if mylar.CONFIG.SHOW_ICONS:
|
||||
<img style="margin: 0px 5px" src="interfaces/default/images/search_add.png" height="25" width="25" class="highqual" />
|
||||
%else:
|
||||
<span class="ui-icon ui-icon-plus"></span>Search
|
||||
%endif
|
||||
</a>
|
||||
%endif
|
||||
<a href="#" onclick="doAjaxCall('queueit?ComicName=${weekly['COMIC'] | u}&ComicID=${weekly['COMICID']}&IssueID=${weekly['ISSUEID']}&ComicIssue=${weekly['ISSUE']}&mode=pullwant&Publisher=${weekly['PUBLISHER']}&pullinfo=${weekinfo['midweek']}&pullweek=${weekinfo['weeknumber']}&pullyear=${weekinfo['year']}',$(this),'table')" data-success="Successfully submitted search request for ${weekly['COMIC']} #${weekly['ISSUE']}" title="Just grab it"><span class="ui-icon ui-icon-plus"></span>One-Off</a>
|
||||
%elif weekly['HASH'] is not None:
|
||||
<a href="#" onclick="doAjaxCall('torrentit?torrent_hash=${weekly['HASH']}&download=True',$(this),'table')" title="Auto-Snatch torrent file"><span class="ui-icon ui-icon-plus"></span>auto-snatch</a>
|
||||
<% dl = True %>
|
||||
%else:
|
||||
<% dl = False %>
|
||||
%endif
|
||||
%if weekly['HAVEIT'] == 'No' and weekly['STATUS'] == 'Skipped':
|
||||
<a href="#" onclick="doAjaxCall('queueit?ComicName=${weekly['COMIC'] | u}&ComicID=${weekly['COMICID']}&IssueID=${weekly['ISSUEID']}&ComicIssue=${weekly['ISSUE']}&mode=pullwant&Publisher=${weekly['PUBLISHER']}&pullinfo=${weekinfo['midweek']}&pullweek=${weekinfo['weeknumber']}&pullyear=${weekinfo['year']}',$(this),'table')" data-success="Successfully submitted search request for ${weekly['COMIC']} #${weekly['ISSUE']}" title="One off download">
|
||||
%if mylar.CONFIG.SHOW_ICONS:
|
||||
<img style="margin: 0px 5px" src="interfaces/default/images/search.png" height="25" width="25" class="highqual" />
|
||||
%else:
|
||||
<span class="ui-icon ui-icon-plus"></span>One-Off
|
||||
%endif
|
||||
</a>
|
||||
%endif
|
||||
<!--
|
||||
<a class="menu_link_edit" id="choose_specific_download" title="Choose Specific Download" href="javascript:void(0)" onclick="getAvailableDownloads('${weekly['ISSUEID']}', '${weekly['COMIC']}', '${weekly['COMICID']}', '${weekly['ISSUE']}', '${weekly['VOLUME']}', 'pullwant', '${weekly['PUBLISHER']}', '${weekinfo['midweek']}', '${weekinfo['weeknumber']}', '${weekinfo['year']}', '${dl}')">
|
||||
%if mylar.CONFIG.SHOW_ICONS:
|
||||
<img style="margin: 0px 5px" src="interfaces/default/images/magnifier.png" height="25" width="25" class="highqual" />
|
||||
%else:
|
||||
<span class="ui-icon ui-icon-plus"></span>Choose
|
||||
%endif
|
||||
</a>
|
||||
<div id="choose_specific_download_dialog" title="Search & Choose a specific download for this issue" style="display:none" class="configtable">
|
||||
<table class="display" id="downloads_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="title">Title</th>
|
||||
<th id="provider">Provider</th>
|
||||
<th id="size">Size</th>
|
||||
<th id="kind">Kind</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="downloads_table_body" value="Now searching....this might take up to 90 seconds.">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
-->
|
||||
%elif weekly['HASH'] is not None and mylar.CONFIG.AUTO_SNATCH is True:
|
||||
<a href="#" onclick="doAjaxCall('torrentit?torrent_hash=${weekly['HASH']}&download=True',$(this),'table')" title="Auto-Snatch torrent file">
|
||||
%if mylar.CONFIG.SHOW_ICONS:
|
||||
<img style="margin: 0px 5px" src="interfaces/default/images/snatch.png" height="25" width="25" class="highqual" />
|
||||
%else:
|
||||
<span class="ui-icon ui-icon-plus"></span>Auto-Snatch
|
||||
%endif
|
||||
</a>
|
||||
%elif weekly['STATUS'] == 'Snatched':
|
||||
<a href="#" onclick="doAjaxCall('queueit?ComicName=${weekly['COMIC'] | u}&ComicID=${weekly['COMICID']}&IssueID=${weekly['ISSUEID']}&ComicIssue=${weekly['ISSUE']}&mode=pullwant&Publisher=${weekly['PUBLISHER']}&pullinfo=${weekinfo['midweek']}&pullweek=${weekinfo['weeknumber']}&pullyear=${weekinfo['year']}',$(this),'table')" data-success="Successfully submitted search request for ${weekly['COMIC']} #${weekly['ISSUE']}" title="Snatch issue again">
|
||||
%if mylar.CONFIG.SHOW_ICONS:
|
||||
<img style="margin: 0px 5px" src="interfaces/default/images/retry.png" height="25" width="25" class="highqual" />
|
||||
%else:
|
||||
<span class="ui-icon ui-icon-plus"></span>Retry
|
||||
%endif
|
||||
</a>
|
||||
%endif
|
||||
</td>
|
||||
%endif
|
||||
|
@ -166,6 +251,74 @@
|
|||
</script>
|
||||
|
||||
<script>
|
||||
function getAvailableDownloads(issueid, comicname, comicid, issue, comicvolume, mode, publisher, pullinfo, pullweek, pullyear, dl) {
|
||||
ShowSpinner();
|
||||
$.getJSON("choose_specific_download", {issueid: issueid, comicname: comicname, comicid: comicid, issue: issue, comicvolume: comicvolume, mode: mode, publisher: publisher, pullinfo: pullinfo, pullweek: pullweek, pullyear: pullyear, action: dl}, function(data) {
|
||||
loader.remove();
|
||||
feedback.fadeOut();
|
||||
search_results = data;
|
||||
if(search_results !=null){
|
||||
for( var i = 0, len = data.length; i < len; i++ ) {
|
||||
$('#downloads_table_body').append('<tr><td id="title"><a href="javascript:void(0)" onclick="downloadSpecificRelease('+i+')">'+data[i].nzbtitle+'</a></td><td id="provider">'+data[i].provider+'</td><td id="size">'+data[i].size+'</td><td id="kind">'+data[i].kind+'</td></tr>');
|
||||
}
|
||||
$('#downloads_table').dataTable({
|
||||
"aoColumns": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"aaSorting": [[ 1, 'desc']],
|
||||
"olanguage": {
|
||||
"emptyTable": "No search results found"},
|
||||
"bFilter": false,
|
||||
"bInfo": false,
|
||||
"bPaginate": false,
|
||||
"bDestroy": true
|
||||
});
|
||||
} else{
|
||||
$('#downloads_table_body').append('<tr><td style="text-align:center;" colspan="3">No results returned</td></tr>');
|
||||
}
|
||||
$("#choose_specific_download_dialog").dialog({
|
||||
width: "70%",
|
||||
maxHeight: 500
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function downloadSpecificRelease(i){
|
||||
|
||||
name = search_results[i].nzbtitle
|
||||
prov = search_results[i].tmpprov
|
||||
nzbid = search_results[i].nzbid
|
||||
ShowSpinner();
|
||||
$.getJSON("download_specific_release", {nzbid: nzbid, provider: prov, name: name}, function(data) {
|
||||
loader.remove();
|
||||
feedback.fadeOut();
|
||||
refreshSubmenu();
|
||||
$("#choose_specific_download_dialog").dialog("close");
|
||||
});
|
||||
}
|
||||
|
||||
function ShowSpinner() {
|
||||
feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
if ( update.is(":visible") ) {
|
||||
var height = update.height() + 35;
|
||||
feedback.css("bottom",height + "px");
|
||||
} else {
|
||||
feedback.removeAttr("style");
|
||||
}
|
||||
loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
||||
feedback.prepend(loader);
|
||||
feedback.fadeIn();
|
||||
}
|
||||
|
||||
var loadingMessage = false;
|
||||
var spinner_active = false;
|
||||
var loadingtext_active = false;
|
||||
var refreshInterval;
|
||||
|
||||
function initThisPage() {
|
||||
$("#weekfolder").click(function(){
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
## This is an example config file for use with running as a daemon on ubuntu.
|
||||
## ( you can just make a blank file with the required lines as well )
|
||||
##
|
||||
## Edit & Copy this file to /etc/default/mylar
|
||||
## Otherwise default mylar options are used (all default options are stated below)
|
||||
##
|
||||
### ADD THE REQUIRED OPTIONS ON INDIVIDUAL LINES BELOW THE 'END OF' ..(ie. MYLAR_USER=evil)
|
||||
###
|
||||
## .:[CONFIG OPTIONS]:.
|
||||
##
|
||||
### MYLAR_USER= #$RUN_AS, username to run mylar under, the default is mylar
|
||||
### MYLAR_HOME= #$APP_PATH, the location of mylar.py, the default is /opt/mylar
|
||||
### MYLAR_DATA= #$DATA_DIR, the location of mylar.db, cache, logs, the default is /opt/mylar
|
||||
### MYLAR_PIDFILE #$PIDFILE, the location of the pidfile, the default is /var/run/mylar/mylar.pid
|
||||
### PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
|
||||
### MYLAR_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for mylar, i.e. " --config=/home/mylar/config.ini"
|
||||
### SSD_OPTS= #$EXTRA_SSD_OPTS, extra start-stop-daemon option like " --group=users"
|
||||
### MYLAR_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
|
||||
##
|
||||
## END OF..
|
||||
##########################################################################################################3
|
|
@ -0,0 +1,156 @@
|
|||
# Script name
|
||||
NAME=mylar
|
||||
|
||||
# App name
|
||||
DESC=mylar
|
||||
|
||||
SETTINGS_LOADED=FALSE
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
# Source mylar configuration
|
||||
if [ -f /etc/default/mylar ]; then
|
||||
SETTINGS=/etc/default/mylar
|
||||
else
|
||||
log_warning_msg "/etc/default/mylar not found using default settings.";
|
||||
fi
|
||||
|
||||
check_retval() {
|
||||
if [ $? -eq 0 ]; then
|
||||
log_end_msg 0
|
||||
return 0
|
||||
else
|
||||
log_end_msg 1
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
load_settings() {
|
||||
if [ $SETTINGS_LOADED != "TRUE" ]; then
|
||||
. $SETTINGS
|
||||
|
||||
## The defaults
|
||||
# Run as username
|
||||
RUN_AS=${MYLAR_USER-mylar}
|
||||
|
||||
# Path to app MYLAR_HOME=path_to_app_mylar.py
|
||||
APP_PATH=${MYLAR_HOME-/opt/mylar}
|
||||
|
||||
# Data directory where mylar.db, cache and logs are stored
|
||||
DATA_DIR=${MYLAR_DATA-/opt/mylar}
|
||||
|
||||
# Path to store PID file
|
||||
PID_FILE=${MYLAR_PIDFILE-/var/run/mylar/mylar.pid}
|
||||
|
||||
# Path to python bin
|
||||
DAEMON=${PYTHON_BIN-/usr/bin/python}
|
||||
|
||||
# Extra daemon option like: MYLAR_OPTS=" --config=/home/mylar/config.ini"
|
||||
EXTRA_DAEMON_OPTS=${MYLAR_OPTS-}
|
||||
|
||||
# Extra start-stop-daemon option like START_OPTS=" --group=users"
|
||||
EXTRA_SSD_OPTS=${SSD_OPTS-}
|
||||
|
||||
# Hardcoded port to run on, overrides config.ini settings
|
||||
[ -n "$MYLAR_PORT" ] && {
|
||||
PORT_OPTS=" --port=${MYLAR_PORT} "
|
||||
}
|
||||
|
||||
DAEMON_OPTS=" Mylar.py --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS} ${EXTRA_DAEMON_OPTS}"
|
||||
|
||||
SETTINGS_LOADED=TRUE
|
||||
fi
|
||||
|
||||
[ -x $DAEMON ] || {
|
||||
log_warning_msg "$DESC: Can't execute daemon, aborting. See $DAEMON";
|
||||
return 1;}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
load_settings || exit 0
|
||||
|
||||
is_running () {
|
||||
# returns 1 when running, else 0.
|
||||
if [ -e $PID_FILE ]; then
|
||||
PID=`cat $PID_FILE`
|
||||
|
||||
RET=$?
|
||||
[ $RET -gt 1 ] && exit 1 || return $RET
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
handle_pid () {
|
||||
PID_PATH=`dirname $PID_FILE`
|
||||
[ -d $PID_PATH ] || mkdir -p $PID_PATH && chown -R $RUN_AS $PID_PATH > /dev/null || {
|
||||
log_warning_msg "$DESC: Could not create $PID_FILE, See $SETTINGS, aborting.";
|
||||
return 1;}
|
||||
|
||||
if [ -e $PID_FILE ]; then
|
||||
PID=`cat $PID_FILE`
|
||||
if ! kill -0 $PID > /dev/null 2>&1; then
|
||||
log_warning_msg "Removing stale $PID_FILE"
|
||||
rm $PID_FILE
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
handle_datadir () {
|
||||
[ -d $DATA_DIR ] || mkdir -p $DATA_DIR && chown -R $RUN_AS $DATA_DIR > /dev/null || {
|
||||
log_warning_msg "$DESC: Could not create $DATA_DIR, See $SETTINGS, aborting.";
|
||||
return 1;}
|
||||
}
|
||||
|
||||
handle_updates () {
|
||||
chown -R $RUN_AS $APP_PATH > /dev/null || {
|
||||
log_warning_msg "$DESC: $APP_PATH not writable by $RUN_AS for web-updates";
|
||||
return 0; }
|
||||
}
|
||||
|
||||
start_mylar () {
|
||||
handle_pid
|
||||
handle_datadir
|
||||
handle_updates
|
||||
if ! is_running; then
|
||||
log_daemon_msg "Starting $DESC"
|
||||
start-stop-daemon -o -d $APP_PATH -c $RUN_AS --start $EXTRA_SSD_OPTS --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
|
||||
check_retval
|
||||
else
|
||||
log_success_msg "$DESC: already running (pid $PID)"
|
||||
fi
|
||||
}
|
||||
|
||||
stop_mylar () {
|
||||
if is_running; then
|
||||
log_daemon_msg "Stopping $DESC"
|
||||
start-stop-daemon -o --stop --pidfile $PID_FILE --retry 15
|
||||
check_retval
|
||||
else
|
||||
log_success_msg "$DESC: not running"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start_mylar
|
||||
;;
|
||||
stop)
|
||||
stop_mylar
|
||||
;;
|
||||
restart|force-reload)
|
||||
stop_mylar
|
||||
start_mylar
|
||||
;;
|
||||
status)
|
||||
status_of_proc -p "$PID_FILE" "$DAEMON" "$DESC"
|
||||
;;
|
||||
*)
|
||||
N=/etc/init.d/$NAME
|
||||
echo "Usage: $N {start|stop|restart|force-reload|status}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
|
@ -0,0 +1,20 @@
|
|||
# Mylar.service to run as $USER with config in $DATA_DIR
|
||||
|
||||
[Unit]
|
||||
SourcePath=/etc/init.d/mylar
|
||||
Description=starts instance of Mylar
|
||||
After=all.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
Restart=no
|
||||
TimeoutSec=5min
|
||||
IgnoreSIGPIPE=no
|
||||
KillMode=process
|
||||
GuessMainPID=no
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/etc/init.d/mylar start
|
||||
ExecStop=/etc/init.d/mylar stop
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -0,0 +1,11 @@
|
|||
Instructions on setting up mylar as a systemd serivce that will run on startup/via systemctl commands...
|
||||
|
||||
1 - copy the mylar.service to /lib/systemd/system/mylar.service
|
||||
2 - create a symbolic link to it: ln -s /lib/systemd/system/mylar.service /etc/systemd/system/mylar.service
|
||||
3 - copy mylar.default to /etc/default/mylar (make sure it's renamed from mylar.default to just mylar)
|
||||
4 - copy mylar.nitd to /etc/init.d/mylar (rename it to just mylar) and then 'sudo chmod +x /etc/init.d/mylar'
|
||||
5 - edit the /etc/default/mylar file to your defaults (make sure to set MYLAR_USER & MYLAR_HOME as they're required)
|
||||
6 - make systemd aware of new services: sudo sytemctl daemon-reload
|
||||
7 - sudo systemctl enable mylar
|
||||
8 - sudo systemctl start mylar
|
||||
9 - to check to see if running/status - sudo sytemctl status mylar
|
|
@ -1,25 +1,25 @@
|
|||
Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of the CherryPy Team nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of the CherryPy Team nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
|
@ -53,14 +53,13 @@ with customized or extended components. The core API's are:
|
|||
* Server API
|
||||
* WSGI API
|
||||
|
||||
These API's are described in the CherryPy specification:
|
||||
http://www.cherrypy.org/wiki/CherryPySpec
|
||||
These API's are described in the `CherryPy specification <https://bitbucket.org/cherrypy/cherrypy/wiki/CherryPySpec>`_.
|
||||
"""
|
||||
|
||||
__version__ = "3.2.0"
|
||||
__version__ = "3.6.0"
|
||||
|
||||
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
|
||||
from cherrypy._cpcompat import basestring, unicodestr
|
||||
from cherrypy._cpcompat import basestring, unicodestr, set
|
||||
|
||||
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
|
||||
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
|
||||
|
@ -89,22 +88,27 @@ except ImportError:
|
|||
engine = process.bus
|
||||
|
||||
|
||||
# Timeout monitor
|
||||
# Timeout monitor. We add two channels to the engine
|
||||
# to which cherrypy.Application will publish.
|
||||
engine.listeners['before_request'] = set()
|
||||
engine.listeners['after_request'] = set()
|
||||
|
||||
|
||||
class _TimeoutMonitor(process.plugins.Monitor):
|
||||
|
||||
|
||||
def __init__(self, bus):
|
||||
self.servings = []
|
||||
process.plugins.Monitor.__init__(self, bus, self.run)
|
||||
|
||||
def acquire(self):
|
||||
|
||||
def before_request(self):
|
||||
self.servings.append((serving.request, serving.response))
|
||||
|
||||
def release(self):
|
||||
|
||||
def after_request(self):
|
||||
try:
|
||||
self.servings.remove((serving.request, serving.response))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def run(self):
|
||||
"""Check timeout on all responses. (Internal)"""
|
||||
for req, resp in self.servings:
|
||||
|
@ -121,6 +125,24 @@ engine.thread_manager.subscribe()
|
|||
engine.signal_handler = process.plugins.SignalHandler(engine)
|
||||
|
||||
|
||||
class _HandleSignalsPlugin(object):
|
||||
|
||||
"""Handle signals from other processes based on the configured
|
||||
platform handlers above."""
|
||||
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def subscribe(self):
|
||||
"""Add the handlers based on the platform"""
|
||||
if hasattr(self.bus, "signal_handler"):
|
||||
self.bus.signal_handler.subscribe()
|
||||
if hasattr(self.bus, "console_control_handler"):
|
||||
self.bus.console_control_handler.subscribe()
|
||||
|
||||
engine.signals = _HandleSignalsPlugin(engine)
|
||||
|
||||
|
||||
from cherrypy import _cpserver
|
||||
server = _cpserver.Server()
|
||||
server.subscribe()
|
||||
|
@ -128,7 +150,7 @@ server.subscribe()
|
|||
|
||||
def quickstart(root=None, script_name="", config=None):
|
||||
"""Mount the given root, start the builtin server (and engine), then block.
|
||||
|
||||
|
||||
root: an instance of a "controller class" (a collection of page handler
|
||||
methods) which represents the root of the application.
|
||||
script_name: a string containing the "mount point" of the application.
|
||||
|
@ -136,7 +158,7 @@ def quickstart(root=None, script_name="", config=None):
|
|||
at which to mount the given root. For example, if root.index() will
|
||||
handle requests to "http://www.example.com:8080/dept/app1/", then
|
||||
the script_name argument would be "/dept/app1".
|
||||
|
||||
|
||||
It MUST NOT end in a slash. If the script_name refers to the root
|
||||
of the URI, it MUST be an empty string (not "/").
|
||||
config: a file or dict containing application config. If this contains
|
||||
|
@ -145,23 +167,21 @@ def quickstart(root=None, script_name="", config=None):
|
|||
"""
|
||||
if config:
|
||||
_global_conf_alias.update(config)
|
||||
|
||||
|
||||
tree.mount(root, script_name, config)
|
||||
|
||||
if hasattr(engine, "signal_handler"):
|
||||
engine.signal_handler.subscribe()
|
||||
if hasattr(engine, "console_control_handler"):
|
||||
engine.console_control_handler.subscribe()
|
||||
|
||||
|
||||
engine.signals.subscribe()
|
||||
engine.start()
|
||||
engine.block()
|
||||
|
||||
|
||||
from cherrypy._cpcompat import threadlocal as _local
|
||||
|
||||
|
||||
class _Serving(_local):
|
||||
|
||||
"""An interface for registering request and response objects.
|
||||
|
||||
|
||||
Rather than have a separate "thread local" object for the request and
|
||||
the response, this class works as a single threadlocal container for
|
||||
both objects (and any others which developers wish to define). In this
|
||||
|
@ -169,22 +189,22 @@ class _Serving(_local):
|
|||
conversation, yet still refer to them as module-level globals in a
|
||||
thread-safe way.
|
||||
"""
|
||||
|
||||
|
||||
request = _cprequest.Request(_httputil.Host("127.0.0.1", 80),
|
||||
_httputil.Host("127.0.0.1", 1111))
|
||||
"""
|
||||
The request object for the current thread. In the main thread,
|
||||
and any threads which are not receiving HTTP requests, this is None."""
|
||||
|
||||
|
||||
response = _cprequest.Response()
|
||||
"""
|
||||
The response object for the current thread. In the main thread,
|
||||
and any threads which are not receiving HTTP requests, this is None."""
|
||||
|
||||
|
||||
def load(self, request, response):
|
||||
self.request = request
|
||||
self.response = response
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""Remove all attributes of self."""
|
||||
self.__dict__.clear()
|
||||
|
@ -193,54 +213,54 @@ serving = _Serving()
|
|||
|
||||
|
||||
class _ThreadLocalProxy(object):
|
||||
|
||||
|
||||
__slots__ = ['__attrname__', '__dict__']
|
||||
|
||||
|
||||
def __init__(self, attrname):
|
||||
self.__attrname__ = attrname
|
||||
|
||||
|
||||
def __getattr__(self, name):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return getattr(child, name)
|
||||
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in ("__attrname__", ):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
child = getattr(serving, self.__attrname__)
|
||||
setattr(child, name, value)
|
||||
|
||||
|
||||
def __delattr__(self, name):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
delattr(child, name)
|
||||
|
||||
|
||||
def _get_dict(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
d = child.__class__.__dict__.copy()
|
||||
d.update(child.__dict__)
|
||||
return d
|
||||
__dict__ = property(_get_dict)
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return child[key]
|
||||
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
child[key] = value
|
||||
|
||||
|
||||
def __delitem__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
del child[key]
|
||||
|
||||
|
||||
def __contains__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return key in child
|
||||
|
||||
|
||||
def __len__(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return len(child)
|
||||
|
||||
|
||||
def __nonzero__(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return bool(child)
|
||||
|
@ -254,7 +274,10 @@ request = _ThreadLocalProxy('request')
|
|||
response = _ThreadLocalProxy('response')
|
||||
|
||||
# Create thread_data object as a thread-specific all-purpose storage
|
||||
|
||||
|
||||
class _ThreadData(_local):
|
||||
|
||||
"""A container for thread-specific data."""
|
||||
thread_data = _ThreadData()
|
||||
|
||||
|
@ -279,27 +302,32 @@ except ImportError:
|
|||
|
||||
from cherrypy import _cplogging
|
||||
|
||||
|
||||
class _GlobalLogManager(_cplogging.LogManager):
|
||||
|
||||
"""A site-wide LogManager; routes to app.log or global log as appropriate.
|
||||
|
||||
|
||||
This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
|
||||
cherrypy.log() and cherrypy.log.access(). If either
|
||||
function is called during a request, the message will be sent to the
|
||||
logger for the current Application. If they are called outside of a
|
||||
request, the message will be sent to the site-wide logger.
|
||||
"""
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Log the given message to the app.log or global log as appropriate."""
|
||||
# Do NOT use try/except here. See http://www.cherrypy.org/ticket/945
|
||||
"""Log the given message to the app.log or global log as appropriate.
|
||||
"""
|
||||
# Do NOT use try/except here. See
|
||||
# https://bitbucket.org/cherrypy/cherrypy/issue/945
|
||||
if hasattr(request, 'app') and hasattr(request.app, 'log'):
|
||||
log = request.app.log
|
||||
else:
|
||||
log = self
|
||||
return log.error(*args, **kwargs)
|
||||
|
||||
|
||||
def access(self):
|
||||
"""Log an access message to the app.log or global log as appropriate."""
|
||||
"""Log an access message to the app.log or global log as appropriate.
|
||||
"""
|
||||
try:
|
||||
return request.app.log.access()
|
||||
except AttributeError:
|
||||
|
@ -313,6 +341,7 @@ log.error_file = ''
|
|||
# Using an access file makes CP about 10% slower. Leave off by default.
|
||||
log.access_file = ''
|
||||
|
||||
|
||||
def _buslog(msg, level):
|
||||
log.error(msg, 'ENGINE', severity=level)
|
||||
engine.subscribe('log', _buslog)
|
||||
|
@ -331,8 +360,9 @@ def expose(func=None, alias=None):
|
|||
for a in alias:
|
||||
parents[a.replace(".", "_")] = func
|
||||
return func
|
||||
|
||||
import sys, types
|
||||
|
||||
import sys
|
||||
import types
|
||||
if isinstance(func, (types.FunctionType, types.MethodType)):
|
||||
if alias is None:
|
||||
# @expose
|
||||
|
@ -359,24 +389,25 @@ def expose(func=None, alias=None):
|
|||
alias = func
|
||||
return expose_
|
||||
|
||||
|
||||
def popargs(*args, **kwargs):
|
||||
"""A decorator for _cp_dispatch
|
||||
"""A decorator for _cp_dispatch
|
||||
(cherrypy.dispatch.Dispatcher.dispatch_method_name).
|
||||
|
||||
Optional keyword argument: handler=(Object or Function)
|
||||
|
||||
Provides a _cp_dispatch function that pops off path segments into
|
||||
|
||||
Provides a _cp_dispatch function that pops off path segments into
|
||||
cherrypy.request.params under the names specified. The dispatch
|
||||
is then forwarded on to the next vpath element.
|
||||
|
||||
|
||||
Note that any existing (and exposed) member function of the class that
|
||||
popargs is applied to will override that value of the argument. For
|
||||
instance, if you have a method named "list" on the class decorated with
|
||||
popargs, then accessing "/list" will call that function instead of popping
|
||||
it off as the requested parameter. This restriction applies to all
|
||||
it off as the requested parameter. This restriction applies to all
|
||||
_cp_dispatch functions. The only way around this restriction is to create
|
||||
a "blank class" whose only function is to provide _cp_dispatch.
|
||||
|
||||
|
||||
If there are path elements after the arguments, or more arguments
|
||||
are requested than are available in the vpath, then the 'handler'
|
||||
keyword argument specifies the next object to handle the parameterized
|
||||
|
@ -385,9 +416,9 @@ def popargs(*args, **kwargs):
|
|||
will be called with the args specified and the return value from that
|
||||
function used as the next object INSTEAD of adding the parameters to
|
||||
cherrypy.request.args.
|
||||
|
||||
|
||||
This decorator may be used in one of two ways:
|
||||
|
||||
|
||||
As a class decorator:
|
||||
@cherrypy.popargs('year', 'month', 'day')
|
||||
class Blog:
|
||||
|
@ -395,122 +426,123 @@ def popargs(*args, **kwargs):
|
|||
#Process the parameters here; any url like
|
||||
#/, /2009, /2009/12, or /2009/12/31
|
||||
#will fill in the appropriate parameters.
|
||||
|
||||
|
||||
def create(self):
|
||||
#This link will still be available at /create. Defined functions
|
||||
#take precedence over arguments.
|
||||
|
||||
|
||||
Or as a member of a class:
|
||||
class Blog:
|
||||
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
|
||||
#...
|
||||
|
||||
|
||||
The handler argument may be used to mix arguments with built in functions.
|
||||
For instance, the following setup allows different activities at the
|
||||
day, month, and year level:
|
||||
|
||||
|
||||
class DayHandler:
|
||||
def index(self, year, month, day):
|
||||
#Do something with this day; probably list entries
|
||||
|
||||
|
||||
def delete(self, year, month, day):
|
||||
#Delete all entries for this day
|
||||
|
||||
|
||||
@cherrypy.popargs('day', handler=DayHandler())
|
||||
class MonthHandler:
|
||||
def index(self, year, month):
|
||||
#Do something with this month; probably list entries
|
||||
|
||||
|
||||
def delete(self, year, month):
|
||||
#Delete all entries for this month
|
||||
|
||||
|
||||
@cherrypy.popargs('month', handler=MonthHandler())
|
||||
class YearHandler:
|
||||
def index(self, year):
|
||||
#Do something with this year
|
||||
|
||||
|
||||
#...
|
||||
|
||||
|
||||
@cherrypy.popargs('year', handler=YearHandler())
|
||||
class Root:
|
||||
def index(self):
|
||||
#...
|
||||
|
||||
|
||||
"""
|
||||
|
||||
#Since keyword arg comes after *args, we have to process it ourselves
|
||||
#for lower versions of python.
|
||||
# Since keyword arg comes after *args, we have to process it ourselves
|
||||
# for lower versions of python.
|
||||
|
||||
handler = None
|
||||
handler_call = False
|
||||
for k,v in kwargs.items():
|
||||
for k, v in kwargs.items():
|
||||
if k == 'handler':
|
||||
handler = v
|
||||
else:
|
||||
raise TypeError(
|
||||
"cherrypy.popargs() got an unexpected keyword argument '{0}'" \
|
||||
"cherrypy.popargs() got an unexpected keyword argument '{0}'"
|
||||
.format(k)
|
||||
)
|
||||
)
|
||||
|
||||
import inspect
|
||||
|
||||
if handler is not None \
|
||||
and (hasattr(handler, '__call__') or inspect.isclass(handler)):
|
||||
and (hasattr(handler, '__call__') or inspect.isclass(handler)):
|
||||
handler_call = True
|
||||
|
||||
|
||||
def decorated(cls_or_self=None, vpath=None):
|
||||
if inspect.isclass(cls_or_self):
|
||||
#cherrypy.popargs is a class decorator
|
||||
# cherrypy.popargs is a class decorator
|
||||
cls = cls_or_self
|
||||
setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated)
|
||||
return cls
|
||||
|
||||
#We're in the actual function
|
||||
|
||||
# We're in the actual function
|
||||
self = cls_or_self
|
||||
parms = {}
|
||||
for arg in args:
|
||||
if not vpath:
|
||||
break
|
||||
parms[arg] = vpath.pop(0)
|
||||
|
||||
|
||||
if handler is not None:
|
||||
if handler_call:
|
||||
return handler(**parms)
|
||||
else:
|
||||
request.params.update(parms)
|
||||
return handler
|
||||
|
||||
|
||||
request.params.update(parms)
|
||||
|
||||
#If we are the ultimate handler, then to prevent our _cp_dispatch
|
||||
#from being called again, we will resolve remaining elements through
|
||||
#getattr() directly.
|
||||
|
||||
# If we are the ultimate handler, then to prevent our _cp_dispatch
|
||||
# from being called again, we will resolve remaining elements through
|
||||
# getattr() directly.
|
||||
if vpath:
|
||||
return getattr(self, vpath.pop(0), None)
|
||||
else:
|
||||
return self
|
||||
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def url(path="", qs="", script_name=None, base=None, relative=None):
|
||||
"""Create an absolute URL for the given path.
|
||||
|
||||
|
||||
If 'path' starts with a slash ('/'), this will return
|
||||
(base + script_name + path + qs).
|
||||
If it does not start with a slash, this returns
|
||||
(base + script_name [+ request.path_info] + path + qs).
|
||||
|
||||
|
||||
If script_name is None, cherrypy.request will be used
|
||||
to find a script_name, if available.
|
||||
|
||||
|
||||
If base is None, cherrypy.request.base will be used (if available).
|
||||
Note that you can use cherrypy.tools.proxy to change this.
|
||||
|
||||
|
||||
Finally, note that this function can be used to obtain an absolute URL
|
||||
for the current request path (minus the querystring) by passing no args.
|
||||
If you call url(qs=cherrypy.request.query_string), you should get the
|
||||
original browser URL (assuming no internal redirections).
|
||||
|
||||
|
||||
If relative is None or not provided, request.app.relative_urls will
|
||||
be used (if available, else False). If False, the output will be an
|
||||
absolute URL (including the scheme, host, vhost, and script_name).
|
||||
|
@ -523,7 +555,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
|
|||
qs = _urlencode(qs)
|
||||
if qs:
|
||||
qs = '?' + qs
|
||||
|
||||
|
||||
if request.app:
|
||||
if not path.startswith("/"):
|
||||
# Append/remove trailing slash from path_info as needed
|
||||
|
@ -536,17 +568,17 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
|
|||
elif request.is_index is False:
|
||||
if pi.endswith('/') and pi != '/':
|
||||
pi = pi[:-1]
|
||||
|
||||
|
||||
if path == "":
|
||||
path = pi
|
||||
else:
|
||||
path = _urljoin(pi, path)
|
||||
|
||||
|
||||
if script_name is None:
|
||||
script_name = request.script_name
|
||||
if base is None:
|
||||
base = request.base
|
||||
|
||||
|
||||
newurl = base + script_name + path + qs
|
||||
else:
|
||||
# No request.app (we're being called outside a request).
|
||||
|
@ -555,10 +587,10 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
|
|||
# if you're using vhosts or tools.proxy.
|
||||
if base is None:
|
||||
base = server.base()
|
||||
|
||||
|
||||
path = (script_name or "") + path
|
||||
newurl = base + path + qs
|
||||
|
||||
|
||||
if './' in newurl:
|
||||
# Normalize the URL by removing ./ and ../
|
||||
atoms = []
|
||||
|
@ -570,12 +602,12 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
|
|||
else:
|
||||
atoms.append(atom)
|
||||
newurl = '/'.join(atoms)
|
||||
|
||||
|
||||
# At this point, we should have a fully-qualified absolute URL.
|
||||
|
||||
|
||||
if relative is None:
|
||||
relative = getattr(request.app, "relative_urls", False)
|
||||
|
||||
|
||||
# See http://www.ietf.org/rfc/rfc2396.txt
|
||||
if relative == 'server':
|
||||
# "A relative reference beginning with a single slash character is
|
||||
|
@ -585,7 +617,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
|
|||
elif relative:
|
||||
# "A relative reference that does not begin with a scheme name
|
||||
# or a slash character is termed a relative-path reference."
|
||||
old = url().split('/')[:-1]
|
||||
old = url(relative=False).split('/')[:-1]
|
||||
new = newurl.split('/')
|
||||
while old and new:
|
||||
a, b = old[0], new[0]
|
||||
|
@ -595,7 +627,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
|
|||
new.pop(0)
|
||||
new = (['..'] * len(old)) + new
|
||||
newurl = '/'.join(new)
|
||||
|
||||
|
||||
return newurl
|
||||
|
||||
|
||||
|
@ -609,7 +641,7 @@ config.defaults = {
|
|||
'tools.log_headers.on': True,
|
||||
'tools.trailing_slash.on': True,
|
||||
'tools.encode.on': True
|
||||
}
|
||||
}
|
||||
config.namespaces["log"] = lambda k, v: setattr(log, k, v)
|
||||
config.namespaces["checker"] = lambda k, v: setattr(checker, k, v)
|
||||
# Must reset to get our defaults applied.
|
||||
|
|
|
@ -6,26 +6,26 @@ from cherrypy._cpcompat import iteritems, copykeys, builtins
|
|||
|
||||
|
||||
class Checker(object):
|
||||
|
||||
"""A checker for CherryPy sites and their mounted applications.
|
||||
|
||||
|
||||
When this object is called at engine startup, it executes each
|
||||
of its own methods whose names start with ``check_``. If you wish
|
||||
to disable selected checks, simply add a line in your global
|
||||
config which sets the appropriate method to False::
|
||||
|
||||
|
||||
[global]
|
||||
checker.check_skipped_app_config = False
|
||||
|
||||
|
||||
You may also dynamically add or replace ``check_*`` methods in this way.
|
||||
"""
|
||||
|
||||
|
||||
on = True
|
||||
"""If True (the default), run all checks; if False, turn off all checks."""
|
||||
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._populate_known_types()
|
||||
|
||||
|
||||
def __call__(self):
|
||||
"""Run all check_* methods."""
|
||||
if self.on:
|
||||
|
@ -39,16 +39,17 @@ class Checker(object):
|
|||
method()
|
||||
finally:
|
||||
warnings.formatwarning = oldformatwarning
|
||||
|
||||
|
||||
def formatwarning(self, message, category, filename, lineno, line=None):
|
||||
"""Function to format a warning."""
|
||||
return "CherryPy Checker:\n%s\n\n" % message
|
||||
|
||||
|
||||
# This value should be set inside _cpconfig.
|
||||
global_config_contained_paths = False
|
||||
|
||||
|
||||
def check_app_config_entries_dont_start_with_script_name(self):
|
||||
"""Check for Application config with sections that repeat script_name."""
|
||||
"""Check for Application config with sections that repeat script_name.
|
||||
"""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
|
@ -61,30 +62,33 @@ class Checker(object):
|
|||
key_atoms = key.strip("/").split("/")
|
||||
if key_atoms[:len(sn_atoms)] == sn_atoms:
|
||||
warnings.warn(
|
||||
"The application mounted at %r has config " \
|
||||
"entries that start with its script name: %r" % (sn, key))
|
||||
|
||||
"The application mounted at %r has config "
|
||||
"entries that start with its script name: %r" % (sn,
|
||||
key))
|
||||
|
||||
def check_site_config_entries_in_app_config(self):
|
||||
"""Check for mounted Applications that have site-scoped config."""
|
||||
for sn, app in iteritems(cherrypy.tree.apps):
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
|
||||
|
||||
msg = []
|
||||
for section, entries in iteritems(app.config):
|
||||
if section.startswith('/'):
|
||||
for key, value in iteritems(entries):
|
||||
for n in ("engine.", "server.", "tree.", "checker."):
|
||||
if key.startswith(n):
|
||||
msg.append("[%s] %s = %s" % (section, key, value))
|
||||
msg.append("[%s] %s = %s" %
|
||||
(section, key, value))
|
||||
if msg:
|
||||
msg.insert(0,
|
||||
"The application mounted at %r contains the following "
|
||||
"config entries, which are only allowed in site-wide "
|
||||
"config. Move them to a [global] section and pass them "
|
||||
"to cherrypy.config.update() instead of tree.mount()." % sn)
|
||||
"The application mounted at %r contains the "
|
||||
"following config entries, which are only allowed "
|
||||
"in site-wide config. Move them to a [global] "
|
||||
"section and pass them to cherrypy.config.update() "
|
||||
"instead of tree.mount()." % sn)
|
||||
warnings.warn(os.linesep.join(msg))
|
||||
|
||||
|
||||
def check_skipped_app_config(self):
|
||||
"""Check for mounted Applications that have no config."""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
|
@ -100,9 +104,11 @@ class Checker(object):
|
|||
"cherrypy.tree.mount(..., config=app_config)")
|
||||
warnings.warn(msg)
|
||||
return
|
||||
|
||||
|
||||
def check_app_config_brackets(self):
|
||||
"""Check for Application config with extraneous brackets in section names."""
|
||||
"""Check for Application config with extraneous brackets in section
|
||||
names.
|
||||
"""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
|
@ -111,11 +117,11 @@ class Checker(object):
|
|||
for key in app.config.keys():
|
||||
if key.startswith("[") or key.endswith("]"):
|
||||
warnings.warn(
|
||||
"The application mounted at %r has config " \
|
||||
"The application mounted at %r has config "
|
||||
"section names with extraneous brackets: %r. "
|
||||
"Config *files* need brackets; config *dicts* "
|
||||
"(e.g. passed to tree.mount) do not." % (sn, key))
|
||||
|
||||
|
||||
def check_static_paths(self):
|
||||
"""Check Application config for incorrect static paths."""
|
||||
# Use the dummy Request object in the main thread.
|
||||
|
@ -128,7 +134,7 @@ class Checker(object):
|
|||
# get_resource will populate request.config
|
||||
request.get_resource(section + "/dummy.html")
|
||||
conf = request.config.get
|
||||
|
||||
|
||||
if conf("tools.staticdir.on", False):
|
||||
msg = ""
|
||||
root = conf("tools.staticdir.root")
|
||||
|
@ -144,30 +150,32 @@ class Checker(object):
|
|||
"though a root is provided.")
|
||||
testdir = os.path.join(root, dir[1:])
|
||||
if os.path.exists(testdir):
|
||||
msg += ("\nIf you meant to serve the "
|
||||
"filesystem folder at %r, remove "
|
||||
"the leading slash from dir." % testdir)
|
||||
msg += (
|
||||
"\nIf you meant to serve the "
|
||||
"filesystem folder at %r, remove the "
|
||||
"leading slash from dir." % (testdir,))
|
||||
else:
|
||||
if not root:
|
||||
msg = "dir is a relative path and no root provided."
|
||||
msg = (
|
||||
"dir is a relative path and "
|
||||
"no root provided.")
|
||||
else:
|
||||
fulldir = os.path.join(root, dir)
|
||||
if not os.path.isabs(fulldir):
|
||||
msg = "%r is not an absolute path." % fulldir
|
||||
|
||||
msg = ("%r is not an absolute path." % (
|
||||
fulldir,))
|
||||
|
||||
if fulldir and not os.path.exists(fulldir):
|
||||
if msg:
|
||||
msg += "\n"
|
||||
msg += ("%r (root + dir) is not an existing "
|
||||
"filesystem path." % fulldir)
|
||||
|
||||
|
||||
if msg:
|
||||
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
|
||||
% (msg, section, root, dir))
|
||||
|
||||
|
||||
|
||||
# -------------------------- Compatibility -------------------------- #
|
||||
|
||||
obsolete = {
|
||||
'server.default_content_type': 'tools.response_headers.headers',
|
||||
'log_access_file': 'log.access_file',
|
||||
|
@ -180,10 +188,10 @@ class Checker(object):
|
|||
'throw_errors': 'request.throw_errors',
|
||||
'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
|
||||
'cherrypy.Application(Root())))'),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
deprecated = {}
|
||||
|
||||
|
||||
def _compat(self, config):
|
||||
"""Process config and warn on each obsolete or deprecated entry."""
|
||||
for section, conf in config.items():
|
||||
|
@ -204,7 +212,7 @@ class Checker(object):
|
|||
elif section in self.deprecated:
|
||||
warnings.warn("%r is deprecated. Use %r instead."
|
||||
% (section, self.deprecated[section]))
|
||||
|
||||
|
||||
def check_compatibility(self):
|
||||
"""Process config and warn on each obsolete or deprecated entry."""
|
||||
self._compat(cherrypy.config)
|
||||
|
@ -212,12 +220,10 @@ class Checker(object):
|
|||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._compat(app.config)
|
||||
|
||||
|
||||
|
||||
# ------------------------ Known Namespaces ------------------------ #
|
||||
|
||||
extra_config_namespaces = []
|
||||
|
||||
|
||||
def _known_ns(self, app):
|
||||
ns = ["wsgi"]
|
||||
ns.extend(copykeys(app.toolboxes))
|
||||
|
@ -225,7 +231,7 @@ class Checker(object):
|
|||
ns.extend(copykeys(app.request_class.namespaces))
|
||||
ns.extend(copykeys(cherrypy.config.namespaces))
|
||||
ns += self.extra_config_namespaces
|
||||
|
||||
|
||||
for section, conf in app.config.items():
|
||||
is_path_section = section.startswith("/")
|
||||
if is_path_section and isinstance(conf, dict):
|
||||
|
@ -235,22 +241,26 @@ class Checker(object):
|
|||
if atoms[0] not in ns:
|
||||
# Spit out a special warning if a known
|
||||
# namespace is preceded by "cherrypy."
|
||||
if (atoms[0] == "cherrypy" and atoms[1] in ns):
|
||||
msg = ("The config entry %r is invalid; "
|
||||
"try %r instead.\nsection: [%s]"
|
||||
% (k, ".".join(atoms[1:]), section))
|
||||
if atoms[0] == "cherrypy" and atoms[1] in ns:
|
||||
msg = (
|
||||
"The config entry %r is invalid; "
|
||||
"try %r instead.\nsection: [%s]"
|
||||
% (k, ".".join(atoms[1:]), section))
|
||||
else:
|
||||
msg = ("The config entry %r is invalid, because "
|
||||
"the %r config namespace is unknown.\n"
|
||||
"section: [%s]" % (k, atoms[0], section))
|
||||
msg = (
|
||||
"The config entry %r is invalid, "
|
||||
"because the %r config namespace "
|
||||
"is unknown.\n"
|
||||
"section: [%s]" % (k, atoms[0], section))
|
||||
warnings.warn(msg)
|
||||
elif atoms[0] == "tools":
|
||||
if atoms[1] not in dir(cherrypy.tools):
|
||||
msg = ("The config entry %r may be invalid, "
|
||||
"because the %r tool was not found.\n"
|
||||
"section: [%s]" % (k, atoms[1], section))
|
||||
msg = (
|
||||
"The config entry %r may be invalid, "
|
||||
"because the %r tool was not found.\n"
|
||||
"section: [%s]" % (k, atoms[1], section))
|
||||
warnings.warn(msg)
|
||||
|
||||
|
||||
def check_config_namespaces(self):
|
||||
"""Process config and warn on each unknown config namespace."""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
|
@ -258,17 +268,13 @@ class Checker(object):
|
|||
continue
|
||||
self._known_ns(app)
|
||||
|
||||
|
||||
|
||||
|
||||
# -------------------------- Config Types -------------------------- #
|
||||
|
||||
known_config_types = {}
|
||||
|
||||
|
||||
def _populate_known_types(self):
|
||||
b = [x for x in vars(builtins).values()
|
||||
if type(x) is type(str)]
|
||||
|
||||
|
||||
def traverse(obj, namespace):
|
||||
for name in dir(obj):
|
||||
# Hack for 3.2's warning about body_params
|
||||
|
@ -277,17 +283,17 @@ class Checker(object):
|
|||
vtype = type(getattr(obj, name, None))
|
||||
if vtype in b:
|
||||
self.known_config_types[namespace + "." + name] = vtype
|
||||
|
||||
|
||||
traverse(cherrypy.request, "request")
|
||||
traverse(cherrypy.response, "response")
|
||||
traverse(cherrypy.server, "server")
|
||||
traverse(cherrypy.engine, "engine")
|
||||
traverse(cherrypy.log, "log")
|
||||
|
||||
|
||||
def _known_types(self, config):
|
||||
msg = ("The config entry %r in section %r is of type %r, "
|
||||
"which does not match the expected type %r.")
|
||||
|
||||
|
||||
for section, conf in config.items():
|
||||
if isinstance(conf, dict):
|
||||
for k, v in conf.items():
|
||||
|
@ -305,7 +311,7 @@ class Checker(object):
|
|||
if expected_type and vtype != expected_type:
|
||||
warnings.warn(msg % (k, section, vtype.__name__,
|
||||
expected_type.__name__))
|
||||
|
||||
|
||||
def check_config_types(self):
|
||||
"""Assert that config values are of the same type as default values."""
|
||||
self._known_types(cherrypy.config)
|
||||
|
@ -313,15 +319,14 @@ class Checker(object):
|
|||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._known_types(app.config)
|
||||
|
||||
|
||||
|
||||
# -------------------- Specific config warnings -------------------- #
|
||||
|
||||
def check_localhost(self):
|
||||
"""Warn if any socket_host is 'localhost'. See #711."""
|
||||
for k, v in cherrypy.config.items():
|
||||
if k == 'server.socket_host' and v == 'localhost':
|
||||
warnings.warn("The use of 'localhost' as a socket host can "
|
||||
"cause problems on newer systems, since 'localhost' can "
|
||||
"map to either an IPv4 or an IPv6 address. You should "
|
||||
"use '127.0.0.1' or '[::1]' instead.")
|
||||
"cause problems on newer systems, since "
|
||||
"'localhost' can map to either an IPv4 or an "
|
||||
"IPv6 address. You should use '127.0.0.1' "
|
||||
"or '[::1]' instead.")
|
||||
|
|
|
@ -16,43 +16,86 @@ It also provides a 'base64_decode' function with native strings as input and
|
|||
output.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
py3k = True
|
||||
bytestr = bytes
|
||||
unicodestr = str
|
||||
nativestr = unicodestr
|
||||
basestring = (bytes, str)
|
||||
|
||||
def ntob(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a byte string in the given encoding."""
|
||||
"""Return the given native string as a byte string in the given
|
||||
encoding.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 3, the native string type is unicode
|
||||
return n.encode(encoding)
|
||||
|
||||
def ntou(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a unicode string with the given encoding."""
|
||||
"""Return the given native string as a unicode string with the given
|
||||
encoding.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 3, the native string type is unicode
|
||||
return n
|
||||
|
||||
def tonative(n, encoding='ISO-8859-1'):
|
||||
"""Return the given string as a native string in the given encoding."""
|
||||
# In Python 3, the native string type is unicode
|
||||
if isinstance(n, bytes):
|
||||
return n.decode(encoding)
|
||||
return n
|
||||
# type("")
|
||||
from io import StringIO
|
||||
# bytes:
|
||||
from io import BytesIO as BytesIO
|
||||
else:
|
||||
# Python 2
|
||||
py3k = False
|
||||
bytestr = str
|
||||
unicodestr = unicode
|
||||
nativestr = bytestr
|
||||
basestring = basestring
|
||||
|
||||
def ntob(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a byte string in the given encoding."""
|
||||
"""Return the given native string as a byte string in the given
|
||||
encoding.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 2, the native string type is bytes. Assume it's already
|
||||
# in the given encoding, which for ISO-8859-1 is almost always what
|
||||
# was intended.
|
||||
return n
|
||||
|
||||
def ntou(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a unicode string with the given encoding."""
|
||||
# In Python 2, the native string type is bytes. Assume it's already
|
||||
# in the given encoding, which for ISO-8859-1 is almost always what
|
||||
# was intended.
|
||||
"""Return the given native string as a unicode string with the given
|
||||
encoding.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 2, the native string type is bytes.
|
||||
# First, check for the special encoding 'escape'. The test suite uses
|
||||
# this to signal that it wants to pass a string with embedded \uXXXX
|
||||
# escapes, but without having to prefix it with u'' for Python 2,
|
||||
# but no prefix for Python 3.
|
||||
if encoding == 'escape':
|
||||
return unicode(
|
||||
re.sub(r'\\u([0-9a-zA-Z]{4})',
|
||||
lambda m: unichr(int(m.group(1), 16)),
|
||||
n.decode('ISO-8859-1')))
|
||||
# Assume it's already in the given encoding, which for ISO-8859-1
|
||||
# is almost always what was intended.
|
||||
return n.decode(encoding)
|
||||
|
||||
def tonative(n, encoding='ISO-8859-1'):
|
||||
"""Return the given string as a native string in the given encoding."""
|
||||
# In Python 2, the native string type is bytes.
|
||||
if isinstance(n, unicode):
|
||||
return n.encode(encoding)
|
||||
return n
|
||||
try:
|
||||
# type("")
|
||||
from cStringIO import StringIO
|
||||
|
@ -62,6 +105,11 @@ else:
|
|||
# bytes:
|
||||
BytesIO = StringIO
|
||||
|
||||
|
||||
def assert_native(n):
|
||||
if not isinstance(n, nativestr):
|
||||
raise TypeError("n must be a native str (got %s)" % type(n).__name__)
|
||||
|
||||
try:
|
||||
set = set
|
||||
except NameError:
|
||||
|
@ -76,6 +124,7 @@ except ImportError:
|
|||
# the legacy API of base64
|
||||
from base64 import decodestring as _base64_decodebytes
|
||||
|
||||
|
||||
def base64_decode(n, encoding='ISO-8859-1'):
|
||||
"""Return the native string base64-decoded (as a native string)."""
|
||||
if isinstance(n, unicodestr):
|
||||
|
@ -174,17 +223,32 @@ except ImportError:
|
|||
import __builtin__ as builtins
|
||||
|
||||
try:
|
||||
# Python 2. We have to do it in this order so Python 2 builds
|
||||
# Python 2. We try Python 2 first clients on Python 2
|
||||
# don't try to import the 'http' module from cherrypy.lib
|
||||
from Cookie import SimpleCookie, CookieError
|
||||
from httplib import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
|
||||
from httplib import BadStatusLine, HTTPConnection, IncompleteRead
|
||||
from httplib import NotConnected
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||
except ImportError:
|
||||
# Python 3
|
||||
from http.cookies import SimpleCookie, CookieError
|
||||
from http.client import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
|
||||
from http.client import BadStatusLine, HTTPConnection, IncompleteRead
|
||||
from http.client import NotConnected
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
|
||||
# Some platforms don't expose HTTPSConnection, so handle it separately
|
||||
if py3k:
|
||||
try:
|
||||
from http.client import HTTPSConnection
|
||||
except ImportError:
|
||||
# Some platforms which don't have SSL don't expose HTTPSConnection
|
||||
HTTPSConnection = None
|
||||
else:
|
||||
try:
|
||||
from httplib import HTTPSConnection
|
||||
except ImportError:
|
||||
HTTPSConnection = None
|
||||
|
||||
try:
|
||||
# Python 2
|
||||
xrange = xrange
|
||||
|
@ -197,16 +261,19 @@ if hasattr(threading.Thread, "daemon"):
|
|||
# Python 2.6+
|
||||
def get_daemon(t):
|
||||
return t.daemon
|
||||
|
||||
def set_daemon(t, val):
|
||||
t.daemon = val
|
||||
else:
|
||||
def get_daemon(t):
|
||||
return t.isDaemon()
|
||||
|
||||
def set_daemon(t, val):
|
||||
t.setDaemon(val)
|
||||
|
||||
try:
|
||||
from email.utils import formatdate
|
||||
|
||||
def HTTPDate(timeval=None):
|
||||
return formatdate(timeval, usegmt=True)
|
||||
except ImportError:
|
||||
|
@ -215,40 +282,49 @@ except ImportError:
|
|||
try:
|
||||
# Python 3
|
||||
from urllib.parse import unquote as parse_unquote
|
||||
|
||||
def unquote_qs(atom, encoding, errors='strict'):
|
||||
return parse_unquote(atom.replace('+', ' '), encoding=encoding, errors=errors)
|
||||
return parse_unquote(
|
||||
atom.replace('+', ' '),
|
||||
encoding=encoding,
|
||||
errors=errors)
|
||||
except ImportError:
|
||||
# Python 2
|
||||
from urllib import unquote as parse_unquote
|
||||
|
||||
def unquote_qs(atom, encoding, errors='strict'):
|
||||
return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors)
|
||||
|
||||
try:
|
||||
# Prefer simplejson, which is usually more advanced than the builtin module.
|
||||
# Prefer simplejson, which is usually more advanced than the builtin
|
||||
# module.
|
||||
import simplejson as json
|
||||
json_decode = json.JSONDecoder().decode
|
||||
json_encode = json.JSONEncoder().iterencode
|
||||
_json_encode = json.JSONEncoder().iterencode
|
||||
except ImportError:
|
||||
if sys.version_info >= (3, 0):
|
||||
# Python 3.0: json is part of the standard library,
|
||||
# but outputs unicode. We need bytes.
|
||||
if sys.version_info >= (2, 6):
|
||||
# Python >=2.6 : json is part of the standard library
|
||||
import json
|
||||
json_decode = json.JSONDecoder().decode
|
||||
_json_encode = json.JSONEncoder().iterencode
|
||||
else:
|
||||
json = None
|
||||
|
||||
def json_decode(s):
|
||||
raise ValueError('No JSON library is available')
|
||||
|
||||
def _json_encode(s):
|
||||
raise ValueError('No JSON library is available')
|
||||
finally:
|
||||
if json and py3k:
|
||||
# The two Python 3 implementations (simplejson/json)
|
||||
# outputs str. We need bytes.
|
||||
def json_encode(value):
|
||||
for chunk in _json_encode(value):
|
||||
yield chunk.encode('utf8')
|
||||
elif sys.version_info >= (2, 6):
|
||||
# Python 2.6: json is part of the standard library
|
||||
import json
|
||||
json_decode = json.JSONDecoder().decode
|
||||
json_encode = json.JSONEncoder().iterencode
|
||||
else:
|
||||
json = None
|
||||
def json_decode(s):
|
||||
raise ValueError('No JSON library is available')
|
||||
def json_encode(s):
|
||||
raise ValueError('No JSON library is available')
|
||||
json_encode = _json_encode
|
||||
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
|
@ -260,11 +336,13 @@ except ImportError:
|
|||
try:
|
||||
os.urandom(20)
|
||||
import binascii
|
||||
|
||||
def random20():
|
||||
return binascii.hexlify(os.urandom(20)).decode('ascii')
|
||||
except (AttributeError, NotImplementedError):
|
||||
import random
|
||||
# os.urandom not available until Python 2.4. Fall back to random.random.
|
||||
|
||||
def random20():
|
||||
return sha('%s' % random.random()).hexdigest()
|
||||
|
||||
|
@ -281,3 +359,25 @@ except NameError:
|
|||
def next(i):
|
||||
return i.next()
|
||||
|
||||
if sys.version_info >= (3, 3):
|
||||
Timer = threading.Timer
|
||||
Event = threading.Event
|
||||
else:
|
||||
# Python 3.2 and earlier
|
||||
Timer = threading._Timer
|
||||
Event = threading._Event
|
||||
|
||||
# Prior to Python 2.6, the Thread class did not have a .daemon property.
|
||||
# This mix-in adds that property.
|
||||
|
||||
|
||||
class SetDaemonProperty:
|
||||
|
||||
def __get_daemon(self):
|
||||
return self.isDaemon()
|
||||
|
||||
def __set_daemon(self, daemon):
|
||||
self.setDaemon(daemon)
|
||||
|
||||
if sys.version_info < (2, 6):
|
||||
daemon = property(__get_daemon, __set_daemon)
|
||||
|
|
|
@ -50,14 +50,14 @@ attribute. For example::
|
|||
|
||||
class Demo:
|
||||
_cp_config = {'tools.gzip.on': True}
|
||||
|
||||
|
||||
def index(self):
|
||||
return "Hello world"
|
||||
index.exposed = True
|
||||
index._cp_config = {'request.show_tracebacks': False}
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
This behavior is only guaranteed for the default dispatcher.
|
||||
Other dispatchers may have different restrictions on where
|
||||
you can attach _cp_config attributes.
|
||||
|
@ -125,15 +125,16 @@ from cherrypy.lib import reprconf
|
|||
# Deprecated in CherryPy 3.2--remove in 3.3
|
||||
NamespaceSet = reprconf.NamespaceSet
|
||||
|
||||
|
||||
def merge(base, other):
|
||||
"""Merge one app config (from a dict, file, or filename) into another.
|
||||
|
||||
|
||||
If the given config is a filename, it will be appended to
|
||||
the list of files to monitor for "autoreload" changes.
|
||||
"""
|
||||
if isinstance(other, basestring):
|
||||
cherrypy.engine.autoreload.files.add(other)
|
||||
|
||||
|
||||
# Load other into base
|
||||
for section, value_map in reprconf.as_dict(other).items():
|
||||
if not isinstance(value_map, dict):
|
||||
|
@ -146,6 +147,7 @@ def merge(base, other):
|
|||
|
||||
|
||||
class Config(reprconf.Config):
|
||||
|
||||
"""The 'global' configuration data for the entire CherryPy process."""
|
||||
|
||||
def update(self, config):
|
||||
|
@ -157,20 +159,21 @@ class Config(reprconf.Config):
|
|||
|
||||
def _apply(self, config):
|
||||
"""Update self from a dict."""
|
||||
if isinstance(config.get("global", None), dict):
|
||||
if isinstance(config.get("global"), dict):
|
||||
if len(config) > 1:
|
||||
cherrypy.checker.global_config_contained_paths = True
|
||||
config = config["global"]
|
||||
if 'tools.staticdir.dir' in config:
|
||||
config['tools.staticdir.section'] = "global"
|
||||
reprconf.Config._apply(self, config)
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Decorator for page handlers to set _cp_config."""
|
||||
if args:
|
||||
raise TypeError(
|
||||
"The cherrypy.config decorator does not accept positional "
|
||||
"arguments; you must use keyword arguments.")
|
||||
|
||||
def tool_decorator(f):
|
||||
if not hasattr(f, "_cp_config"):
|
||||
f._cp_config = {}
|
||||
|
@ -180,25 +183,26 @@ class Config(reprconf.Config):
|
|||
return tool_decorator
|
||||
|
||||
|
||||
# Sphinx begin config.environments
|
||||
Config.environments = environments = {
|
||||
"staging": {
|
||||
'engine.autoreload_on': False,
|
||||
'engine.autoreload.on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': False,
|
||||
'request.show_mismatched_params': False,
|
||||
},
|
||||
},
|
||||
"production": {
|
||||
'engine.autoreload_on': False,
|
||||
'engine.autoreload.on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': False,
|
||||
'request.show_mismatched_params': False,
|
||||
'log.screen': False,
|
||||
},
|
||||
},
|
||||
"embedded": {
|
||||
# For use with CherryPy embedded in another deployment stack.
|
||||
'engine.autoreload_on': False,
|
||||
'engine.autoreload.on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': False,
|
||||
|
@ -206,16 +210,17 @@ Config.environments = environments = {
|
|||
'log.screen': False,
|
||||
'engine.SIGHUP': None,
|
||||
'engine.SIGTERM': None,
|
||||
},
|
||||
},
|
||||
"test_suite": {
|
||||
'engine.autoreload_on': False,
|
||||
'engine.autoreload.on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': True,
|
||||
'request.show_mismatched_params': True,
|
||||
'log.screen': False,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
# Sphinx end config.environments
|
||||
|
||||
|
||||
def _server_namespace_handler(k, v):
|
||||
|
@ -226,14 +231,14 @@ def _server_namespace_handler(k, v):
|
|||
# to configure additional HTTP servers.
|
||||
if not hasattr(cherrypy, "servers"):
|
||||
cherrypy.servers = {}
|
||||
|
||||
|
||||
servername, k = atoms
|
||||
if servername not in cherrypy.servers:
|
||||
from cherrypy import _cpserver
|
||||
cherrypy.servers[servername] = _cpserver.Server()
|
||||
# On by default, but 'on = False' can unsubscribe it (see below).
|
||||
cherrypy.servers[servername].subscribe()
|
||||
|
||||
|
||||
if k == 'on':
|
||||
if v:
|
||||
cherrypy.servers[servername].subscribe()
|
||||
|
@ -245,9 +250,24 @@ def _server_namespace_handler(k, v):
|
|||
setattr(cherrypy.server, k, v)
|
||||
Config.namespaces["server"] = _server_namespace_handler
|
||||
|
||||
|
||||
def _engine_namespace_handler(k, v):
|
||||
"""Backward compatibility handler for the "engine" namespace."""
|
||||
engine = cherrypy.engine
|
||||
|
||||
deprecated = {
|
||||
'autoreload_on': 'autoreload.on',
|
||||
'autoreload_frequency': 'autoreload.frequency',
|
||||
'autoreload_match': 'autoreload.match',
|
||||
'reload_files': 'autoreload.files',
|
||||
'deadlock_poll_freq': 'timeout_monitor.frequency'
|
||||
}
|
||||
|
||||
if k in deprecated:
|
||||
engine.log(
|
||||
'WARNING: Use of engine.%s is deprecated and will be removed in a '
|
||||
'future version. Use engine.%s instead.' % (k, deprecated[k]))
|
||||
|
||||
if k == 'autoreload_on':
|
||||
if v:
|
||||
engine.autoreload.subscribe()
|
||||
|
@ -272,7 +292,10 @@ def _engine_namespace_handler(k, v):
|
|||
if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
|
||||
plugin.subscribe()
|
||||
return
|
||||
elif (not v) and hasattr(getattr(plugin, 'unsubscribe', None), '__call__'):
|
||||
elif (
|
||||
(not v) and
|
||||
hasattr(getattr(plugin, 'unsubscribe', None), '__call__')
|
||||
):
|
||||
plugin.unsubscribe()
|
||||
return
|
||||
setattr(plugin, attrname, v)
|
||||
|
@ -286,10 +309,9 @@ def _tree_namespace_handler(k, v):
|
|||
if isinstance(v, dict):
|
||||
for script_name, app in v.items():
|
||||
cherrypy.tree.graft(app, script_name)
|
||||
cherrypy.engine.log("Mounted: %s on %s" % (app, script_name or "/"))
|
||||
cherrypy.engine.log("Mounted: %s on %s" %
|
||||
(app, script_name or "/"))
|
||||
else:
|
||||
cherrypy.tree.graft(v, v.script_name)
|
||||
cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/"))
|
||||
Config.namespaces["tree"] = _tree_namespace_handler
|
||||
|
||||
|
||||
|
|
|
@ -12,18 +12,50 @@ to a hierarchical arrangement of objects, starting at request.app.root.
|
|||
import string
|
||||
import sys
|
||||
import types
|
||||
try:
|
||||
classtype = (type, types.ClassType)
|
||||
except AttributeError:
|
||||
classtype = type
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import set
|
||||
|
||||
|
||||
class PageHandler(object):
|
||||
|
||||
"""Callable which sets response.body."""
|
||||
|
||||
|
||||
def __init__(self, callable, *args, **kwargs):
|
||||
self.callable = callable
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
def get_args(self):
|
||||
return cherrypy.serving.request.args
|
||||
|
||||
def set_args(self, args):
|
||||
cherrypy.serving.request.args = args
|
||||
return cherrypy.serving.request.args
|
||||
|
||||
args = property(
|
||||
get_args,
|
||||
set_args,
|
||||
doc="The ordered args should be accessible from post dispatch hooks"
|
||||
)
|
||||
|
||||
def get_kwargs(self):
|
||||
return cherrypy.serving.request.kwargs
|
||||
|
||||
def set_kwargs(self, kwargs):
|
||||
cherrypy.serving.request.kwargs = kwargs
|
||||
return cherrypy.serving.request.kwargs
|
||||
|
||||
kwargs = property(
|
||||
get_kwargs,
|
||||
set_kwargs,
|
||||
doc="The named kwargs should be accessible from post dispatch hooks"
|
||||
)
|
||||
|
||||
def __call__(self):
|
||||
try:
|
||||
return self.callable(*self.args, **self.kwargs)
|
||||
|
@ -49,7 +81,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
|
|||
2. Too little parameters are passed to the function.
|
||||
|
||||
There are 3 sources of parameters to a cherrypy handler.
|
||||
1. query string parameters are passed as keyword parameters to the handler.
|
||||
1. query string parameters are passed as keyword parameters to the
|
||||
handler.
|
||||
2. body parameters are also passed as keyword parameters.
|
||||
3. when partial matching occurs, the final path atoms are passed as
|
||||
positional args.
|
||||
|
@ -60,12 +93,13 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
|
|||
show_mismatched_params = getattr(
|
||||
cherrypy.serving.request, 'show_mismatched_params', False)
|
||||
try:
|
||||
(args, varargs, varkw, defaults) = inspect.getargspec(callable)
|
||||
(args, varargs, varkw, defaults) = getargspec(callable)
|
||||
except TypeError:
|
||||
if isinstance(callable, object) and hasattr(callable, '__call__'):
|
||||
(args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
|
||||
(args, varargs, varkw,
|
||||
defaults) = getargspec(callable.__call__)
|
||||
else:
|
||||
# If it wasn't one of our own types, re-raise
|
||||
# If it wasn't one of our own types, re-raise
|
||||
# the original error
|
||||
raise
|
||||
|
||||
|
@ -112,15 +146,15 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
|
|||
# 2. not enough body parameters -> 400
|
||||
# 3. not enough path parts (partial matches) -> 404
|
||||
#
|
||||
# We can't actually tell which case it is,
|
||||
# We can't actually tell which case it is,
|
||||
# so I'm raising a 404 because that covers 2/3 of the
|
||||
# possibilities
|
||||
#
|
||||
#
|
||||
# In the case where the method does not allow body
|
||||
# arguments it's definitely a 404.
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message="Missing parameters: %s" % ",".join(missing_args)
|
||||
message = "Missing parameters: %s" % ",".join(missing_args)
|
||||
raise cherrypy.HTTPError(404, message=message)
|
||||
|
||||
# the extra positional arguments come from the path - 404 Not Found
|
||||
|
@ -142,8 +176,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
|
|||
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message="Multiple values for parameters: "\
|
||||
"%s" % ",".join(multiple_args)
|
||||
message = "Multiple values for parameters: "\
|
||||
"%s" % ",".join(multiple_args)
|
||||
raise cherrypy.HTTPError(error, message=message)
|
||||
|
||||
if not varkw and varkw_usage > 0:
|
||||
|
@ -153,8 +187,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
|
|||
if extra_qs_params:
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message="Unexpected query string "\
|
||||
"parameters: %s" % ", ".join(extra_qs_params)
|
||||
message = "Unexpected query string "\
|
||||
"parameters: %s" % ", ".join(extra_qs_params)
|
||||
raise cherrypy.HTTPError(404, message=message)
|
||||
|
||||
# If there were any extra body parameters, it's a 400 Not Found
|
||||
|
@ -162,8 +196,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
|
|||
if extra_body_params:
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message="Unexpected body parameters: "\
|
||||
"%s" % ", ".join(extra_body_params)
|
||||
message = "Unexpected body parameters: "\
|
||||
"%s" % ", ".join(extra_body_params)
|
||||
raise cherrypy.HTTPError(400, message=message)
|
||||
|
||||
|
||||
|
@ -171,10 +205,16 @@ try:
|
|||
import inspect
|
||||
except ImportError:
|
||||
test_callable_spec = lambda callable, args, kwargs: 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]
|
||||
|
||||
|
||||
class LateParamPageHandler(PageHandler):
|
||||
|
||||
"""When passing cherrypy.request.params to the page handler, we do not
|
||||
want to capture that dict too early; we want to give tools like the
|
||||
decoding tool a chance to modify the params dict in-between the lookup
|
||||
|
@ -182,27 +222,43 @@ class LateParamPageHandler(PageHandler):
|
|||
takes that into account, and allows request.params to be 'bound late'
|
||||
(it's more complicated than that, but that's the effect).
|
||||
"""
|
||||
|
||||
|
||||
def _get_kwargs(self):
|
||||
kwargs = cherrypy.serving.request.params.copy()
|
||||
if self._kwargs:
|
||||
kwargs.update(self._kwargs)
|
||||
return kwargs
|
||||
|
||||
|
||||
def _set_kwargs(self, kwargs):
|
||||
cherrypy.serving.request.kwargs = kwargs
|
||||
self._kwargs = kwargs
|
||||
|
||||
|
||||
kwargs = property(_get_kwargs, _set_kwargs,
|
||||
doc='page handler kwargs (with '
|
||||
'cherrypy.request.params copied in)')
|
||||
|
||||
|
||||
punctuation_to_underscores = string.maketrans(
|
||||
string.punctuation, '_' * len(string.punctuation))
|
||||
if sys.version_info < (3, 0):
|
||||
punctuation_to_underscores = string.maketrans(
|
||||
string.punctuation, '_' * len(string.punctuation))
|
||||
|
||||
def validate_translator(t):
|
||||
if not isinstance(t, str) or len(t) != 256:
|
||||
raise ValueError(
|
||||
"The translate argument must be a str of len 256.")
|
||||
else:
|
||||
punctuation_to_underscores = str.maketrans(
|
||||
string.punctuation, '_' * len(string.punctuation))
|
||||
|
||||
def validate_translator(t):
|
||||
if not isinstance(t, dict):
|
||||
raise ValueError("The translate argument must be a dict.")
|
||||
|
||||
|
||||
class Dispatcher(object):
|
||||
|
||||
"""CherryPy Dispatcher which walks a tree of objects to find a handler.
|
||||
|
||||
|
||||
The tree is rooted at cherrypy.request.app.root, and each hierarchical
|
||||
component in the path_info argument is matched to a corresponding nested
|
||||
attribute of the root object. Matching handlers must have an 'exposed'
|
||||
|
@ -210,20 +266,19 @@ class Dispatcher(object):
|
|||
matches a URI which ends in a slash ("/"). The special method name
|
||||
"default" may match a portion of the path_info (but only when no longer
|
||||
substring of the path_info matches some other object).
|
||||
|
||||
|
||||
This is the default, built-in dispatcher for CherryPy.
|
||||
"""
|
||||
|
||||
|
||||
dispatch_method_name = '_cp_dispatch'
|
||||
"""
|
||||
The name of the dispatch method that nodes may optionally implement
|
||||
to provide their own dynamic dispatch algorithm.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, dispatch_method_name=None,
|
||||
translate=punctuation_to_underscores):
|
||||
if not isinstance(translate, str) or len(translate) != 256:
|
||||
raise ValueError("The translate argument must be a str of len 256.")
|
||||
validate_translator(translate)
|
||||
self.translate = translate
|
||||
if dispatch_method_name:
|
||||
self.dispatch_method_name = dispatch_method_name
|
||||
|
@ -232,27 +287,27 @@ class Dispatcher(object):
|
|||
"""Set handler and config for the current request."""
|
||||
request = cherrypy.serving.request
|
||||
func, vpath = self.find_handler(path_info)
|
||||
|
||||
|
||||
if func:
|
||||
# Decode any leftover %2F in the virtual_path atoms.
|
||||
vpath = [x.replace("%2F", "/") for x in vpath]
|
||||
request.handler = LateParamPageHandler(func, *vpath)
|
||||
else:
|
||||
request.handler = cherrypy.NotFound()
|
||||
|
||||
|
||||
def find_handler(self, path):
|
||||
"""Return the appropriate page handler, plus any virtual path.
|
||||
|
||||
|
||||
This will return two objects. The first will be a callable,
|
||||
which can be used to generate page output. Any parameters from
|
||||
the query string or request body will be sent to that callable
|
||||
as keyword arguments.
|
||||
|
||||
|
||||
The callable is found by traversing the application's tree,
|
||||
starting from cherrypy.request.app.root, and matching path
|
||||
components to successive objects in the tree. For example, the
|
||||
URL "/path/to/handler" might return root.path.to.handler.
|
||||
|
||||
|
||||
The second object returned will be a list of names which are
|
||||
'virtual path' components: parts of the URL which are dynamic,
|
||||
and were not used when looking up the handler.
|
||||
|
@ -263,7 +318,7 @@ class Dispatcher(object):
|
|||
app = request.app
|
||||
root = app.root
|
||||
dispatch_name = self.dispatch_method_name
|
||||
|
||||
|
||||
# Get config for the root object/path.
|
||||
fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
|
||||
fullpath_len = len(fullpath)
|
||||
|
@ -274,14 +329,14 @@ class Dispatcher(object):
|
|||
if "/" in app.config:
|
||||
nodeconf.update(app.config["/"])
|
||||
object_trail = [['root', root, nodeconf, segleft]]
|
||||
|
||||
|
||||
node = root
|
||||
iternames = fullpath[:]
|
||||
while iternames:
|
||||
name = iternames[0]
|
||||
# map to legal Python identifiers (e.g. replace '.' with '_')
|
||||
objname = name.translate(self.translate)
|
||||
|
||||
|
||||
nodeconf = {}
|
||||
subnode = getattr(node, objname, None)
|
||||
pre_len = len(iternames)
|
||||
|
@ -290,31 +345,31 @@ class Dispatcher(object):
|
|||
if dispatch and hasattr(dispatch, '__call__') and not \
|
||||
getattr(dispatch, 'exposed', False) and \
|
||||
pre_len > 1:
|
||||
#Don't expose the hidden 'index' token to _cp_dispatch
|
||||
#We skip this if pre_len == 1 since it makes no sense
|
||||
#to call a dispatcher when we have no tokens left.
|
||||
# Don't expose the hidden 'index' token to _cp_dispatch
|
||||
# We skip this if pre_len == 1 since it makes no sense
|
||||
# to call a dispatcher when we have no tokens left.
|
||||
index_name = iternames.pop()
|
||||
subnode = dispatch(vpath=iternames)
|
||||
iternames.append(index_name)
|
||||
else:
|
||||
#We didn't find a path, but keep processing in case there
|
||||
#is a default() handler.
|
||||
# We didn't find a path, but keep processing in case there
|
||||
# is a default() handler.
|
||||
iternames.pop(0)
|
||||
else:
|
||||
#We found the path, remove the vpath entry
|
||||
# We found the path, remove the vpath entry
|
||||
iternames.pop(0)
|
||||
segleft = len(iternames)
|
||||
if segleft > pre_len:
|
||||
#No path segment was removed. Raise an error.
|
||||
# No path segment was removed. Raise an error.
|
||||
raise cherrypy.CherryPyException(
|
||||
"A vpath segment was added. Custom dispatchers may only "
|
||||
+ "remove elements. While trying to process "
|
||||
+ "{0} in {1}".format(name, fullpath)
|
||||
)
|
||||
)
|
||||
elif segleft == pre_len:
|
||||
#Assume that the handler used the current path segment, but
|
||||
#did not pop it. This allows things like
|
||||
#return getattr(self, vpath[0], None)
|
||||
# Assume that the handler used the current path segment, but
|
||||
# did not pop it. This allows things like
|
||||
# return getattr(self, vpath[0], None)
|
||||
iternames.pop(0)
|
||||
segleft -= 1
|
||||
node = subnode
|
||||
|
@ -323,7 +378,7 @@ class Dispatcher(object):
|
|||
# Get _cp_config attached to this node.
|
||||
if hasattr(node, "_cp_config"):
|
||||
nodeconf.update(node._cp_config)
|
||||
|
||||
|
||||
# Mix in values from app.config for this path.
|
||||
existing_len = fullpath_len - pre_len
|
||||
if existing_len != 0:
|
||||
|
@ -335,43 +390,47 @@ class Dispatcher(object):
|
|||
curpath += '/' + seg
|
||||
if curpath in app.config:
|
||||
nodeconf.update(app.config[curpath])
|
||||
|
||||
|
||||
object_trail.append([name, node, nodeconf, segleft])
|
||||
|
||||
|
||||
def set_conf():
|
||||
"""Collapse all object_trail config into cherrypy.request.config."""
|
||||
"""Collapse all object_trail config into cherrypy.request.config.
|
||||
"""
|
||||
base = cherrypy.config.copy()
|
||||
# Note that we merge the config from each node
|
||||
# even if that node was None.
|
||||
for name, obj, conf, segleft in object_trail:
|
||||
base.update(conf)
|
||||
if 'tools.staticdir.dir' in conf:
|
||||
base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
|
||||
base['tools.staticdir.section'] = '/' + \
|
||||
'/'.join(fullpath[0:fullpath_len - segleft])
|
||||
return base
|
||||
|
||||
|
||||
# Try successive objects (reverse order)
|
||||
num_candidates = len(object_trail) - 1
|
||||
for i in range(num_candidates, -1, -1):
|
||||
|
||||
|
||||
name, candidate, nodeconf, segleft = object_trail[i]
|
||||
if candidate is None:
|
||||
continue
|
||||
|
||||
|
||||
# Try a "default" method on the current leaf.
|
||||
if hasattr(candidate, "default"):
|
||||
defhandler = candidate.default
|
||||
if getattr(defhandler, 'exposed', False):
|
||||
# Insert any extra _cp_config from the default handler.
|
||||
conf = getattr(defhandler, "_cp_config", {})
|
||||
object_trail.insert(i+1, ["default", defhandler, conf, segleft])
|
||||
object_trail.insert(
|
||||
i + 1, ["default", defhandler, conf, segleft])
|
||||
request.config = set_conf()
|
||||
# See http://www.cherrypy.org/ticket/613
|
||||
# See https://bitbucket.org/cherrypy/cherrypy/issue/613
|
||||
request.is_index = path.endswith("/")
|
||||
return defhandler, fullpath[fullpath_len - segleft:-1]
|
||||
|
||||
# Uncomment the next line to restrict positional params to "default".
|
||||
|
||||
# Uncomment the next line to restrict positional params to
|
||||
# "default".
|
||||
# if i < num_candidates - 2: continue
|
||||
|
||||
|
||||
# Try the current leaf.
|
||||
if getattr(candidate, 'exposed', False):
|
||||
request.config = set_conf()
|
||||
|
@ -386,28 +445,29 @@ class Dispatcher(object):
|
|||
# positional parameters (virtual paths).
|
||||
request.is_index = False
|
||||
return candidate, fullpath[fullpath_len - segleft:-1]
|
||||
|
||||
|
||||
# We didn't find anything
|
||||
request.config = set_conf()
|
||||
return None, []
|
||||
|
||||
|
||||
class MethodDispatcher(Dispatcher):
|
||||
|
||||
"""Additional dispatch based on cherrypy.request.method.upper().
|
||||
|
||||
|
||||
Methods named GET, POST, etc will be called on an exposed class.
|
||||
The method names must be all caps; the appropriate Allow header
|
||||
will be output showing all capitalized method names as allowable
|
||||
HTTP verbs.
|
||||
|
||||
|
||||
Note that the containing class must be exposed, not the methods.
|
||||
"""
|
||||
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
request = cherrypy.serving.request
|
||||
resource, vpath = self.find_handler(path_info)
|
||||
|
||||
|
||||
if resource:
|
||||
# Set Allow header
|
||||
avail = [m for m in dir(resource) if m.isupper()]
|
||||
|
@ -415,7 +475,7 @@ class MethodDispatcher(Dispatcher):
|
|||
avail.append("HEAD")
|
||||
avail.sort()
|
||||
cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
|
||||
|
||||
|
||||
# Find the subhandler
|
||||
meth = request.method.upper()
|
||||
func = getattr(resource, meth, None)
|
||||
|
@ -425,7 +485,7 @@ class MethodDispatcher(Dispatcher):
|
|||
# Grab any _cp_config on the subhandler.
|
||||
if hasattr(func, "_cp_config"):
|
||||
request.config.update(func._cp_config)
|
||||
|
||||
|
||||
# Decode any leftover %2F in the virtual_path atoms.
|
||||
vpath = [x.replace("%2F", "/") for x in vpath]
|
||||
request.handler = LateParamPageHandler(func, *vpath)
|
||||
|
@ -436,9 +496,10 @@ class MethodDispatcher(Dispatcher):
|
|||
|
||||
|
||||
class RoutesDispatcher(object):
|
||||
|
||||
"""A Routes based dispatcher for CherryPy."""
|
||||
|
||||
def __init__(self, full_result=False):
|
||||
|
||||
def __init__(self, full_result=False, **mapper_options):
|
||||
"""
|
||||
Routes dispatcher
|
||||
|
||||
|
@ -449,16 +510,16 @@ class RoutesDispatcher(object):
|
|||
import routes
|
||||
self.full_result = full_result
|
||||
self.controllers = {}
|
||||
self.mapper = routes.Mapper()
|
||||
self.mapper = routes.Mapper(**mapper_options)
|
||||
self.mapper.controller_scan = self.controllers.keys
|
||||
|
||||
|
||||
def connect(self, name, route, controller, **kwargs):
|
||||
self.controllers[name] = controller
|
||||
self.mapper.connect(name, route, controller=name, **kwargs)
|
||||
|
||||
|
||||
def redirect(self, url):
|
||||
raise cherrypy.HTTPRedirect(url)
|
||||
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
func = self.find_handler(path_info)
|
||||
|
@ -466,13 +527,13 @@ class RoutesDispatcher(object):
|
|||
cherrypy.serving.request.handler = LateParamPageHandler(func)
|
||||
else:
|
||||
cherrypy.serving.request.handler = cherrypy.NotFound()
|
||||
|
||||
|
||||
def find_handler(self, path_info):
|
||||
"""Find the right page handler, and set request.config."""
|
||||
import routes
|
||||
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
config = routes.request_config()
|
||||
config.mapper = self.mapper
|
||||
if hasattr(request, 'wsgi_environ'):
|
||||
|
@ -480,9 +541,9 @@ class RoutesDispatcher(object):
|
|||
config.host = request.headers.get('Host', None)
|
||||
config.protocol = request.scheme
|
||||
config.redirect = self.redirect
|
||||
|
||||
|
||||
result = self.mapper.match(path_info)
|
||||
|
||||
|
||||
config.mapper_dict = result
|
||||
params = {}
|
||||
if result:
|
||||
|
@ -491,23 +552,23 @@ class RoutesDispatcher(object):
|
|||
params.pop('controller', None)
|
||||
params.pop('action', None)
|
||||
request.params.update(params)
|
||||
|
||||
|
||||
# Get config for the root object/path.
|
||||
request.config = base = cherrypy.config.copy()
|
||||
curpath = ""
|
||||
|
||||
|
||||
def merge(nodeconf):
|
||||
if 'tools.staticdir.dir' in nodeconf:
|
||||
nodeconf['tools.staticdir.section'] = curpath or "/"
|
||||
base.update(nodeconf)
|
||||
|
||||
|
||||
app = request.app
|
||||
root = app.root
|
||||
if hasattr(root, "_cp_config"):
|
||||
merge(root._cp_config)
|
||||
if "/" in app.config:
|
||||
merge(app.config["/"])
|
||||
|
||||
|
||||
# Mix in values from app.config.
|
||||
atoms = [x for x in path_info.split("/") if x]
|
||||
if atoms:
|
||||
|
@ -518,75 +579,77 @@ class RoutesDispatcher(object):
|
|||
curpath = "/".join((curpath, atom))
|
||||
if curpath in app.config:
|
||||
merge(app.config[curpath])
|
||||
|
||||
|
||||
handler = None
|
||||
if result:
|
||||
controller = result.get('controller')
|
||||
controller = self.controllers.get(controller, controller)
|
||||
if controller:
|
||||
if isinstance(controller, (type, types.ClassType)):
|
||||
if isinstance(controller, classtype):
|
||||
controller = controller()
|
||||
# Get config from the controller.
|
||||
if hasattr(controller, "_cp_config"):
|
||||
merge(controller._cp_config)
|
||||
|
||||
|
||||
action = result.get('action')
|
||||
if action is not None:
|
||||
handler = getattr(controller, action, None)
|
||||
# Get config from the handler
|
||||
if hasattr(handler, "_cp_config"):
|
||||
# Get config from the handler
|
||||
if hasattr(handler, "_cp_config"):
|
||||
merge(handler._cp_config)
|
||||
else:
|
||||
handler = controller
|
||||
|
||||
|
||||
# Do the last path atom here so it can
|
||||
# override the controller's _cp_config.
|
||||
if last:
|
||||
curpath = "/".join((curpath, last))
|
||||
if curpath in app.config:
|
||||
merge(app.config[curpath])
|
||||
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
|
||||
from cherrypy.lib import xmlrpc
|
||||
from cherrypy.lib import xmlrpcutil
|
||||
|
||||
def xmlrpc_dispatch(path_info):
|
||||
path_info = xmlrpc.patched_path(path_info)
|
||||
path_info = xmlrpcutil.patched_path(path_info)
|
||||
return next_dispatcher(path_info)
|
||||
return xmlrpc_dispatch
|
||||
|
||||
|
||||
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
|
||||
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
|
||||
**domains):
|
||||
"""
|
||||
Select a different handler based on the Host header.
|
||||
|
||||
|
||||
This can be useful when running multiple sites within one CP server.
|
||||
It allows several domains to point to different parts of a single
|
||||
website structure. For example::
|
||||
|
||||
|
||||
http://www.domain.example -> root
|
||||
http://www.domain2.example -> root/domain2/
|
||||
http://www.domain2.example:443 -> root/secure
|
||||
|
||||
|
||||
can be accomplished via the following config::
|
||||
|
||||
|
||||
[/]
|
||||
request.dispatch = cherrypy.dispatch.VirtualHost(
|
||||
**{'www.domain2.example': '/domain2',
|
||||
'www.domain2.example:443': '/secure',
|
||||
})
|
||||
|
||||
|
||||
next_dispatcher
|
||||
The next dispatcher object in the dispatch chain.
|
||||
The VirtualHost dispatcher adds a prefix to the URL and calls
|
||||
another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
|
||||
|
||||
|
||||
use_x_forwarded_host
|
||||
If True (the default), any "X-Forwarded-Host"
|
||||
request header will be used instead of the "Host" header. This
|
||||
is commonly added by HTTP servers (such as Apache) when proxying.
|
||||
|
||||
|
||||
``**domains``
|
||||
A dict of {host header value: virtual prefix} pairs.
|
||||
The incoming "Host" request header is looked up in this dict,
|
||||
|
@ -597,26 +660,27 @@ def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domai
|
|||
headers may contain the port number.
|
||||
"""
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
def vhost_dispatch(path_info):
|
||||
request = cherrypy.serving.request
|
||||
header = request.headers.get
|
||||
|
||||
|
||||
domain = header('Host', '')
|
||||
if use_x_forwarded_host:
|
||||
domain = header("X-Forwarded-Host", domain)
|
||||
|
||||
|
||||
prefix = domains.get(domain, "")
|
||||
if prefix:
|
||||
path_info = httputil.urljoin(prefix, path_info)
|
||||
|
||||
|
||||
result = next_dispatcher(path_info)
|
||||
|
||||
# Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
|
||||
|
||||
# Touch up staticdir config. See
|
||||
# https://bitbucket.org/cherrypy/cherrypy/issue/614.
|
||||
section = request.config.get('tools.staticdir.section')
|
||||
if section:
|
||||
section = section[len(prefix):]
|
||||
request.config['tools.staticdir.section'] = section
|
||||
|
||||
|
||||
return result
|
||||
return vhost_dispatch
|
||||
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
CherryPy provides (and uses) exceptions for declaring that the HTTP response
|
||||
should be a status other than the default "200 OK". You can ``raise`` them like
|
||||
normal Python exceptions. You can also call them and they will raise themselves;
|
||||
this means you can set an :class:`HTTPError<cherrypy._cperror.HTTPError>`
|
||||
normal Python exceptions. You can also call them and they will raise
|
||||
themselves; this means you can set an
|
||||
:class:`HTTPError<cherrypy._cperror.HTTPError>`
|
||||
or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the
|
||||
:attr:`request.handler<cherrypy._cprequest.Request.handler>`.
|
||||
|
||||
|
@ -21,7 +22,8 @@ POST, however, is neither safe nor idempotent--if you
|
|||
charge a credit card, you don't want to be charged twice by a redirect!
|
||||
|
||||
For this reason, *none* of the 3xx responses permit a user-agent (browser) to
|
||||
resubmit a POST on redirection without first confirming the action with the user:
|
||||
resubmit a POST on redirection without first confirming the action with the
|
||||
user:
|
||||
|
||||
===== ================================= ===========
|
||||
300 Multiple Choices Confirm with the user
|
||||
|
@ -53,14 +55,16 @@ Anticipated HTTP responses
|
|||
--------------------------
|
||||
|
||||
The 'error_page' config namespace can be used to provide custom HTML output for
|
||||
expected responses (like 404 Not Found). Supply a filename from which the output
|
||||
will be read. The contents will be interpolated with the values %(status)s,
|
||||
%(message)s, %(traceback)s, and %(version)s using plain old Python
|
||||
`string formatting <http://www.python.org/doc/2.6.4/library/stdtypes.html#string-formatting-operations>`_.
|
||||
expected responses (like 404 Not Found). Supply a filename from which the
|
||||
output will be read. The contents will be interpolated with the values
|
||||
%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python
|
||||
`string formatting <http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_.
|
||||
|
||||
::
|
||||
|
||||
_cp_config = {'error_page.404': os.path.join(localDir, "static/index.html")}
|
||||
_cp_config = {
|
||||
'error_page.404': os.path.join(localDir, "static/index.html")
|
||||
}
|
||||
|
||||
|
||||
Beginning in version 3.1, you may also provide a function or other callable as
|
||||
|
@ -72,7 +76,8 @@ version arguments that are interpolated into templates::
|
|||
cherrypy.config.update({'error_page.402': error_page_402})
|
||||
|
||||
Also in 3.1, in addition to the numbered error codes, you may also supply
|
||||
"error_page.default" to handle all codes which do not have their own error_page entry.
|
||||
"error_page.default" to handle all codes which do not have their own error_page
|
||||
entry.
|
||||
|
||||
|
||||
|
||||
|
@ -81,8 +86,9 @@ Unanticipated errors
|
|||
|
||||
CherryPy also has a generic error handling mechanism: whenever an unanticipated
|
||||
error occurs in your code, it will call
|
||||
:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to set
|
||||
the response status, headers, and body. By default, this is the same output as
|
||||
:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to
|
||||
set the response status, headers, and body. By default, this is the same
|
||||
output as
|
||||
:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide
|
||||
some other behavior, you generally replace "request.error_response".
|
||||
|
||||
|
@ -93,67 +99,78 @@ send an e-mail containing the error::
|
|||
|
||||
def handle_error():
|
||||
cherrypy.response.status = 500
|
||||
cherrypy.response.body = ["<html><body>Sorry, an error occured</body></html>"]
|
||||
sendMail('error@domain.com', 'Error in your web app', _cperror.format_exc())
|
||||
cherrypy.response.body = [
|
||||
"<html><body>Sorry, an error occured</body></html>"
|
||||
]
|
||||
sendMail('error@domain.com',
|
||||
'Error in your web app',
|
||||
_cperror.format_exc())
|
||||
|
||||
class Root:
|
||||
_cp_config = {'request.error_response': handle_error}
|
||||
|
||||
|
||||
Note that you have to explicitly set :attr:`response.body <cherrypy._cprequest.Response.body>`
|
||||
Note that you have to explicitly set
|
||||
:attr:`response.body <cherrypy._cprequest.Response.body>`
|
||||
and not simply return an error message as a result.
|
||||
"""
|
||||
|
||||
from cgi import escape as _escape
|
||||
from sys import exc_info as _exc_info
|
||||
from traceback import format_exception as _format_exception
|
||||
from cherrypy._cpcompat import basestring, iteritems, urljoin as _urljoin
|
||||
from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob
|
||||
from cherrypy._cpcompat import tonative, urljoin as _urljoin
|
||||
from cherrypy.lib import httputil as _httputil
|
||||
|
||||
|
||||
class CherryPyException(Exception):
|
||||
|
||||
"""A base class for CherryPy exceptions."""
|
||||
pass
|
||||
|
||||
|
||||
class TimeoutError(CherryPyException):
|
||||
|
||||
"""Exception raised when Response.timed_out is detected."""
|
||||
pass
|
||||
|
||||
|
||||
class InternalRedirect(CherryPyException):
|
||||
|
||||
"""Exception raised to switch to the handler for a different URL.
|
||||
|
||||
|
||||
This exception will redirect processing to another path within the site
|
||||
(without informing the client). Provide the new path as an argument when
|
||||
raising the exception. Provide any params in the querystring for the new URL.
|
||||
raising the exception. Provide any params in the querystring for the new
|
||||
URL.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, path, query_string=""):
|
||||
import cherrypy
|
||||
self.request = cherrypy.serving.request
|
||||
|
||||
|
||||
self.query_string = query_string
|
||||
if "?" in path:
|
||||
# Separate any params included in the path
|
||||
path, self.query_string = path.split("?", 1)
|
||||
|
||||
|
||||
# Note that urljoin will "do the right thing" whether url is:
|
||||
# 1. a URL relative to root (e.g. "/dummy")
|
||||
# 2. a URL relative to the current path
|
||||
# Note that any query string will be discarded.
|
||||
path = _urljoin(self.request.path_info, path)
|
||||
|
||||
|
||||
# Set a 'path' member attribute so that code which traps this
|
||||
# error can have access to it.
|
||||
self.path = path
|
||||
|
||||
|
||||
CherryPyException.__init__(self, path, self.query_string)
|
||||
|
||||
|
||||
class HTTPRedirect(CherryPyException):
|
||||
|
||||
"""Exception raised when the request should be redirected.
|
||||
|
||||
|
||||
This exception will force a HTTP redirect to the URL or URL's you give it.
|
||||
The new URL must be passed as the first argument to the Exception,
|
||||
e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
|
||||
|
@ -162,41 +179,40 @@ class HTTPRedirect(CherryPyException):
|
|||
|
||||
If one of the provided URL is a unicode object, it will be encoded
|
||||
using the default encoding or the one passed in parameter.
|
||||
|
||||
|
||||
There are multiple types of redirect, from which you can select via the
|
||||
``status`` argument. If you do not provide a ``status`` arg, it defaults to
|
||||
303 (or 302 if responding with HTTP/1.0).
|
||||
|
||||
|
||||
Examples::
|
||||
|
||||
|
||||
raise cherrypy.HTTPRedirect("")
|
||||
raise cherrypy.HTTPRedirect("/abs/path", 307)
|
||||
raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
|
||||
|
||||
|
||||
See :ref:`redirectingpost` for additional caveats.
|
||||
"""
|
||||
|
||||
|
||||
status = None
|
||||
"""The integer HTTP status code to emit."""
|
||||
|
||||
|
||||
urls = None
|
||||
"""The list of URL's to emit."""
|
||||
|
||||
encoding = 'utf-8'
|
||||
"""The encoding when passed urls are unicode objects"""
|
||||
|
||||
"""The encoding when passed urls are not native strings"""
|
||||
|
||||
def __init__(self, urls, status=None, encoding=None):
|
||||
import cherrypy
|
||||
request = cherrypy.serving.request
|
||||
|
||||
|
||||
if isinstance(urls, basestring):
|
||||
urls = [urls]
|
||||
|
||||
|
||||
abs_urls = []
|
||||
for url in urls:
|
||||
if isinstance(url, unicode):
|
||||
url = url.encode(encoding or self.encoding)
|
||||
|
||||
url = tonative(url, encoding or self.encoding)
|
||||
|
||||
# Note that urljoin will "do the right thing" whether url is:
|
||||
# 1. a complete URL with host (e.g. "http://www.example.com/test")
|
||||
# 2. a URL relative to root (e.g. "/dummy")
|
||||
|
@ -205,7 +221,7 @@ class HTTPRedirect(CherryPyException):
|
|||
url = _urljoin(cherrypy.url(), url)
|
||||
abs_urls.append(url)
|
||||
self.urls = abs_urls
|
||||
|
||||
|
||||
# RFC 2616 indicates a 301 response code fits our goal; however,
|
||||
# browser support for 301 is quite messy. Do 302/303 instead. See
|
||||
# http://www.alanflavell.org.uk/www/post-redirect.html
|
||||
|
@ -218,37 +234,41 @@ class HTTPRedirect(CherryPyException):
|
|||
status = int(status)
|
||||
if status < 300 or status > 399:
|
||||
raise ValueError("status must be between 300 and 399.")
|
||||
|
||||
|
||||
self.status = status
|
||||
CherryPyException.__init__(self, abs_urls, status)
|
||||
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent self.
|
||||
|
||||
"""Modify cherrypy.response status, headers, and body to represent
|
||||
self.
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create an
|
||||
HTTPRedirect object and set its output without *raising* the exception.
|
||||
"""
|
||||
import cherrypy
|
||||
response = cherrypy.serving.response
|
||||
response.status = status = self.status
|
||||
|
||||
|
||||
if status in (300, 301, 302, 303, 307):
|
||||
response.headers['Content-Type'] = "text/html;charset=utf-8"
|
||||
# "The ... URI SHOULD be given by the Location field
|
||||
# in the response."
|
||||
response.headers['Location'] = self.urls[0]
|
||||
|
||||
|
||||
# "Unless the request method was HEAD, the entity of the response
|
||||
# SHOULD contain a short hypertext note with a hyperlink to the
|
||||
# new URI(s)."
|
||||
msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
|
||||
301: "This resource has permanently moved to <a href='%s'>%s</a>.",
|
||||
302: "This resource resides temporarily at <a href='%s'>%s</a>.",
|
||||
303: "This resource can be found at <a href='%s'>%s</a>.",
|
||||
307: "This resource has moved temporarily to <a href='%s'>%s</a>.",
|
||||
}[status]
|
||||
msgs = [msg % (u, u) for u in self.urls]
|
||||
response.body = "<br />\n".join(msgs)
|
||||
msg = {
|
||||
300: "This resource can be found at ",
|
||||
301: "This resource has permanently moved to ",
|
||||
302: "This resource resides temporarily at ",
|
||||
303: "This resource can be found at ",
|
||||
307: "This resource has moved temporarily to ",
|
||||
}[status]
|
||||
msg += '<a href=%s>%s</a>.'
|
||||
from xml.sax import saxutils
|
||||
msgs = [msg % (saxutils.quoteattr(u), u) for u in self.urls]
|
||||
response.body = ntob("<br />\n".join(msgs), 'utf-8')
|
||||
# Previous code may have set C-L, so we have to reset it
|
||||
# (allow finalize to set it).
|
||||
response.headers.pop('Content-Length', None)
|
||||
|
@ -257,7 +277,7 @@ class HTTPRedirect(CherryPyException):
|
|||
# "The response MUST include the following header fields:
|
||||
# Date, unless its omission is required by section 14.18.1"
|
||||
# The "Date" header should have been set in Response.__init__
|
||||
|
||||
|
||||
# "...the response SHOULD NOT include other entity-headers."
|
||||
for key in ('Allow', 'Content-Encoding', 'Content-Language',
|
||||
'Content-Length', 'Content-Location', 'Content-MD5',
|
||||
|
@ -265,7 +285,7 @@ class HTTPRedirect(CherryPyException):
|
|||
'Last-Modified'):
|
||||
if key in response.headers:
|
||||
del response.headers[key]
|
||||
|
||||
|
||||
# "The 304 response MUST NOT contain a message-body."
|
||||
response.body = None
|
||||
# Previous code may have set C-L, so we have to reset it.
|
||||
|
@ -279,7 +299,7 @@ class HTTPRedirect(CherryPyException):
|
|||
response.headers.pop('Content-Length', None)
|
||||
else:
|
||||
raise ValueError("The %s status code is unknown." % status)
|
||||
|
||||
|
||||
def __call__(self):
|
||||
"""Use this exception as a request.handler (raise self)."""
|
||||
raise self
|
||||
|
@ -288,9 +308,9 @@ class HTTPRedirect(CherryPyException):
|
|||
def clean_headers(status):
|
||||
"""Remove any headers which should not apply to an error response."""
|
||||
import cherrypy
|
||||
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
# Remove headers which applied to the original content,
|
||||
# but do not apply to the error page.
|
||||
respheaders = response.headers
|
||||
|
@ -299,7 +319,7 @@ def clean_headers(status):
|
|||
"Content-Location", "Content-MD5", "Last-Modified"]:
|
||||
if key in respheaders:
|
||||
del respheaders[key]
|
||||
|
||||
|
||||
if status != 416:
|
||||
# A server sending a response with status code 416 (Requested
|
||||
# range not satisfiable) SHOULD include a Content-Range field
|
||||
|
@ -312,88 +332,93 @@ def clean_headers(status):
|
|||
|
||||
|
||||
class HTTPError(CherryPyException):
|
||||
|
||||
"""Exception used to return an HTTP error code (4xx-5xx) to the client.
|
||||
|
||||
This exception can be used to automatically send a response using a http status
|
||||
code, with an appropriate error page. It takes an optional
|
||||
|
||||
This exception can be used to automatically send a response using a
|
||||
http status code, with an appropriate error page. It takes an optional
|
||||
``status`` argument (which must be between 400 and 599); it defaults to 500
|
||||
("Internal Server Error"). It also takes an optional ``message`` argument,
|
||||
which will be returned in the response body. See
|
||||
`RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
|
||||
`RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
|
||||
for a complete list of available error codes and when to use them.
|
||||
|
||||
|
||||
Examples::
|
||||
|
||||
|
||||
raise cherrypy.HTTPError(403)
|
||||
raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.")
|
||||
raise cherrypy.HTTPError(
|
||||
"403 Forbidden", "You are not allowed to access this resource.")
|
||||
"""
|
||||
|
||||
|
||||
status = None
|
||||
"""The HTTP status code. May be of type int or str (with a Reason-Phrase)."""
|
||||
|
||||
"""The HTTP status code. May be of type int or str (with a Reason-Phrase).
|
||||
"""
|
||||
|
||||
code = None
|
||||
"""The integer HTTP status code."""
|
||||
|
||||
|
||||
reason = None
|
||||
"""The HTTP Reason-Phrase string."""
|
||||
|
||||
|
||||
def __init__(self, status=500, message=None):
|
||||
self.status = status
|
||||
try:
|
||||
self.code, self.reason, defaultmsg = _httputil.valid_status(status)
|
||||
except ValueError, x:
|
||||
raise self.__class__(500, x.args[0])
|
||||
|
||||
except ValueError:
|
||||
raise self.__class__(500, _exc_info()[1].args[0])
|
||||
|
||||
if self.code < 400 or self.code > 599:
|
||||
raise ValueError("status must be between 400 and 599.")
|
||||
|
||||
|
||||
# See http://www.python.org/dev/peps/pep-0352/
|
||||
# self.message = message
|
||||
self._message = message or defaultmsg
|
||||
CherryPyException.__init__(self, status, message)
|
||||
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent self.
|
||||
|
||||
"""Modify cherrypy.response status, headers, and body to represent
|
||||
self.
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create an
|
||||
HTTPError object and set its output without *raising* the exception.
|
||||
"""
|
||||
import cherrypy
|
||||
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
clean_headers(self.code)
|
||||
|
||||
|
||||
# In all cases, finalize will be called after this method,
|
||||
# so don't bother cleaning up response values here.
|
||||
response.status = self.status
|
||||
tb = None
|
||||
if cherrypy.serving.request.show_tracebacks:
|
||||
tb = format_exc()
|
||||
response.headers['Content-Type'] = "text/html;charset=utf-8"
|
||||
|
||||
response.headers.pop('Content-Length', None)
|
||||
|
||||
|
||||
content = self.get_error_page(self.status, traceback=tb,
|
||||
message=self._message)
|
||||
response.body = content
|
||||
|
||||
|
||||
_be_ie_unfriendly(self.code)
|
||||
|
||||
|
||||
def get_error_page(self, *args, **kwargs):
|
||||
return get_error_page(*args, **kwargs)
|
||||
|
||||
|
||||
def __call__(self):
|
||||
"""Use this exception as a request.handler (raise self)."""
|
||||
raise self
|
||||
|
||||
|
||||
class NotFound(HTTPError):
|
||||
|
||||
"""Exception raised when a URL could not be mapped to any handler (404).
|
||||
|
||||
|
||||
This is equivalent to raising
|
||||
:class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, path=None):
|
||||
if path is None:
|
||||
import cherrypy
|
||||
|
@ -403,7 +428,8 @@ class NotFound(HTTPError):
|
|||
HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
|
||||
|
||||
|
||||
_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
|
||||
"-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html>
|
||||
<head>
|
||||
|
@ -426,25 +452,28 @@ _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitiona
|
|||
<p>%(message)s</p>
|
||||
<pre id="traceback">%(traceback)s</pre>
|
||||
<div id="powered_by">
|
||||
<span>Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
|
||||
<span>
|
||||
Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a>
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
def get_error_page(status, **kwargs):
|
||||
"""Return an HTML page, containing a pretty error response.
|
||||
|
||||
|
||||
status should be an int or a str.
|
||||
kwargs will be interpolated into the page template.
|
||||
"""
|
||||
import cherrypy
|
||||
|
||||
|
||||
try:
|
||||
code, reason, message = _httputil.valid_status(status)
|
||||
except ValueError, x:
|
||||
raise cherrypy.HTTPError(500, x.args[0])
|
||||
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(500, _exc_info()[1].args[0])
|
||||
|
||||
# We can't use setdefault here, because some
|
||||
# callers send None for kwarg values.
|
||||
if kwargs.get('status') is None:
|
||||
|
@ -455,22 +484,43 @@ def get_error_page(status, **kwargs):
|
|||
kwargs['traceback'] = ''
|
||||
if kwargs.get('version') is None:
|
||||
kwargs['version'] = cherrypy.__version__
|
||||
|
||||
|
||||
for k, v in iteritems(kwargs):
|
||||
if v is None:
|
||||
kwargs[k] = ""
|
||||
else:
|
||||
kwargs[k] = _escape(kwargs[k])
|
||||
|
||||
|
||||
# Use a custom template or callable for the error page?
|
||||
pages = cherrypy.serving.request.error_page
|
||||
error_page = pages.get(code) or pages.get('default')
|
||||
|
||||
# Default template, can be overridden below.
|
||||
template = _HTTPErrorTemplate
|
||||
if error_page:
|
||||
try:
|
||||
if hasattr(error_page, '__call__'):
|
||||
return error_page(**kwargs)
|
||||
# The caller function may be setting headers manually,
|
||||
# so we delegate to it completely. We may be returning
|
||||
# an iterator as well as a string here.
|
||||
#
|
||||
# We *must* make sure any content is not unicode.
|
||||
result = error_page(**kwargs)
|
||||
if cherrypy.lib.is_iterator(result):
|
||||
from cherrypy.lib.encoding import UTF8StreamEncoder
|
||||
return UTF8StreamEncoder(result)
|
||||
elif isinstance(result, cherrypy._cpcompat.unicodestr):
|
||||
return result.encode('utf-8')
|
||||
else:
|
||||
if not isinstance(result, cherrypy._cpcompat.bytestr):
|
||||
raise ValueError('error page function did not '
|
||||
'return a bytestring, unicodestring or an '
|
||||
'iterator - returned object of type %s.'
|
||||
% (type(result).__name__))
|
||||
return result
|
||||
else:
|
||||
return open(error_page, 'rb').read() % kwargs
|
||||
# Load the template from this path.
|
||||
template = tonative(open(error_page, 'rb').read())
|
||||
except:
|
||||
e = _format_exception(*_exc_info())[-1]
|
||||
m = kwargs['message']
|
||||
|
@ -478,21 +528,25 @@ def get_error_page(status, **kwargs):
|
|||
m += "<br />"
|
||||
m += "In addition, the custom error page failed:\n<br />%s" % e
|
||||
kwargs['message'] = m
|
||||
|
||||
return _HTTPErrorTemplate % kwargs
|
||||
|
||||
response = cherrypy.serving.response
|
||||
response.headers['Content-Type'] = "text/html;charset=utf-8"
|
||||
result = template % kwargs
|
||||
return result.encode('utf-8')
|
||||
|
||||
|
||||
|
||||
_ie_friendly_error_sizes = {
|
||||
400: 512, 403: 256, 404: 512, 405: 256,
|
||||
406: 512, 408: 512, 409: 512, 410: 256,
|
||||
500: 512, 501: 512, 505: 512,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _be_ie_unfriendly(status):
|
||||
import cherrypy
|
||||
response = cherrypy.serving.response
|
||||
|
||||
|
||||
# For some statuses, Internet Explorer 5+ shows "friendly error
|
||||
# messages" instead of our response.body if the body is smaller
|
||||
# than a given size. Fix this by returning a body over that size
|
||||
|
@ -508,46 +562,48 @@ def _be_ie_unfriendly(status):
|
|||
if l and l < s:
|
||||
# IN ADDITION: the response must be written to IE
|
||||
# in one chunk or it will still get replaced! Bah.
|
||||
content = content + (" " * (s - l))
|
||||
content = content + (ntob(" ") * (s - l))
|
||||
response.body = content
|
||||
response.headers['Content-Length'] = str(len(content))
|
||||
|
||||
|
||||
def format_exc(exc=None):
|
||||
"""Return exc (or sys.exc_info if None), formatted."""
|
||||
if exc is None:
|
||||
exc = _exc_info()
|
||||
if exc == (None, None, None):
|
||||
return ""
|
||||
import traceback
|
||||
return "".join(traceback.format_exception(*exc))
|
||||
try:
|
||||
if exc is None:
|
||||
exc = _exc_info()
|
||||
if exc == (None, None, None):
|
||||
return ""
|
||||
import traceback
|
||||
return "".join(traceback.format_exception(*exc))
|
||||
finally:
|
||||
del exc
|
||||
|
||||
|
||||
def bare_error(extrabody=None):
|
||||
"""Produce status, headers, body for a critical error.
|
||||
|
||||
|
||||
Returns a triple without calling any other questionable functions,
|
||||
so it should be as error-free as possible. Call it from an HTTP server
|
||||
if you get errors outside of the request.
|
||||
|
||||
|
||||
If extrabody is None, a friendly but rather unhelpful error message
|
||||
is set in the body. If extrabody is a string, it will be appended
|
||||
as-is to the body.
|
||||
"""
|
||||
|
||||
|
||||
# The whole point of this function is to be a last line-of-defense
|
||||
# in handling errors. That is, it must not raise any errors itself;
|
||||
# it cannot be allowed to fail. Therefore, don't add to it!
|
||||
# In particular, don't call any other CP functions.
|
||||
|
||||
body = "Unrecoverable error in the server."
|
||||
|
||||
body = ntob("Unrecoverable error in the server.")
|
||||
if extrabody is not None:
|
||||
if not isinstance(extrabody, str):
|
||||
if not isinstance(extrabody, bytestr):
|
||||
extrabody = extrabody.encode('utf-8')
|
||||
body += "\n" + extrabody
|
||||
|
||||
return ("500 Internal Server Error",
|
||||
[('Content-Type', 'text/plain'),
|
||||
('Content-Length', str(len(body)))],
|
||||
body += ntob("\n") + extrabody
|
||||
|
||||
return (ntob("500 Internal Server Error"),
|
||||
[(ntob('Content-Type'), ntob('text/plain')),
|
||||
(ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))],
|
||||
[body])
|
||||
|
||||
|
||||
|
|
|
@ -34,10 +34,11 @@ and another set of rules specific to each application. The global log
|
|||
manager is found at :func:`cherrypy.log`, and the log manager for each
|
||||
application is found at :attr:`app.log<cherrypy._cptree.Application.log>`.
|
||||
If you're inside a request, the latter is reachable from
|
||||
``cherrypy.request.app.log``; if you're outside a request, you'll have to obtain
|
||||
a reference to the ``app``: either the return value of
|
||||
``cherrypy.request.app.log``; if you're outside a request, you'll have to
|
||||
obtain a reference to the ``app``: either the return value of
|
||||
:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used
|
||||
:func:`quickstart()<cherrypy.quickstart>` instead, via ``cherrypy.tree.apps['/']``.
|
||||
:func:`quickstart()<cherrypy.quickstart>` instead, via
|
||||
``cherrypy.tree.apps['/']``.
|
||||
|
||||
By default, the global logs are named "cherrypy.error" and "cherrypy.access",
|
||||
and the application logs are named "cherrypy.error.2378745" and
|
||||
|
@ -55,6 +56,13 @@ errors! The format of access messages is highly formalized, but the error log
|
|||
isn't--it receives messages from a variety of sources (including full error
|
||||
tracebacks, if enabled).
|
||||
|
||||
If you are logging the access log and error log to the same source, then there
|
||||
is a possibility that a specially crafted error message may replicate an access
|
||||
log message as described in CWE-117. In this case it is the application
|
||||
developer's responsibility to manually escape data before using CherryPy's log()
|
||||
functionality, or they may create an application that is vulnerable to CWE-117.
|
||||
This would be achieved by using a custom handler escape any special characters,
|
||||
and attached as described below.
|
||||
|
||||
Custom Handlers
|
||||
===============
|
||||
|
@ -69,21 +77,21 @@ and uses a RotatingFileHandler instead:
|
|||
|
||||
#python
|
||||
log = app.log
|
||||
|
||||
|
||||
# Remove the default FileHandlers if present.
|
||||
log.error_file = ""
|
||||
log.access_file = ""
|
||||
|
||||
|
||||
maxBytes = getattr(log, "rot_maxBytes", 10000000)
|
||||
backupCount = getattr(log, "rot_backupCount", 1000)
|
||||
|
||||
|
||||
# Make a new RotatingFileHandler for the error log.
|
||||
fname = getattr(log, "rot_error_file", "error.log")
|
||||
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
|
||||
h.setLevel(DEBUG)
|
||||
h.setFormatter(_cplogging.logfmt)
|
||||
log.error_log.addHandler(h)
|
||||
|
||||
|
||||
# Make a new RotatingFileHandler for the access log.
|
||||
fname = getattr(log, "rot_access_file", "access.log")
|
||||
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
|
||||
|
@ -109,38 +117,58 @@ import sys
|
|||
|
||||
import cherrypy
|
||||
from cherrypy import _cperror
|
||||
from cherrypy._cpcompat import ntob, py3k
|
||||
|
||||
|
||||
class NullHandler(logging.Handler):
|
||||
|
||||
"""A no-op logging handler to silence the logging.lastResort handler."""
|
||||
|
||||
def handle(self, record):
|
||||
pass
|
||||
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
def createLock(self):
|
||||
self.lock = None
|
||||
|
||||
|
||||
class LogManager(object):
|
||||
|
||||
"""An object to assist both simple and advanced logging.
|
||||
|
||||
|
||||
``cherrypy.log`` is an instance of this class.
|
||||
"""
|
||||
|
||||
|
||||
appid = None
|
||||
"""The id() of the Application object which owns this log manager. If this
|
||||
is a global log manager, appid is None."""
|
||||
|
||||
|
||||
error_log = None
|
||||
"""The actual :class:`logging.Logger` instance for error messages."""
|
||||
|
||||
|
||||
access_log = None
|
||||
"""The actual :class:`logging.Logger` instance for access messages."""
|
||||
|
||||
access_log_format = \
|
||||
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
|
||||
if py3k:
|
||||
access_log_format = \
|
||||
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
|
||||
else:
|
||||
access_log_format = \
|
||||
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
|
||||
logger_root = None
|
||||
"""The "top-level" logger name.
|
||||
|
||||
|
||||
This string will be used as the first segment in the Logger names.
|
||||
The default is "cherrypy", for example, in which case the Logger names
|
||||
will be of the form::
|
||||
|
||||
|
||||
cherrypy.error.<appid>
|
||||
cherrypy.access.<appid>
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, appid=None, logger_root="cherrypy"):
|
||||
self.logger_root = logger_root
|
||||
self.appid = appid
|
||||
|
@ -148,12 +176,19 @@ class LogManager(object):
|
|||
self.error_log = logging.getLogger("%s.error" % logger_root)
|
||||
self.access_log = logging.getLogger("%s.access" % logger_root)
|
||||
else:
|
||||
self.error_log = logging.getLogger("%s.error.%s" % (logger_root, appid))
|
||||
self.access_log = logging.getLogger("%s.access.%s" % (logger_root, appid))
|
||||
self.error_log = logging.getLogger(
|
||||
"%s.error.%s" % (logger_root, appid))
|
||||
self.access_log = logging.getLogger(
|
||||
"%s.access.%s" % (logger_root, appid))
|
||||
self.error_log.setLevel(logging.INFO)
|
||||
self.access_log.setLevel(logging.INFO)
|
||||
|
||||
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
|
||||
self.error_log.addHandler(NullHandler())
|
||||
self.access_log.addHandler(NullHandler())
|
||||
|
||||
cherrypy.engine.subscribe('graceful', self.reopen_files)
|
||||
|
||||
|
||||
def reopen_files(self):
|
||||
"""Close and reopen all file handlers."""
|
||||
for log in (self.error_log, self.access_log):
|
||||
|
@ -163,34 +198,36 @@ class LogManager(object):
|
|||
h.stream.close()
|
||||
h.stream = open(h.baseFilename, h.mode)
|
||||
h.release()
|
||||
|
||||
def error(self, msg='', context='', severity=logging.INFO, traceback=False):
|
||||
|
||||
def error(self, msg='', context='', severity=logging.INFO,
|
||||
traceback=False):
|
||||
"""Write the given ``msg`` to the error log.
|
||||
|
||||
|
||||
This is not just for errors! Applications may call this at any time
|
||||
to log application-specific information.
|
||||
|
||||
|
||||
If ``traceback`` is True, the traceback of the current exception
|
||||
(if any) will be appended to ``msg``.
|
||||
"""
|
||||
if traceback:
|
||||
msg += _cperror.format_exc()
|
||||
self.error_log.log(severity, ' '.join((self.time(), context, msg)))
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""An alias for ``error``."""
|
||||
return self.error(*args, **kwargs)
|
||||
|
||||
|
||||
def access(self):
|
||||
"""Write to the access log (in Apache/NCSA Combined Log format).
|
||||
|
||||
See http://httpd.apache.org/docs/2.0/logs.html#combined for format
|
||||
details.
|
||||
|
||||
|
||||
See the
|
||||
`apache documentation <http://httpd.apache.org/docs/current/logs.html#combined>`_
|
||||
for format details.
|
||||
|
||||
CherryPy calls this automatically for you. Note there are no arguments;
|
||||
it collects the data itself from
|
||||
:class:`cherrypy.request<cherrypy._cprequest.Request>`.
|
||||
|
||||
|
||||
Like Apache started doing in 2.0.46, non-printable and other special
|
||||
characters in %r (and we expand that to all parts) are escaped using
|
||||
\\xhh sequences, where hh stands for the hexadecimal representation
|
||||
|
@ -206,8 +243,10 @@ class LogManager(object):
|
|||
if response.output_status is None:
|
||||
status = "-"
|
||||
else:
|
||||
status = response.output_status.split(" ", 1)[0]
|
||||
|
||||
status = response.output_status.split(ntob(" "), 1)[0]
|
||||
if py3k:
|
||||
status = status.decode('ISO-8859-1')
|
||||
|
||||
atoms = {'h': remote.name or remote.ip,
|
||||
'l': '-',
|
||||
'u': getattr(request, "login", None) or "-",
|
||||
|
@ -217,23 +256,48 @@ class LogManager(object):
|
|||
'b': dict.get(outheaders, 'Content-Length', '') or "-",
|
||||
'f': dict.get(inheaders, 'Referer', ''),
|
||||
'a': dict.get(inheaders, 'User-Agent', ''),
|
||||
'o': dict.get(inheaders, 'Host', '-'),
|
||||
}
|
||||
for k, v in atoms.items():
|
||||
if isinstance(v, unicode):
|
||||
v = v.encode('utf8')
|
||||
elif not isinstance(v, str):
|
||||
v = str(v)
|
||||
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
||||
# and backslash for us. All we have to do is strip the quotes.
|
||||
v = repr(v)[1:-1]
|
||||
# Escape double-quote.
|
||||
atoms[k] = v.replace('"', '\\"')
|
||||
|
||||
try:
|
||||
self.access_log.log(logging.INFO, self.access_log_format % atoms)
|
||||
except:
|
||||
self(traceback=True)
|
||||
|
||||
if py3k:
|
||||
for k, v in atoms.items():
|
||||
if not isinstance(v, str):
|
||||
v = str(v)
|
||||
v = v.replace('"', '\\"').encode('utf8')
|
||||
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
||||
# and backslash for us. All we have to do is strip the quotes.
|
||||
v = repr(v)[2:-1]
|
||||
|
||||
# in python 3.0 the repr of bytes (as returned by encode)
|
||||
# uses double \'s. But then the logger escapes them yet, again
|
||||
# resulting in quadruple slashes. Remove the extra one here.
|
||||
v = v.replace('\\\\', '\\')
|
||||
|
||||
# Escape double-quote.
|
||||
atoms[k] = v
|
||||
|
||||
try:
|
||||
self.access_log.log(
|
||||
logging.INFO, self.access_log_format.format(**atoms))
|
||||
except:
|
||||
self(traceback=True)
|
||||
else:
|
||||
for k, v in atoms.items():
|
||||
if isinstance(v, unicode):
|
||||
v = v.encode('utf8')
|
||||
elif not isinstance(v, str):
|
||||
v = str(v)
|
||||
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
||||
# and backslash for us. All we have to do is strip the quotes.
|
||||
v = repr(v)[1:-1]
|
||||
# Escape double-quote.
|
||||
atoms[k] = v.replace('"', '\\"')
|
||||
|
||||
try:
|
||||
self.access_log.log(
|
||||
logging.INFO, self.access_log_format % atoms)
|
||||
except:
|
||||
self(traceback=True)
|
||||
|
||||
def time(self):
|
||||
"""Return now() in Apache Common Log Format (no timezone)."""
|
||||
now = datetime.datetime.now()
|
||||
|
@ -242,51 +306,49 @@ class LogManager(object):
|
|||
month = monthnames[now.month - 1].capitalize()
|
||||
return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
|
||||
(now.day, month, now.year, now.hour, now.minute, now.second))
|
||||
|
||||
|
||||
def _get_builtin_handler(self, log, key):
|
||||
for h in log.handlers:
|
||||
if getattr(h, "_cpbuiltin", None) == key:
|
||||
return h
|
||||
|
||||
|
||||
|
||||
# ------------------------- Screen handlers ------------------------- #
|
||||
|
||||
def _set_screen_handler(self, log, enable, stream=None):
|
||||
h = self._get_builtin_handler(log, "screen")
|
||||
if enable:
|
||||
if not h:
|
||||
if stream is None:
|
||||
stream=sys.stderr
|
||||
stream = sys.stderr
|
||||
h = logging.StreamHandler(stream)
|
||||
h.setFormatter(logfmt)
|
||||
h._cpbuiltin = "screen"
|
||||
log.addHandler(h)
|
||||
elif h:
|
||||
log.handlers.remove(h)
|
||||
|
||||
|
||||
def _get_screen(self):
|
||||
h = self._get_builtin_handler
|
||||
has_h = h(self.error_log, "screen") or h(self.access_log, "screen")
|
||||
return bool(has_h)
|
||||
|
||||
|
||||
def _set_screen(self, newvalue):
|
||||
self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
|
||||
self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
|
||||
screen = property(_get_screen, _set_screen,
|
||||
doc="""Turn stderr/stdout logging on or off.
|
||||
|
||||
doc="""Turn stderr/stdout logging on or off.
|
||||
|
||||
If you set this to True, it'll add the appropriate StreamHandler for
|
||||
you. If you set it to False, it will remove the handler.
|
||||
""")
|
||||
|
||||
|
||||
# -------------------------- File handlers -------------------------- #
|
||||
|
||||
|
||||
def _add_builtin_file_handler(self, log, fname):
|
||||
h = logging.FileHandler(fname)
|
||||
h.setFormatter(logfmt)
|
||||
h._cpbuiltin = "file"
|
||||
log.addHandler(h)
|
||||
|
||||
|
||||
def _set_file_handler(self, log, filename):
|
||||
h = self._get_builtin_handler(log, "file")
|
||||
if filename:
|
||||
|
@ -301,37 +363,39 @@ class LogManager(object):
|
|||
if h:
|
||||
h.close()
|
||||
log.handlers.remove(h)
|
||||
|
||||
|
||||
def _get_error_file(self):
|
||||
h = self._get_builtin_handler(self.error_log, "file")
|
||||
if h:
|
||||
return h.baseFilename
|
||||
return ''
|
||||
|
||||
def _set_error_file(self, newvalue):
|
||||
self._set_file_handler(self.error_log, newvalue)
|
||||
error_file = property(_get_error_file, _set_error_file,
|
||||
doc="""The filename for self.error_log.
|
||||
|
||||
doc="""The filename for self.error_log.
|
||||
|
||||
If you set this to a string, it'll add the appropriate FileHandler for
|
||||
you. If you set it to ``None`` or ``''``, it will remove the handler.
|
||||
""")
|
||||
|
||||
|
||||
def _get_access_file(self):
|
||||
h = self._get_builtin_handler(self.access_log, "file")
|
||||
if h:
|
||||
return h.baseFilename
|
||||
return ''
|
||||
|
||||
def _set_access_file(self, newvalue):
|
||||
self._set_file_handler(self.access_log, newvalue)
|
||||
access_file = property(_get_access_file, _set_access_file,
|
||||
doc="""The filename for self.access_log.
|
||||
|
||||
doc="""The filename for self.access_log.
|
||||
|
||||
If you set this to a string, it'll add the appropriate FileHandler for
|
||||
you. If you set it to ``None`` or ``''``, it will remove the handler.
|
||||
""")
|
||||
|
||||
|
||||
# ------------------------- WSGI handlers ------------------------- #
|
||||
|
||||
|
||||
def _set_wsgi_handler(self, log, enable):
|
||||
h = self._get_builtin_handler(log, "wsgi")
|
||||
if enable:
|
||||
|
@ -342,15 +406,15 @@ class LogManager(object):
|
|||
log.addHandler(h)
|
||||
elif h:
|
||||
log.handlers.remove(h)
|
||||
|
||||
|
||||
def _get_wsgi(self):
|
||||
return bool(self._get_builtin_handler(self.error_log, "wsgi"))
|
||||
|
||||
|
||||
def _set_wsgi(self, newvalue):
|
||||
self._set_wsgi_handler(self.error_log, newvalue)
|
||||
wsgi = property(_get_wsgi, _set_wsgi,
|
||||
doc="""Write errors to wsgi.errors.
|
||||
|
||||
doc="""Write errors to wsgi.errors.
|
||||
|
||||
If you set this to True, it'll add the appropriate
|
||||
:class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
|
||||
(which writes errors to ``wsgi.errors``).
|
||||
|
@ -359,8 +423,9 @@ class LogManager(object):
|
|||
|
||||
|
||||
class WSGIErrorHandler(logging.Handler):
|
||||
|
||||
"A handler class which writes logging records to environ['wsgi.errors']."
|
||||
|
||||
|
||||
def flush(self):
|
||||
"""Flushes the stream."""
|
||||
try:
|
||||
|
@ -369,7 +434,7 @@ class WSGIErrorHandler(logging.Handler):
|
|||
pass
|
||||
else:
|
||||
stream.flush()
|
||||
|
||||
|
||||
def emit(self, record):
|
||||
"""Emit a record."""
|
||||
try:
|
||||
|
@ -381,7 +446,8 @@ class WSGIErrorHandler(logging.Handler):
|
|||
msg = self.format(record)
|
||||
fs = "%s\n"
|
||||
import types
|
||||
if not hasattr(types, "UnicodeType"): #if no unicode support...
|
||||
# if no unicode support...
|
||||
if not hasattr(types, "UnicodeType"):
|
||||
stream.write(fs % msg)
|
||||
else:
|
||||
try:
|
||||
|
|
|
@ -35,12 +35,12 @@ Listen 8080
|
|||
LoadModule python_module /usr/lib/apache2/modules/mod_python.so
|
||||
|
||||
<Location "/">
|
||||
PythonPath "sys.path+['/path/to/my/application']"
|
||||
SetHandler python-program
|
||||
PythonHandler cherrypy._cpmodpy::handler
|
||||
PythonOption cherrypy.setup myapp::setup_server
|
||||
PythonDebug On
|
||||
</Location>
|
||||
PythonPath "sys.path+['/path/to/my/application']"
|
||||
SetHandler python-program
|
||||
PythonHandler cherrypy._cpmodpy::handler
|
||||
PythonOption cherrypy.setup myapp::setup_server
|
||||
PythonDebug On
|
||||
</Location>
|
||||
# End
|
||||
|
||||
The actual path to your mod_python.so is dependent on your
|
||||
|
@ -67,11 +67,11 @@ from cherrypy.lib import httputil
|
|||
# ------------------------------ Request-handling
|
||||
|
||||
|
||||
|
||||
def setup(req):
|
||||
from mod_python import apache
|
||||
|
||||
# Run any setup functions defined by a "PythonOption cherrypy.setup" directive.
|
||||
|
||||
# Run any setup functions defined by a "PythonOption cherrypy.setup"
|
||||
# directive.
|
||||
options = req.get_options()
|
||||
if 'cherrypy.setup' in options:
|
||||
for function in options['cherrypy.setup'].split():
|
||||
|
@ -83,12 +83,12 @@ def setup(req):
|
|||
mod = __import__(modname, globals(), locals(), [fname])
|
||||
func = getattr(mod, fname)
|
||||
func()
|
||||
|
||||
|
||||
cherrypy.config.update({'log.screen': False,
|
||||
"tools.ignore_headers.on": True,
|
||||
"tools.ignore_headers.headers": ['Range'],
|
||||
})
|
||||
|
||||
|
||||
engine = cherrypy.engine
|
||||
if hasattr(engine, "signal_handler"):
|
||||
engine.signal_handler.unsubscribe()
|
||||
|
@ -96,7 +96,7 @@ def setup(req):
|
|||
engine.console_control_handler.unsubscribe()
|
||||
engine.autoreload.unsubscribe()
|
||||
cherrypy.server.unsubscribe()
|
||||
|
||||
|
||||
def _log(msg, level):
|
||||
newlevel = apache.APLOG_ERR
|
||||
if logging.DEBUG >= level:
|
||||
|
@ -106,13 +106,13 @@ def setup(req):
|
|||
elif logging.WARNING >= level:
|
||||
newlevel = apache.APLOG_WARNING
|
||||
# On Windows, req.server is required or the msg will vanish. See
|
||||
# http://www.modpython.org/pipermail/mod_python/2003-October/014291.html.
|
||||
# http://www.modpython.org/pipermail/mod_python/2003-October/014291.html
|
||||
# Also, "When server is not specified...LogLevel does not apply..."
|
||||
apache.log_error(msg, newlevel, req.server)
|
||||
engine.subscribe('log', _log)
|
||||
|
||||
|
||||
engine.start()
|
||||
|
||||
|
||||
def cherrypy_cleanup(data):
|
||||
engine.exit()
|
||||
try:
|
||||
|
@ -124,6 +124,7 @@ def setup(req):
|
|||
|
||||
class _ReadOnlyRequest:
|
||||
expose = ('read', 'readline', 'readlines')
|
||||
|
||||
def __init__(self, req):
|
||||
for method in self.expose:
|
||||
self.__dict__[method] = getattr(req, method)
|
||||
|
@ -132,6 +133,8 @@ class _ReadOnlyRequest:
|
|||
recursive = False
|
||||
|
||||
_isSetUp = False
|
||||
|
||||
|
||||
def handler(req):
|
||||
from mod_python import apache
|
||||
try:
|
||||
|
@ -139,16 +142,18 @@ def handler(req):
|
|||
if not _isSetUp:
|
||||
setup(req)
|
||||
_isSetUp = True
|
||||
|
||||
|
||||
# Obtain a Request object from CherryPy
|
||||
local = req.connection.local_addr
|
||||
local = httputil.Host(local[0], local[1], req.connection.local_host or "")
|
||||
local = httputil.Host(
|
||||
local[0], local[1], req.connection.local_host or "")
|
||||
remote = req.connection.remote_addr
|
||||
remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "")
|
||||
|
||||
remote = httputil.Host(
|
||||
remote[0], remote[1], req.connection.remote_host or "")
|
||||
|
||||
scheme = req.parsed_uri[0] or 'http'
|
||||
req.get_basic_auth_pw()
|
||||
|
||||
|
||||
try:
|
||||
# apache.mpm_query only became available in mod_python 3.1
|
||||
q = apache.mpm_query
|
||||
|
@ -158,7 +163,7 @@ def handler(req):
|
|||
bad_value = ("You must provide a PythonOption '%s', "
|
||||
"either 'on' or 'off', when running a version "
|
||||
"of mod_python < 3.1")
|
||||
|
||||
|
||||
threaded = options.get('multithread', '').lower()
|
||||
if threaded == 'on':
|
||||
threaded = True
|
||||
|
@ -166,7 +171,7 @@ def handler(req):
|
|||
threaded = False
|
||||
else:
|
||||
raise ValueError(bad_value % "multithread")
|
||||
|
||||
|
||||
forked = options.get('multiprocess', '').lower()
|
||||
if forked == 'on':
|
||||
forked = True
|
||||
|
@ -174,7 +179,7 @@ def handler(req):
|
|||
forked = False
|
||||
else:
|
||||
raise ValueError(bad_value % "multiprocess")
|
||||
|
||||
|
||||
sn = cherrypy.tree.script_name(req.uri or "/")
|
||||
if sn is None:
|
||||
send_response(req, '404 Not Found', [], '')
|
||||
|
@ -187,7 +192,7 @@ def handler(req):
|
|||
headers = copyitems(req.headers_in)
|
||||
rfile = _ReadOnlyRequest(req)
|
||||
prev = None
|
||||
|
||||
|
||||
try:
|
||||
redirections = []
|
||||
while True:
|
||||
|
@ -198,7 +203,7 @@ def handler(req):
|
|||
request.multiprocess = bool(forked)
|
||||
request.app = app
|
||||
request.prev = prev
|
||||
|
||||
|
||||
# Run the CherryPy Request object and obtain the response
|
||||
try:
|
||||
request.run(method, path, qs, reqproto, headers, rfile)
|
||||
|
@ -207,25 +212,28 @@ def handler(req):
|
|||
ir = sys.exc_info()[1]
|
||||
app.release_serving()
|
||||
prev = request
|
||||
|
||||
|
||||
if not recursive:
|
||||
if ir.path in redirections:
|
||||
raise RuntimeError("InternalRedirector visited the "
|
||||
"same URL twice: %r" % ir.path)
|
||||
raise RuntimeError(
|
||||
"InternalRedirector visited the same URL "
|
||||
"twice: %r" % ir.path)
|
||||
else:
|
||||
# Add the *previous* path_info + qs to redirections.
|
||||
# Add the *previous* path_info + qs to
|
||||
# redirections.
|
||||
if qs:
|
||||
qs = "?" + qs
|
||||
redirections.append(sn + path + qs)
|
||||
|
||||
|
||||
# Munge environment and try again.
|
||||
method = "GET"
|
||||
path = ir.path
|
||||
qs = ir.query_string
|
||||
rfile = BytesIO()
|
||||
|
||||
send_response(req, response.status, response.header_list,
|
||||
response.body, response.stream)
|
||||
|
||||
send_response(
|
||||
req, response.output_status, response.header_list,
|
||||
response.body, response.stream)
|
||||
finally:
|
||||
app.release_serving()
|
||||
except:
|
||||
|
@ -239,7 +247,7 @@ def handler(req):
|
|||
def send_response(req, status, headers, body, stream=False):
|
||||
# Set response status
|
||||
req.status = int(status[:3])
|
||||
|
||||
|
||||
# Set response headers
|
||||
req.content_type = "text/plain"
|
||||
for header, value in headers:
|
||||
|
@ -247,11 +255,11 @@ def send_response(req, status, headers, body, stream=False):
|
|||
req.content_type = value
|
||||
continue
|
||||
req.headers_out.add(header, value)
|
||||
|
||||
|
||||
if stream:
|
||||
# Flush now so the status and headers are sent immediately.
|
||||
req.flush()
|
||||
|
||||
|
||||
# Set response body
|
||||
if isinstance(body, basestring):
|
||||
req.write(body)
|
||||
|
@ -260,21 +268,34 @@ def send_response(req, status, headers, body, stream=False):
|
|||
req.write(seg)
|
||||
|
||||
|
||||
|
||||
# --------------- Startup tools for CherryPy + mod_python --------------- #
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
def popen(fullcmd):
|
||||
p = subprocess.Popen(fullcmd, shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
close_fds=True)
|
||||
return p.stdout
|
||||
except ImportError:
|
||||
def popen(fullcmd):
|
||||
pipein, pipeout = os.popen4(fullcmd)
|
||||
return pipeout
|
||||
|
||||
|
||||
def read_process(cmd, args=""):
|
||||
fullcmd = "%s %s" % (cmd, args)
|
||||
pipein, pipeout = os.popen4(fullcmd)
|
||||
pipeout = popen(fullcmd)
|
||||
try:
|
||||
firstline = pipeout.readline()
|
||||
if (re.search(ntob("(not recognized|No such file|not found)"), firstline,
|
||||
re.IGNORECASE)):
|
||||
cmd_not_found = re.search(
|
||||
ntob("(not recognized|No such file|not found)"),
|
||||
firstline,
|
||||
re.IGNORECASE
|
||||
)
|
||||
if cmd_not_found:
|
||||
raise IOError('%s must be on your system path.' % cmd)
|
||||
output = firstline + pipeout.read()
|
||||
finally:
|
||||
|
@ -283,7 +304,7 @@ def read_process(cmd, args=""):
|
|||
|
||||
|
||||
class ModPythonServer(object):
|
||||
|
||||
|
||||
template = """
|
||||
# Apache2 server configuration file for running CherryPy with mod_python.
|
||||
|
||||
|
@ -298,7 +319,7 @@ LoadModule python_module modules/mod_python.so
|
|||
%(opts)s
|
||||
</Location>
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, loc="/", port=80, opts=None, apache_path="apache",
|
||||
handler="cherrypy._cpmodpy::handler"):
|
||||
self.loc = loc
|
||||
|
@ -306,7 +327,7 @@ LoadModule python_module modules/mod_python.so
|
|||
self.opts = opts
|
||||
self.apache_path = apache_path
|
||||
self.handler = handler
|
||||
|
||||
|
||||
def start(self):
|
||||
opts = "".join([" PythonOption %s %s\n" % (k, v)
|
||||
for k, v in self.opts])
|
||||
|
@ -315,19 +336,18 @@ LoadModule python_module modules/mod_python.so
|
|||
"opts": opts,
|
||||
"handler": self.handler,
|
||||
}
|
||||
|
||||
|
||||
mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
|
||||
f = open(mpconf, 'wb')
|
||||
try:
|
||||
f.write(conf_data)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
|
||||
response = read_process(self.apache_path, "-k start -f %s" % mpconf)
|
||||
self.ready = True
|
||||
return response
|
||||
|
||||
|
||||
def stop(self):
|
||||
os.popen("apache -k stop")
|
||||
self.ready = False
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ from cherrypy import wsgiserver
|
|||
|
||||
|
||||
class NativeGateway(wsgiserver.Gateway):
|
||||
|
||||
|
||||
recursive = False
|
||||
|
||||
|
||||
def respond(self):
|
||||
req = self.req
|
||||
try:
|
||||
|
@ -22,7 +22,7 @@ class NativeGateway(wsgiserver.Gateway):
|
|||
local = httputil.Host(local[0], local[1], "")
|
||||
remote = req.conn.remote_addr, req.conn.remote_port
|
||||
remote = httputil.Host(remote[0], remote[1], "")
|
||||
|
||||
|
||||
scheme = req.scheme
|
||||
sn = cherrypy.tree.script_name(req.uri or "/")
|
||||
if sn is None:
|
||||
|
@ -35,7 +35,7 @@ class NativeGateway(wsgiserver.Gateway):
|
|||
headers = req.inheaders.items()
|
||||
rfile = req.rfile
|
||||
prev = None
|
||||
|
||||
|
||||
try:
|
||||
redirections = []
|
||||
while True:
|
||||
|
@ -45,32 +45,36 @@ class NativeGateway(wsgiserver.Gateway):
|
|||
request.multiprocess = False
|
||||
request.app = app
|
||||
request.prev = prev
|
||||
|
||||
# Run the CherryPy Request object and obtain the response
|
||||
|
||||
# Run the CherryPy Request object and obtain the
|
||||
# response
|
||||
try:
|
||||
request.run(method, path, qs, req.request_protocol, headers, rfile)
|
||||
request.run(method, path, qs,
|
||||
req.request_protocol, headers, rfile)
|
||||
break
|
||||
except cherrypy.InternalRedirect:
|
||||
ir = sys.exc_info()[1]
|
||||
app.release_serving()
|
||||
prev = request
|
||||
|
||||
|
||||
if not self.recursive:
|
||||
if ir.path in redirections:
|
||||
raise RuntimeError("InternalRedirector visited the "
|
||||
"same URL twice: %r" % ir.path)
|
||||
raise RuntimeError(
|
||||
"InternalRedirector visited the same "
|
||||
"URL twice: %r" % ir.path)
|
||||
else:
|
||||
# Add the *previous* path_info + qs to redirections.
|
||||
# Add the *previous* path_info + qs to
|
||||
# redirections.
|
||||
if qs:
|
||||
qs = "?" + qs
|
||||
redirections.append(sn + path + qs)
|
||||
|
||||
|
||||
# Munge environment and try again.
|
||||
method = "GET"
|
||||
path = ir.path
|
||||
qs = ir.query_string
|
||||
rfile = BytesIO()
|
||||
|
||||
|
||||
self.send_response(
|
||||
response.output_status, response.header_list,
|
||||
response.body)
|
||||
|
@ -78,59 +82,62 @@ class NativeGateway(wsgiserver.Gateway):
|
|||
app.release_serving()
|
||||
except:
|
||||
tb = format_exc()
|
||||
#print tb
|
||||
# print tb
|
||||
cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
|
||||
s, h, b = bare_error()
|
||||
self.send_response(s, h, b)
|
||||
|
||||
|
||||
def send_response(self, status, headers, body):
|
||||
req = self.req
|
||||
|
||||
|
||||
# Set response status
|
||||
req.status = str(status or "500 Server Error")
|
||||
|
||||
|
||||
# Set response headers
|
||||
for header, value in headers:
|
||||
req.outheaders.append((header, value))
|
||||
if (req.ready and not req.sent_headers):
|
||||
req.sent_headers = True
|
||||
req.send_headers()
|
||||
|
||||
|
||||
# Set response body
|
||||
for seg in body:
|
||||
req.write(seg)
|
||||
|
||||
|
||||
class CPHTTPServer(wsgiserver.HTTPServer):
|
||||
|
||||
"""Wrapper for wsgiserver.HTTPServer.
|
||||
|
||||
|
||||
wsgiserver has been designed to not reference CherryPy in any way,
|
||||
so that it can be used in other frameworks and applications.
|
||||
Therefore, we wrap it here, so we can apply some attributes
|
||||
from config -> cherrypy.server -> HTTPServer.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, server_adapter=cherrypy.server):
|
||||
self.server_adapter = server_adapter
|
||||
|
||||
|
||||
server_name = (self.server_adapter.socket_host or
|
||||
self.server_adapter.socket_file or
|
||||
None)
|
||||
|
||||
|
||||
wsgiserver.HTTPServer.__init__(
|
||||
self, server_adapter.bind_addr, NativeGateway,
|
||||
minthreads=server_adapter.thread_pool,
|
||||
maxthreads=server_adapter.thread_pool_max,
|
||||
server_name=server_name)
|
||||
|
||||
self.max_request_header_size = self.server_adapter.max_request_header_size or 0
|
||||
self.max_request_body_size = self.server_adapter.max_request_body_size or 0
|
||||
|
||||
self.max_request_header_size = (
|
||||
self.server_adapter.max_request_header_size or 0)
|
||||
self.max_request_body_size = (
|
||||
self.server_adapter.max_request_body_size or 0)
|
||||
self.request_queue_size = self.server_adapter.socket_queue_size
|
||||
self.timeout = self.server_adapter.socket_timeout
|
||||
self.shutdown_timeout = self.server_adapter.shutdown_timeout
|
||||
self.protocol = self.server_adapter.protocol_version
|
||||
self.nodelay = self.server_adapter.nodelay
|
||||
|
||||
|
||||
ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
|
||||
if self.server_adapter.ssl_context:
|
||||
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
|
||||
|
@ -145,5 +152,3 @@ class CPHTTPServer(wsgiserver.HTTPServer):
|
|||
self.server_adapter.ssl_certificate,
|
||||
self.server_adapter.ssl_private_key,
|
||||
self.server_adapter.ssl_certificate_chain)
|
||||
|
||||
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
.. versionadded:: 3.2
|
||||
|
||||
Application authors have complete control over the parsing of HTTP request
|
||||
entities. In short, :attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>`
|
||||
is now always set to an instance of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>`,
|
||||
entities. In short,
|
||||
:attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>`
|
||||
is now always set to an instance of
|
||||
:class:`RequestBody<cherrypy._cpreqbody.RequestBody>`,
|
||||
and *that* class is a subclass of :class:`Entity<cherrypy._cpreqbody.Entity>`.
|
||||
|
||||
When an HTTP request includes an entity body, it is often desirable to
|
||||
|
@ -21,9 +23,9 @@ key to look up a value in the
|
|||
:attr:`request.body.processors<cherrypy._cpreqbody.Entity.processors>` dict.
|
||||
If the full media
|
||||
type is not found, then the major type is tried; for example, if no processor
|
||||
is found for the 'image/jpeg' type, then we look for a processor for the 'image'
|
||||
types altogether. If neither the full type nor the major type has a matching
|
||||
processor, then a default processor is used
|
||||
is found for the 'image/jpeg' type, then we look for a processor for the
|
||||
'image' types altogether. If neither the full type nor the major type has a
|
||||
matching processor, then a default processor is used
|
||||
(:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>`). For most
|
||||
types, this means no processing is done, and the body is left unread as a
|
||||
raw byte stream. Processors are configurable in an 'on_start_resource' hook.
|
||||
|
@ -53,7 +55,7 @@ Custom Processors
|
|||
|
||||
You can add your own processors for any specific or major MIME type. Simply add
|
||||
it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a
|
||||
hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
|
||||
hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
|
||||
Here's the built-in JSON tool for an example::
|
||||
|
||||
def json_in(force=True, debug=False):
|
||||
|
@ -62,7 +64,7 @@ Here's the built-in JSON tool for an example::
|
|||
\"""Read application/json data into request.json.\"""
|
||||
if not entity.headers.get("Content-Length", ""):
|
||||
raise cherrypy.HTTPError(411)
|
||||
|
||||
|
||||
body = entity.fp.read()
|
||||
try:
|
||||
request.json = json_decode(body)
|
||||
|
@ -74,44 +76,67 @@ Here's the built-in JSON tool for an example::
|
|||
415, 'Expected an application/json content type')
|
||||
request.body.processors['application/json'] = json_processor
|
||||
|
||||
We begin by defining a new ``json_processor`` function to stick in the ``processors``
|
||||
dictionary. All processor functions take a single argument, the ``Entity`` instance
|
||||
they are to process. It will be called whenever a request is received (for those
|
||||
URI's where the tool is turned on) which has a ``Content-Type`` of
|
||||
"application/json".
|
||||
We begin by defining a new ``json_processor`` function to stick in the
|
||||
``processors`` dictionary. All processor functions take a single argument,
|
||||
the ``Entity`` instance they are to process. It will be called whenever a
|
||||
request is received (for those URI's where the tool is turned on) which
|
||||
has a ``Content-Type`` of "application/json".
|
||||
|
||||
First, it checks for a valid ``Content-Length`` (raising 411 if not valid), then
|
||||
reads the remaining bytes on the socket. The ``fp`` object knows its own length, so
|
||||
it won't hang waiting for data that never arrives. It will return when all data
|
||||
has been read. Then, we decode those bytes using Python's built-in ``json`` module,
|
||||
and stick the decoded result onto ``request.json`` . If it cannot be decoded, we
|
||||
raise 400.
|
||||
First, it checks for a valid ``Content-Length`` (raising 411 if not valid),
|
||||
then reads the remaining bytes on the socket. The ``fp`` object knows its
|
||||
own length, so it won't hang waiting for data that never arrives. It will
|
||||
return when all data has been read. Then, we decode those bytes using
|
||||
Python's built-in ``json`` module, and stick the decoded result onto
|
||||
``request.json`` . If it cannot be decoded, we raise 400.
|
||||
|
||||
If the "force" argument is True (the default), the ``Tool`` clears the ``processors``
|
||||
dict so that request entities of other ``Content-Types`` aren't parsed at all. Since
|
||||
there's no entry for those invalid MIME types, the ``default_proc`` method of ``cherrypy.request.body``
|
||||
is called. But this does nothing by default (usually to provide the page handler an opportunity to handle it.)
|
||||
But in our case, we want to raise 415, so we replace ``request.body.default_proc``
|
||||
If the "force" argument is True (the default), the ``Tool`` clears the
|
||||
``processors`` dict so that request entities of other ``Content-Types``
|
||||
aren't parsed at all. Since there's no entry for those invalid MIME
|
||||
types, the ``default_proc`` method of ``cherrypy.request.body`` is
|
||||
called. But this does nothing by default (usually to provide the page
|
||||
handler an opportunity to handle it.)
|
||||
But in our case, we want to raise 415, so we replace
|
||||
``request.body.default_proc``
|
||||
with the error (``HTTPError`` instances, when called, raise themselves).
|
||||
|
||||
If we were defining a custom processor, we can do so without making a ``Tool``. Just add the config entry::
|
||||
If we were defining a custom processor, we can do so without making a ``Tool``.
|
||||
Just add the config entry::
|
||||
|
||||
request.body.processors = {'application/json': json_processor}
|
||||
|
||||
Note that you can only replace the ``processors`` dict wholesale this way, not update the existing one.
|
||||
Note that you can only replace the ``processors`` dict wholesale this way,
|
||||
not update the existing one.
|
||||
"""
|
||||
|
||||
try:
|
||||
from io import DEFAULT_BUFFER_SIZE
|
||||
except ImportError:
|
||||
DEFAULT_BUFFER_SIZE = 8192
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from urllib import unquote_plus
|
||||
try:
|
||||
from urllib import unquote_plus
|
||||
except ImportError:
|
||||
def unquote_plus(bs):
|
||||
"""Bytes version of urllib.parse.unquote_plus."""
|
||||
bs = bs.replace(ntob('+'), ntob(' '))
|
||||
atoms = bs.split(ntob('%'))
|
||||
for i in range(1, len(atoms)):
|
||||
item = atoms[i]
|
||||
try:
|
||||
pct = int(item[:2], 16)
|
||||
atoms[i] = bytes([pct]) + item[2:]
|
||||
except ValueError:
|
||||
pass
|
||||
return ntob('').join(atoms)
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import basestring, ntob, ntou
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
|
||||
# -------------------------------- Processors -------------------------------- #
|
||||
# ------------------------------- Processors -------------------------------- #
|
||||
|
||||
def process_urlencoded(entity):
|
||||
"""Read application/x-www-form-urlencoded data into entity.params."""
|
||||
|
@ -123,14 +148,14 @@ def process_urlencoded(entity):
|
|||
for pair in aparam.split(ntob(';')):
|
||||
if not pair:
|
||||
continue
|
||||
|
||||
|
||||
atoms = pair.split(ntob('='), 1)
|
||||
if len(atoms) == 1:
|
||||
atoms.append(ntob(''))
|
||||
|
||||
|
||||
key = unquote_plus(atoms[0]).decode(charset)
|
||||
value = unquote_plus(atoms[1]).decode(charset)
|
||||
|
||||
|
||||
if key in params:
|
||||
if not isinstance(params[key], list):
|
||||
params[key] = [params[key]]
|
||||
|
@ -146,7 +171,7 @@ def process_urlencoded(entity):
|
|||
raise cherrypy.HTTPError(
|
||||
400, "The request entity could not be decoded. The following "
|
||||
"charsets were attempted: %s" % repr(entity.attempt_charsets))
|
||||
|
||||
|
||||
# Now that all values have been successfully parsed and decoded,
|
||||
# apply them to the entity.params dict.
|
||||
for key, value in params.items():
|
||||
|
@ -167,22 +192,22 @@ def process_multipart(entity):
|
|||
# is often necessary to enclose the boundary parameter values in quotes
|
||||
# on the Content-type line"
|
||||
ib = entity.content_type.params['boundary'].strip('"')
|
||||
|
||||
|
||||
if not re.match("^[ -~]{0,200}[!-~]$", ib):
|
||||
raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
|
||||
|
||||
|
||||
ib = ('--' + ib).encode('ascii')
|
||||
|
||||
|
||||
# Find the first marker
|
||||
while True:
|
||||
b = entity.readline()
|
||||
if not b:
|
||||
return
|
||||
|
||||
|
||||
b = b.strip()
|
||||
if b == ib:
|
||||
break
|
||||
|
||||
|
||||
# Read all parts
|
||||
while True:
|
||||
part = entity.part_class.from_fp(entity.fp, ib)
|
||||
|
@ -191,10 +216,12 @@ def process_multipart(entity):
|
|||
if part.fp.done:
|
||||
break
|
||||
|
||||
|
||||
def process_multipart_form_data(entity):
|
||||
"""Read all multipart/form-data parts into entity.parts or entity.params."""
|
||||
"""Read all multipart/form-data parts into entity.parts or entity.params.
|
||||
"""
|
||||
process_multipart(entity)
|
||||
|
||||
|
||||
kept_parts = []
|
||||
for part in entity.parts:
|
||||
if part.name is None:
|
||||
|
@ -207,28 +234,29 @@ def process_multipart_form_data(entity):
|
|||
# It's a file upload. Retain the whole part so consumer code
|
||||
# has access to its .file and .filename attributes.
|
||||
value = part
|
||||
|
||||
|
||||
if part.name in entity.params:
|
||||
if not isinstance(entity.params[part.name], list):
|
||||
entity.params[part.name] = [entity.params[part.name]]
|
||||
entity.params[part.name].append(value)
|
||||
else:
|
||||
entity.params[part.name] = value
|
||||
|
||||
|
||||
entity.parts = kept_parts
|
||||
|
||||
|
||||
def _old_process_multipart(entity):
|
||||
"""The behavior of 3.2 and lower. Deprecated and will be changed in 3.3."""
|
||||
process_multipart(entity)
|
||||
|
||||
|
||||
params = entity.params
|
||||
|
||||
|
||||
for part in entity.parts:
|
||||
if part.name is None:
|
||||
key = ntou('parts')
|
||||
else:
|
||||
key = part.name
|
||||
|
||||
|
||||
if part.filename is None:
|
||||
# It's a regular field
|
||||
value = part.fullvalue()
|
||||
|
@ -236,7 +264,7 @@ def _old_process_multipart(entity):
|
|||
# It's a file upload. Retain the whole part so consumer code
|
||||
# has access to its .file and .filename attributes.
|
||||
value = part
|
||||
|
||||
|
||||
if key in params:
|
||||
if not isinstance(params[key], list):
|
||||
params[key] = [params[key]]
|
||||
|
@ -245,31 +273,32 @@ def _old_process_multipart(entity):
|
|||
params[key] = value
|
||||
|
||||
|
||||
|
||||
# --------------------------------- Entities --------------------------------- #
|
||||
|
||||
|
||||
# -------------------------------- Entities --------------------------------- #
|
||||
class Entity(object):
|
||||
|
||||
"""An HTTP request body, or MIME multipart body.
|
||||
|
||||
|
||||
This class collects information about the HTTP request entity. When a
|
||||
given entity is of MIME type "multipart", each part is parsed into its own
|
||||
Entity instance, and the set of parts stored in
|
||||
:attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`.
|
||||
|
||||
|
||||
Between the ``before_request_body`` and ``before_handler`` tools, CherryPy
|
||||
tries to process the request body (if any) by calling
|
||||
:func:`request.body.process<cherrypy._cpreqbody.RequestBody.process`.
|
||||
This uses the ``content_type`` of the Entity to look up a suitable processor
|
||||
in :attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`, a dict.
|
||||
:func:`request.body.process<cherrypy._cpreqbody.RequestBody.process>`.
|
||||
This uses the ``content_type`` of the Entity to look up a suitable
|
||||
processor in
|
||||
:attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`,
|
||||
a dict.
|
||||
If a matching processor cannot be found for the complete Content-Type,
|
||||
it tries again using the major type. For example, if a request with an
|
||||
entity of type "image/jpeg" arrives, but no processor can be found for
|
||||
that complete type, then one is sought for the major type "image". If a
|
||||
processor is still not found, then the
|
||||
:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method of the
|
||||
Entity is called (which does nothing by default; you can override this too).
|
||||
|
||||
:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method
|
||||
of the Entity is called (which does nothing by default; you can
|
||||
override this too).
|
||||
|
||||
CherryPy includes processors for the "application/x-www-form-urlencoded"
|
||||
type, the "multipart/form-data" type, and the "multipart" major type.
|
||||
CherryPy 3.2 processes these types almost exactly as older versions.
|
||||
|
@ -280,43 +309,43 @@ class Entity(object):
|
|||
case it will have ``file`` and ``filename`` attributes, or possibly a
|
||||
``value`` attribute). Each Part is itself a subclass of
|
||||
Entity, and has its own ``process`` method and ``processors`` dict.
|
||||
|
||||
|
||||
There is a separate processor for the "multipart" major type which is more
|
||||
flexible, and simply stores all multipart parts in
|
||||
:attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
|
||||
enable it with::
|
||||
|
||||
|
||||
cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart
|
||||
|
||||
|
||||
in an ``on_start_resource`` tool.
|
||||
"""
|
||||
|
||||
|
||||
# http://tools.ietf.org/html/rfc2046#section-4.1.2:
|
||||
# "The default character set, which must be assumed in the
|
||||
# absence of a charset parameter, is US-ASCII."
|
||||
# However, many browsers send data in utf-8 with no charset.
|
||||
attempt_charsets = ['utf-8']
|
||||
"""A list of strings, each of which should be a known encoding.
|
||||
|
||||
|
||||
When the Content-Type of the request body warrants it, each of the given
|
||||
encodings will be tried in order. The first one to successfully decode the
|
||||
entity without raising an error is stored as
|
||||
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
|
||||
to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
|
||||
`HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
|
||||
to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
|
||||
`HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
|
||||
but ``['us-ascii', 'utf-8']`` for multipart parts.
|
||||
"""
|
||||
|
||||
|
||||
charset = None
|
||||
"""The successful decoding; see "attempt_charsets" above."""
|
||||
|
||||
|
||||
content_type = None
|
||||
"""The value of the Content-Type request header.
|
||||
|
||||
|
||||
If the Entity is part of a multipart payload, this will be the Content-Type
|
||||
given in the MIME headers for this part.
|
||||
"""
|
||||
|
||||
|
||||
default_content_type = 'application/x-www-form-urlencoded'
|
||||
"""This defines a default ``Content-Type`` to use if no Content-Type header
|
||||
is given. The empty string is used for RequestBody, which results in the
|
||||
|
@ -326,26 +355,26 @@ class Entity(object):
|
|||
declares that a part with no Content-Type defaults to "text/plain"
|
||||
(see :class:`Part<cherrypy._cpreqbody.Part>`).
|
||||
"""
|
||||
|
||||
|
||||
filename = None
|
||||
"""The ``Content-Disposition.filename`` header, if available."""
|
||||
|
||||
|
||||
fp = None
|
||||
"""The readable socket file object."""
|
||||
|
||||
|
||||
headers = None
|
||||
"""A dict of request/multipart header names and values.
|
||||
|
||||
|
||||
This is a copy of the ``request.headers`` for the ``request.body``;
|
||||
for multipart parts, it is the set of headers for that part.
|
||||
"""
|
||||
|
||||
|
||||
length = None
|
||||
"""The value of the ``Content-Length`` header, if provided."""
|
||||
|
||||
|
||||
name = None
|
||||
"""The "name" parameter of the ``Content-Disposition`` header, if any."""
|
||||
|
||||
|
||||
params = None
|
||||
"""
|
||||
If the request Content-Type is 'application/x-www-form-urlencoded' or
|
||||
|
@ -355,39 +384,40 @@ class Entity(object):
|
|||
can be sent with various HTTP method verbs). This value is set between
|
||||
the 'before_request_body' and 'before_handler' hooks (assuming that
|
||||
process_request_body is True)."""
|
||||
|
||||
|
||||
processors = {'application/x-www-form-urlencoded': process_urlencoded,
|
||||
'multipart/form-data': process_multipart_form_data,
|
||||
'multipart': process_multipart,
|
||||
}
|
||||
"""A dict of Content-Type names to processor methods."""
|
||||
|
||||
|
||||
parts = None
|
||||
"""A list of Part instances if ``Content-Type`` is of major type "multipart"."""
|
||||
|
||||
"""A list of Part instances if ``Content-Type`` is of major type
|
||||
"multipart"."""
|
||||
|
||||
part_class = None
|
||||
"""The class used for multipart parts.
|
||||
|
||||
|
||||
You can replace this with custom subclasses to alter the processing of
|
||||
multipart parts.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, fp, headers, params=None, parts=None):
|
||||
# Make an instance-specific copy of the class processors
|
||||
# so Tools, etc. can replace them per-request.
|
||||
self.processors = self.processors.copy()
|
||||
|
||||
|
||||
self.fp = fp
|
||||
self.headers = headers
|
||||
|
||||
|
||||
if params is None:
|
||||
params = {}
|
||||
self.params = params
|
||||
|
||||
|
||||
if parts is None:
|
||||
parts = []
|
||||
self.parts = parts
|
||||
|
||||
|
||||
# Content-Type
|
||||
self.content_type = headers.elements('Content-Type')
|
||||
if self.content_type:
|
||||
|
@ -395,26 +425,29 @@ class Entity(object):
|
|||
else:
|
||||
self.content_type = httputil.HeaderElement.from_str(
|
||||
self.default_content_type)
|
||||
|
||||
# Copy the class 'attempt_charsets', prepending any Content-Type charset
|
||||
|
||||
# Copy the class 'attempt_charsets', prepending any Content-Type
|
||||
# charset
|
||||
dec = self.content_type.params.get("charset", None)
|
||||
if dec:
|
||||
#dec = dec.decode('ISO-8859-1')
|
||||
self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
|
||||
if c != dec]
|
||||
else:
|
||||
self.attempt_charsets = self.attempt_charsets[:]
|
||||
|
||||
|
||||
# Length
|
||||
self.length = None
|
||||
clen = headers.get('Content-Length', None)
|
||||
# If Transfer-Encoding is 'chunked', ignore any Content-Length.
|
||||
if clen is not None and 'chunked' not in headers.get('Transfer-Encoding', ''):
|
||||
if (
|
||||
clen is not None and
|
||||
'chunked' not in headers.get('Transfer-Encoding', '')
|
||||
):
|
||||
try:
|
||||
self.length = int(clen)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
# Content-Disposition
|
||||
self.name = None
|
||||
self.filename = None
|
||||
|
@ -427,45 +460,57 @@ class Entity(object):
|
|||
self.name = self.name[1:-1]
|
||||
if 'filename' in disp.params:
|
||||
self.filename = disp.params['filename']
|
||||
if self.filename.startswith('"') and self.filename.endswith('"'):
|
||||
if (
|
||||
self.filename.startswith('"') and
|
||||
self.filename.endswith('"')
|
||||
):
|
||||
self.filename = self.filename[1:-1]
|
||||
|
||||
|
||||
# The 'type' attribute is deprecated in 3.2; remove it in 3.3.
|
||||
type = property(lambda self: self.content_type,
|
||||
doc="""A deprecated alias for :attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`.""")
|
||||
|
||||
type = property(
|
||||
lambda self: self.content_type,
|
||||
doc="A deprecated alias for "
|
||||
":attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`."
|
||||
)
|
||||
|
||||
def read(self, size=None, fp_out=None):
|
||||
return self.fp.read(size, fp_out)
|
||||
|
||||
|
||||
def readline(self, size=None):
|
||||
return self.fp.readline(size)
|
||||
|
||||
|
||||
def readlines(self, sizehint=None):
|
||||
return self.fp.readlines(sizehint)
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
|
||||
def __next__(self):
|
||||
line = self.readline()
|
||||
if not line:
|
||||
raise StopIteration
|
||||
return line
|
||||
|
||||
|
||||
def next(self):
|
||||
return self.__next__()
|
||||
|
||||
def read_into_file(self, fp_out=None):
|
||||
"""Read the request body into fp_out (or make_file() if None). Return fp_out."""
|
||||
"""Read the request body into fp_out (or make_file() if None).
|
||||
|
||||
Return fp_out.
|
||||
"""
|
||||
if fp_out is None:
|
||||
fp_out = self.make_file()
|
||||
self.read(fp_out=fp_out)
|
||||
return fp_out
|
||||
|
||||
|
||||
def make_file(self):
|
||||
"""Return a file-like object into which the request body will be read.
|
||||
|
||||
|
||||
By default, this will return a TemporaryFile. Override as needed.
|
||||
See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`."""
|
||||
return tempfile.TemporaryFile()
|
||||
|
||||
|
||||
def fullvalue(self):
|
||||
"""Return this entity as a string, whether stored in a file or not."""
|
||||
if self.file:
|
||||
|
@ -476,7 +521,7 @@ class Entity(object):
|
|||
else:
|
||||
value = self.value
|
||||
return value
|
||||
|
||||
|
||||
def process(self):
|
||||
"""Execute the best-match processor for the given media type."""
|
||||
proc = None
|
||||
|
@ -493,9 +538,11 @@ class Entity(object):
|
|||
self.default_proc()
|
||||
else:
|
||||
proc(self)
|
||||
|
||||
|
||||
def default_proc(self):
|
||||
"""Called if a more-specific processor is not found for the ``Content-Type``."""
|
||||
"""Called if a more-specific processor is not found for the
|
||||
``Content-Type``.
|
||||
"""
|
||||
# Leave the fp alone for someone else to read. This works fine
|
||||
# for request.body, but the Part subclasses need to override this
|
||||
# so they can move on to the next part.
|
||||
|
@ -503,25 +550,26 @@ class Entity(object):
|
|||
|
||||
|
||||
class Part(Entity):
|
||||
|
||||
"""A MIME part entity, part of a multipart entity."""
|
||||
|
||||
|
||||
# "The default character set, which must be assumed in the absence of a
|
||||
# charset parameter, is US-ASCII."
|
||||
attempt_charsets = ['us-ascii', 'utf-8']
|
||||
"""A list of strings, each of which should be a known encoding.
|
||||
|
||||
|
||||
When the Content-Type of the request body warrants it, each of the given
|
||||
encodings will be tried in order. The first one to successfully decode the
|
||||
entity without raising an error is stored as
|
||||
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
|
||||
to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
|
||||
`HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
|
||||
to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
|
||||
`HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
|
||||
but ``['us-ascii', 'utf-8']`` for multipart parts.
|
||||
"""
|
||||
|
||||
|
||||
boundary = None
|
||||
"""The MIME multipart boundary."""
|
||||
|
||||
|
||||
default_content_type = 'text/plain'
|
||||
"""This defines a default ``Content-Type`` to use if no Content-Type header
|
||||
is given. The empty string is used for RequestBody, which results in the
|
||||
|
@ -531,26 +579,27 @@ class Part(Entity):
|
|||
the MIME spec declares that a part with no Content-Type defaults to
|
||||
"text/plain".
|
||||
"""
|
||||
|
||||
|
||||
# This is the default in stdlib cgi. We may want to increase it.
|
||||
maxrambytes = 1000
|
||||
"""The threshold of bytes after which point the ``Part`` will store its data
|
||||
in a file (generated by :func:`make_file<cherrypy._cprequest.Entity.make_file>`)
|
||||
instead of a string. Defaults to 1000, just like the :mod:`cgi` module in
|
||||
Python's standard library.
|
||||
"""The threshold of bytes after which point the ``Part`` will store
|
||||
its data in a file (generated by
|
||||
:func:`make_file<cherrypy._cprequest.Entity.make_file>`)
|
||||
instead of a string. Defaults to 1000, just like the :mod:`cgi`
|
||||
module in Python's standard library.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, fp, headers, boundary):
|
||||
Entity.__init__(self, fp, headers)
|
||||
self.boundary = boundary
|
||||
self.file = None
|
||||
self.value = None
|
||||
|
||||
|
||||
def from_fp(cls, fp, boundary):
|
||||
headers = cls.read_headers(fp)
|
||||
return cls(fp, headers, boundary)
|
||||
from_fp = classmethod(from_fp)
|
||||
|
||||
|
||||
def read_headers(cls, fp):
|
||||
headers = httputil.HeaderMap()
|
||||
while True:
|
||||
|
@ -558,13 +607,13 @@ class Part(Entity):
|
|||
if not line:
|
||||
# No more data--illegal end of headers
|
||||
raise EOFError("Illegal end of headers.")
|
||||
|
||||
|
||||
if line == ntob('\r\n'):
|
||||
# Normal end of headers
|
||||
break
|
||||
if not line.endswith(ntob('\r\n')):
|
||||
raise ValueError("MIME requires CRLF terminators: %r" % line)
|
||||
|
||||
|
||||
if line[0] in ntob(' \t'):
|
||||
# It's a continuation line.
|
||||
v = line.strip().decode('ISO-8859-1')
|
||||
|
@ -572,24 +621,24 @@ class Part(Entity):
|
|||
k, v = line.split(ntob(":"), 1)
|
||||
k = k.strip().decode('ISO-8859-1')
|
||||
v = v.strip().decode('ISO-8859-1')
|
||||
|
||||
|
||||
existing = headers.get(k)
|
||||
if existing:
|
||||
v = ", ".join((existing, v))
|
||||
headers[k] = v
|
||||
|
||||
|
||||
return headers
|
||||
read_headers = classmethod(read_headers)
|
||||
|
||||
|
||||
def read_lines_to_boundary(self, fp_out=None):
|
||||
"""Read bytes from self.fp and return or write them to a file.
|
||||
|
||||
|
||||
If the 'fp_out' argument is None (the default), all bytes read are
|
||||
returned in a single byte string.
|
||||
|
||||
If the 'fp_out' argument is not None, it must be a file-like object that
|
||||
supports the 'write' method; all bytes read will be written to the fp,
|
||||
and that fp is returned.
|
||||
|
||||
If the 'fp_out' argument is not None, it must be a file-like
|
||||
object that supports the 'write' method; all bytes read will be
|
||||
written to the fp, and that fp is returned.
|
||||
"""
|
||||
endmarker = self.boundary + ntob("--")
|
||||
delim = ntob("")
|
||||
|
@ -597,7 +646,7 @@ class Part(Entity):
|
|||
lines = []
|
||||
seen = 0
|
||||
while True:
|
||||
line = self.fp.readline(1<<16)
|
||||
line = self.fp.readline(1 << 16)
|
||||
if not line:
|
||||
raise EOFError("Illegal end of multipart body.")
|
||||
if line.startswith(ntob("--")) and prev_lf:
|
||||
|
@ -607,9 +656,9 @@ class Part(Entity):
|
|||
if strippedline == endmarker:
|
||||
self.fp.finish()
|
||||
break
|
||||
|
||||
|
||||
line = delim + line
|
||||
|
||||
|
||||
if line.endswith(ntob("\r\n")):
|
||||
delim = ntob("\r\n")
|
||||
line = line[:-2]
|
||||
|
@ -621,7 +670,7 @@ class Part(Entity):
|
|||
else:
|
||||
delim = ntob("")
|
||||
prev_lf = False
|
||||
|
||||
|
||||
if fp_out is None:
|
||||
lines.append(line)
|
||||
seen += len(line)
|
||||
|
@ -631,7 +680,7 @@ class Part(Entity):
|
|||
fp_out.write(line)
|
||||
else:
|
||||
fp_out.write(line)
|
||||
|
||||
|
||||
if fp_out is None:
|
||||
result = ntob('').join(lines)
|
||||
for charset in self.attempt_charsets:
|
||||
|
@ -644,14 +693,18 @@ class Part(Entity):
|
|||
return result
|
||||
else:
|
||||
raise cherrypy.HTTPError(
|
||||
400, "The request entity could not be decoded. The following "
|
||||
"charsets were attempted: %s" % repr(self.attempt_charsets))
|
||||
400,
|
||||
"The request entity could not be decoded. The following "
|
||||
"charsets were attempted: %s" % repr(self.attempt_charsets)
|
||||
)
|
||||
else:
|
||||
fp_out.seek(0)
|
||||
return fp_out
|
||||
|
||||
|
||||
def default_proc(self):
|
||||
"""Called if a more-specific processor is not found for the ``Content-Type``."""
|
||||
"""Called if a more-specific processor is not found for the
|
||||
``Content-Type``.
|
||||
"""
|
||||
if self.filename:
|
||||
# Always read into a file if a .filename was given.
|
||||
self.file = self.read_into_file()
|
||||
|
@ -661,9 +714,12 @@ class Part(Entity):
|
|||
self.value = result
|
||||
else:
|
||||
self.file = result
|
||||
|
||||
|
||||
def read_into_file(self, fp_out=None):
|
||||
"""Read the request body into fp_out (or make_file() if None). Return fp_out."""
|
||||
"""Read the request body into fp_out (or make_file() if None).
|
||||
|
||||
Return fp_out.
|
||||
"""
|
||||
if fp_out is None:
|
||||
fp_out = self.make_file()
|
||||
self.read_lines_to_boundary(fp_out=fp_out)
|
||||
|
@ -671,25 +727,35 @@ class Part(Entity):
|
|||
|
||||
Entity.part_class = Part
|
||||
|
||||
try:
|
||||
inf = float('inf')
|
||||
except ValueError:
|
||||
# Python 2.4 and lower
|
||||
class Infinity(object):
|
||||
|
||||
class Infinity(object):
|
||||
def __cmp__(self, other):
|
||||
return 1
|
||||
def __sub__(self, other):
|
||||
return self
|
||||
inf = Infinity()
|
||||
def __cmp__(self, other):
|
||||
return 1
|
||||
|
||||
def __sub__(self, other):
|
||||
return self
|
||||
inf = Infinity()
|
||||
|
||||
|
||||
comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
|
||||
'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control', 'Connection',
|
||||
'Content-Encoding', 'Content-Language', 'Expect', 'If-Match',
|
||||
'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'Te', 'Trailer',
|
||||
'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning', 'Www-Authenticate']
|
||||
comma_separated_headers = [
|
||||
'Accept', 'Accept-Charset', 'Accept-Encoding',
|
||||
'Accept-Language', 'Accept-Ranges', 'Allow',
|
||||
'Cache-Control', 'Connection', 'Content-Encoding',
|
||||
'Content-Language', 'Expect', 'If-Match',
|
||||
'If-None-Match', 'Pragma', 'Proxy-Authenticate',
|
||||
'Te', 'Trailer', 'Transfer-Encoding', 'Upgrade',
|
||||
'Vary', 'Via', 'Warning', 'Www-Authenticate'
|
||||
]
|
||||
|
||||
|
||||
class SizedReader:
|
||||
|
||||
def __init__(self, fp, length, maxbytes, bufsize=8192, has_trailers=False):
|
||||
|
||||
def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE,
|
||||
has_trailers=False):
|
||||
# Wrap our fp in a buffer so peek() works
|
||||
self.fp = fp
|
||||
self.length = length
|
||||
|
@ -699,25 +765,25 @@ class SizedReader:
|
|||
self.bytes_read = 0
|
||||
self.done = False
|
||||
self.has_trailers = has_trailers
|
||||
|
||||
|
||||
def read(self, size=None, fp_out=None):
|
||||
"""Read bytes from the request body and return or write them to a file.
|
||||
|
||||
|
||||
A number of bytes less than or equal to the 'size' argument are read
|
||||
off the socket. The actual number of bytes read are tracked in
|
||||
self.bytes_read. The number may be smaller than 'size' when 1) the
|
||||
client sends fewer bytes, 2) the 'Content-Length' request header
|
||||
specifies fewer bytes than requested, or 3) the number of bytes read
|
||||
exceeds self.maxbytes (in which case, 413 is raised).
|
||||
|
||||
|
||||
If the 'fp_out' argument is None (the default), all bytes read are
|
||||
returned in a single byte string.
|
||||
|
||||
If the 'fp_out' argument is not None, it must be a file-like object that
|
||||
supports the 'write' method; all bytes read will be written to the fp,
|
||||
and None is returned.
|
||||
|
||||
If the 'fp_out' argument is not None, it must be a file-like
|
||||
object that supports the 'write' method; all bytes read will be
|
||||
written to the fp, and None is returned.
|
||||
"""
|
||||
|
||||
|
||||
if self.length is None:
|
||||
if size is None:
|
||||
remaining = inf
|
||||
|
@ -733,9 +799,9 @@ class SizedReader:
|
|||
return ntob('')
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
chunks = []
|
||||
|
||||
|
||||
# Read bytes from the buffer.
|
||||
if self.buffer:
|
||||
if remaining is inf:
|
||||
|
@ -746,18 +812,18 @@ class SizedReader:
|
|||
self.buffer = self.buffer[remaining:]
|
||||
datalen = len(data)
|
||||
remaining -= datalen
|
||||
|
||||
|
||||
# Check lengths.
|
||||
self.bytes_read += datalen
|
||||
if self.maxbytes and self.bytes_read > self.maxbytes:
|
||||
raise cherrypy.HTTPError(413)
|
||||
|
||||
|
||||
# Store the data.
|
||||
if fp_out is None:
|
||||
chunks.append(data)
|
||||
else:
|
||||
fp_out.write(data)
|
||||
|
||||
|
||||
# Read bytes from the socket.
|
||||
while remaining > 0:
|
||||
chunksize = min(remaining, self.bufsize)
|
||||
|
@ -776,21 +842,21 @@ class SizedReader:
|
|||
break
|
||||
datalen = len(data)
|
||||
remaining -= datalen
|
||||
|
||||
|
||||
# Check lengths.
|
||||
self.bytes_read += datalen
|
||||
if self.maxbytes and self.bytes_read > self.maxbytes:
|
||||
raise cherrypy.HTTPError(413)
|
||||
|
||||
|
||||
# Store the data.
|
||||
if fp_out is None:
|
||||
chunks.append(data)
|
||||
else:
|
||||
fp_out.write(data)
|
||||
|
||||
|
||||
if fp_out is None:
|
||||
return ntob('').join(chunks)
|
||||
|
||||
|
||||
def readline(self, size=None):
|
||||
"""Read a line from the request body and return it."""
|
||||
chunks = []
|
||||
|
@ -811,7 +877,7 @@ class SizedReader:
|
|||
else:
|
||||
chunks.append(data)
|
||||
return ntob('').join(chunks)
|
||||
|
||||
|
||||
def readlines(self, sizehint=None):
|
||||
"""Read lines from the request body and return them."""
|
||||
if self.length is not None:
|
||||
|
@ -819,7 +885,7 @@ class SizedReader:
|
|||
sizehint = self.length - self.bytes_read
|
||||
else:
|
||||
sizehint = min(sizehint, self.length - self.bytes_read)
|
||||
|
||||
|
||||
lines = []
|
||||
seen = 0
|
||||
while True:
|
||||
|
@ -831,12 +897,12 @@ class SizedReader:
|
|||
if seen >= sizehint:
|
||||
break
|
||||
return lines
|
||||
|
||||
|
||||
def finish(self):
|
||||
self.done = True
|
||||
if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
|
||||
self.trailers = {}
|
||||
|
||||
|
||||
try:
|
||||
for line in self.fp.read_trailer_lines():
|
||||
if line[0] in ntob(' \t'):
|
||||
|
@ -849,7 +915,7 @@ class SizedReader:
|
|||
raise ValueError("Illegal header line.")
|
||||
k = k.strip().title()
|
||||
v = v.strip()
|
||||
|
||||
|
||||
if k in comma_separated_headers:
|
||||
existing = self.trailers.get(envname)
|
||||
if existing:
|
||||
|
@ -866,13 +932,15 @@ class SizedReader:
|
|||
|
||||
|
||||
class RequestBody(Entity):
|
||||
|
||||
"""The entity of the HTTP request."""
|
||||
|
||||
|
||||
bufsize = 8 * 1024
|
||||
"""The buffer size used when reading the socket."""
|
||||
|
||||
|
||||
# Don't parse the request body at all if the client didn't provide
|
||||
# a Content-Type header. See http://www.cherrypy.org/ticket/790
|
||||
# a Content-Type header. See
|
||||
# https://bitbucket.org/cherrypy/cherrypy/issue/790
|
||||
default_content_type = ''
|
||||
"""This defines a default ``Content-Type`` to use if no Content-Type header
|
||||
is given. The empty string is used for RequestBody, which results in the
|
||||
|
@ -882,13 +950,15 @@ class RequestBody(Entity):
|
|||
declares that a part with no Content-Type defaults to "text/plain"
|
||||
(see :class:`Part<cherrypy._cpreqbody.Part>`).
|
||||
"""
|
||||
|
||||
|
||||
maxbytes = None
|
||||
"""Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket."""
|
||||
|
||||
"""Raise ``MaxSizeExceeded`` if more bytes than this are read from
|
||||
the socket.
|
||||
"""
|
||||
|
||||
def __init__(self, fp, headers, params=None, request_params=None):
|
||||
Entity.__init__(self, fp, headers, params)
|
||||
|
||||
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
|
||||
# When no explicit charset parameter is provided by the
|
||||
# sender, media subtypes of the "text" type are defined
|
||||
|
@ -900,14 +970,14 @@ class RequestBody(Entity):
|
|||
break
|
||||
else:
|
||||
self.attempt_charsets.append('ISO-8859-1')
|
||||
|
||||
|
||||
# Temporary fix while deprecating passing .parts as .params.
|
||||
self.processors['multipart'] = _old_process_multipart
|
||||
|
||||
|
||||
if request_params is None:
|
||||
request_params = {}
|
||||
self.request_params = request_params
|
||||
|
||||
|
||||
def process(self):
|
||||
"""Process the request entity based on its Content-Type."""
|
||||
# "The presence of a message-body in a request is signaled by the
|
||||
|
@ -919,20 +989,22 @@ class RequestBody(Entity):
|
|||
h = cherrypy.serving.request.headers
|
||||
if 'Content-Length' not in h and 'Transfer-Encoding' not in h:
|
||||
raise cherrypy.HTTPError(411)
|
||||
|
||||
|
||||
self.fp = SizedReader(self.fp, self.length,
|
||||
self.maxbytes, bufsize=self.bufsize,
|
||||
has_trailers='Trailer' in h)
|
||||
super(RequestBody, self).process()
|
||||
|
||||
|
||||
# Body params should also be a part of the request_params
|
||||
# add them in here.
|
||||
request_params = self.request_params
|
||||
for key, value in self.params.items():
|
||||
# Python 2 only: keyword arguments must be byte strings (type 'str').
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('ISO-8859-1')
|
||||
|
||||
# Python 2 only: keyword arguments must be byte strings (type
|
||||
# 'str').
|
||||
if sys.version_info < (3, 0):
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('ISO-8859-1')
|
||||
|
||||
if key in request_params:
|
||||
if not isinstance(request_params[key], list):
|
||||
request_params[key] = [request_params[key]]
|
||||
|
|
|
@ -6,56 +6,62 @@ import warnings
|
|||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import basestring, copykeys, ntob, unicodestr
|
||||
from cherrypy._cpcompat import SimpleCookie, CookieError
|
||||
from cherrypy._cpcompat import SimpleCookie, CookieError, py3k
|
||||
from cherrypy import _cpreqbody, _cpconfig
|
||||
from cherrypy._cperror import format_exc, bare_error
|
||||
from cherrypy.lib import httputil, file_generator
|
||||
|
||||
|
||||
class Hook(object):
|
||||
|
||||
"""A callback and its metadata: failsafe, priority, and kwargs."""
|
||||
|
||||
|
||||
callback = None
|
||||
"""
|
||||
The bare callable that this Hook object is wrapping, which will
|
||||
be called when the Hook is called."""
|
||||
|
||||
|
||||
failsafe = False
|
||||
"""
|
||||
If True, the callback is guaranteed to run even if other callbacks
|
||||
from the same call point raise exceptions."""
|
||||
|
||||
|
||||
priority = 50
|
||||
"""
|
||||
Defines the order of execution for a list of Hooks. Priority numbers
|
||||
should be limited to the closed interval [0, 100], but values outside
|
||||
this range are acceptable, as are fractional values."""
|
||||
|
||||
|
||||
kwargs = {}
|
||||
"""
|
||||
A set of keyword arguments that will be passed to the
|
||||
callable on each call."""
|
||||
|
||||
|
||||
def __init__(self, callback, failsafe=None, priority=None, **kwargs):
|
||||
self.callback = callback
|
||||
|
||||
|
||||
if failsafe is None:
|
||||
failsafe = getattr(callback, "failsafe", False)
|
||||
self.failsafe = failsafe
|
||||
|
||||
|
||||
if priority is None:
|
||||
priority = getattr(callback, "priority", 50)
|
||||
self.priority = priority
|
||||
|
||||
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
def __lt__(self, other):
|
||||
# Python 3
|
||||
return self.priority < other.priority
|
||||
|
||||
def __cmp__(self, other):
|
||||
# Python 2
|
||||
return cmp(self.priority, other.priority)
|
||||
|
||||
|
||||
def __call__(self):
|
||||
"""Run self.callback(**self.kwargs)."""
|
||||
return self.callback(**self.kwargs)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)"
|
||||
|
@ -66,21 +72,22 @@ class Hook(object):
|
|||
|
||||
|
||||
class HookMap(dict):
|
||||
|
||||
"""A map of call points to lists of callbacks (Hook objects)."""
|
||||
|
||||
|
||||
def __new__(cls, points=None):
|
||||
d = dict.__new__(cls)
|
||||
for p in points or []:
|
||||
d[p] = []
|
||||
return d
|
||||
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
|
||||
|
||||
def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
|
||||
"""Append a new Hook made from the supplied arguments."""
|
||||
self[point].append(Hook(callback, failsafe, priority, **kwargs))
|
||||
|
||||
|
||||
def run(self, point):
|
||||
"""Execute all registered Hooks (callbacks) for the given point."""
|
||||
exc = None
|
||||
|
@ -104,8 +111,8 @@ class HookMap(dict):
|
|||
exc = sys.exc_info()[1]
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
if exc:
|
||||
raise
|
||||
|
||||
raise exc
|
||||
|
||||
def __copy__(self):
|
||||
newmap = self.__class__()
|
||||
# We can't just use 'update' because we want copies of the
|
||||
|
@ -114,10 +121,14 @@ class HookMap(dict):
|
|||
newmap[k] = v[:]
|
||||
return newmap
|
||||
copy = __copy__
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, copykeys(self))
|
||||
return "%s.%s(points=%r)" % (
|
||||
cls.__module__,
|
||||
cls.__name__,
|
||||
copykeys(self)
|
||||
)
|
||||
|
||||
|
||||
# Config namespace handlers
|
||||
|
@ -134,14 +145,17 @@ def hooks_namespace(k, v):
|
|||
v = Hook(v)
|
||||
cherrypy.serving.request.hooks[hookpoint].append(v)
|
||||
|
||||
|
||||
def request_namespace(k, v):
|
||||
"""Attach request attributes declared in config."""
|
||||
# Provides config entries to set request.body attrs (like attempt_charsets).
|
||||
# Provides config entries to set request.body attrs (like
|
||||
# attempt_charsets).
|
||||
if k[:5] == 'body.':
|
||||
setattr(cherrypy.serving.request.body, k[5:], v)
|
||||
else:
|
||||
setattr(cherrypy.serving.request, k, v)
|
||||
|
||||
|
||||
def response_namespace(k, v):
|
||||
"""Attach response attributes declared in config."""
|
||||
# Provides config entries to set default response headers
|
||||
|
@ -151,6 +165,7 @@ def response_namespace(k, v):
|
|||
else:
|
||||
setattr(cherrypy.serving.response, k, v)
|
||||
|
||||
|
||||
def error_page_namespace(k, v):
|
||||
"""Attach error pages declared in config."""
|
||||
if k != 'default':
|
||||
|
@ -165,8 +180,9 @@ hookpoints = ['on_start_resource', 'before_request_body',
|
|||
|
||||
|
||||
class Request(object):
|
||||
|
||||
"""An HTTP request.
|
||||
|
||||
|
||||
This object represents the metadata of an HTTP request message;
|
||||
that is, it contains attributes which describe the environment
|
||||
in which the request URL, headers, and body were sent (if you
|
||||
|
@ -176,43 +192,43 @@ class Request(object):
|
|||
also contains data regarding the configuration in effect for
|
||||
the given URL, and the execution plan for generating a response.
|
||||
"""
|
||||
|
||||
|
||||
prev = None
|
||||
"""
|
||||
The previous Request object (if any). This should be None
|
||||
unless we are processing an InternalRedirect."""
|
||||
|
||||
|
||||
# Conversation/connection attributes
|
||||
local = httputil.Host("127.0.0.1", 80)
|
||||
"An httputil.Host(ip, port, hostname) object for the server socket."
|
||||
|
||||
|
||||
remote = httputil.Host("127.0.0.1", 1111)
|
||||
"An httputil.Host(ip, port, hostname) object for the client socket."
|
||||
|
||||
|
||||
scheme = "http"
|
||||
"""
|
||||
The protocol used between client and server. In most cases,
|
||||
this will be either 'http' or 'https'."""
|
||||
|
||||
|
||||
server_protocol = "HTTP/1.1"
|
||||
"""
|
||||
The HTTP version for which the HTTP server is at least
|
||||
conditionally compliant."""
|
||||
|
||||
|
||||
base = ""
|
||||
"""The (scheme://host) portion of the requested URL.
|
||||
In some cases (e.g. when proxying via mod_rewrite), this may contain
|
||||
path segments which cherrypy.url uses when constructing url's, but
|
||||
which otherwise are ignored by CherryPy. Regardless, this value
|
||||
MUST NOT end in a slash."""
|
||||
|
||||
|
||||
# Request-Line attributes
|
||||
request_line = ""
|
||||
"""
|
||||
The complete Request-Line received from the client. This is a
|
||||
single string consisting of the request method, URI, and protocol
|
||||
version (joined by spaces). Any final CRLF is removed."""
|
||||
|
||||
|
||||
method = "GET"
|
||||
"""
|
||||
Indicates the HTTP method to be performed on the resource identified
|
||||
|
@ -220,7 +236,7 @@ class Request(object):
|
|||
DELETE. CherryPy allows any extension method; however, various HTTP
|
||||
servers and gateways may restrict the set of allowable methods.
|
||||
CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
|
||||
|
||||
|
||||
query_string = ""
|
||||
"""
|
||||
The query component of the Request-URI, a string of information to be
|
||||
|
@ -228,7 +244,7 @@ class Request(object):
|
|||
path component, and is separated by a '?'. For example, the URI
|
||||
'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
|
||||
'a=3&b=4'."""
|
||||
|
||||
|
||||
query_string_encoding = 'utf8'
|
||||
"""
|
||||
The encoding expected for query string arguments after % HEX HEX decoding).
|
||||
|
@ -237,7 +253,7 @@ class Request(object):
|
|||
arbitrary encodings to not error, set this to 'Latin-1'; you can then
|
||||
encode back to bytes and re-decode to whatever encoding you like later.
|
||||
"""
|
||||
|
||||
|
||||
protocol = (1, 1)
|
||||
"""The HTTP protocol version corresponding to the set
|
||||
of features which should be allowed in the response. If BOTH
|
||||
|
@ -245,20 +261,20 @@ class Request(object):
|
|||
compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
|
||||
If either is 1.0, this attribute will be the tuple (1, 0).
|
||||
Lower HTTP protocol versions are not explicitly supported."""
|
||||
|
||||
|
||||
params = {}
|
||||
"""
|
||||
A dict which combines query string (GET) and request entity (POST)
|
||||
variables. This is populated in two stages: GET params are added
|
||||
before the 'on_start_resource' hook, and POST params are added
|
||||
between the 'before_request_body' and 'before_handler' hooks."""
|
||||
|
||||
|
||||
# Message attributes
|
||||
header_list = []
|
||||
"""
|
||||
A list of the HTTP request headers as (name, value) tuples.
|
||||
In general, you should use request.headers (a dict) instead."""
|
||||
|
||||
|
||||
headers = httputil.HeaderMap()
|
||||
"""
|
||||
A dict-like object containing the request headers. Keys are header
|
||||
|
@ -267,10 +283,10 @@ class Request(object):
|
|||
headers['content-type'] refer to the same value. Values are header
|
||||
values (decoded according to :rfc:`2047` if necessary). See also:
|
||||
httputil.HeaderMap, httputil.HeaderElement."""
|
||||
|
||||
|
||||
cookie = SimpleCookie()
|
||||
"""See help(Cookie)."""
|
||||
|
||||
|
||||
rfile = None
|
||||
"""
|
||||
If the request included an entity (body), it will be available
|
||||
|
@ -278,11 +294,11 @@ class Request(object):
|
|||
be read for you between the 'before_request_body' hook and the
|
||||
'before_handler' hook, and the resulting string is placed into
|
||||
either request.params or the request.body attribute.
|
||||
|
||||
|
||||
You may disable the automatic consumption of the rfile by setting
|
||||
request.process_request_body to False, either in config for the desired
|
||||
path, or in an 'on_start_resource' or 'before_request_body' hook.
|
||||
|
||||
|
||||
WARNING: In almost every case, you should not attempt to read from the
|
||||
rfile stream after CherryPy's automatic mechanism has read it. If you
|
||||
turn off the automatic parsing of rfile, you should read exactly the
|
||||
|
@ -290,17 +306,20 @@ class Request(object):
|
|||
Ignoring either of these warnings may result in a hung request thread
|
||||
or in corruption of the next (pipelined) request.
|
||||
"""
|
||||
|
||||
|
||||
process_request_body = True
|
||||
"""
|
||||
If True, the rfile (if any) is automatically read and parsed,
|
||||
and the result placed into request.params or request.body."""
|
||||
|
||||
|
||||
methods_with_bodies = ("POST", "PUT")
|
||||
"""
|
||||
A sequence of HTTP methods for which CherryPy will automatically
|
||||
attempt to read a body from the rfile."""
|
||||
|
||||
attempt to read a body from the rfile. If you are going to change
|
||||
this property, modify it on the configuration (recommended)
|
||||
or on the "hook point" `on_start_resource`.
|
||||
"""
|
||||
|
||||
body = None
|
||||
"""
|
||||
If the request Content-Type is 'application/x-www-form-urlencoded'
|
||||
|
@ -308,7 +327,7 @@ class Request(object):
|
|||
of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
|
||||
can .read()); this value is set between the 'before_request_body' and
|
||||
'before_handler' hooks (assuming that process_request_body is True)."""
|
||||
|
||||
|
||||
# Dispatch attributes
|
||||
dispatch = cherrypy.dispatch.Dispatcher()
|
||||
"""
|
||||
|
@ -317,19 +336,19 @@ class Request(object):
|
|||
request attributes, and the application architecture. The core
|
||||
calls the dispatcher as early as possible, passing it a 'path_info'
|
||||
argument.
|
||||
|
||||
|
||||
The default dispatcher discovers the page handler by matching path_info
|
||||
to a hierarchical arrangement of objects, starting at request.app.root.
|
||||
See help(cherrypy.dispatch) for more information."""
|
||||
|
||||
|
||||
script_name = ""
|
||||
"""
|
||||
The 'mount point' of the application which is handling this request.
|
||||
|
||||
|
||||
This attribute MUST NOT end in a slash. If the script_name refers to
|
||||
the root of the URI, it MUST be an empty string (not "/").
|
||||
"""
|
||||
|
||||
|
||||
path_info = "/"
|
||||
"""
|
||||
The 'relative path' portion of the Request-URI. This is relative
|
||||
|
@ -341,12 +360,12 @@ class Request(object):
|
|||
When authentication is used during the request processing this is
|
||||
set to 'False' if it failed and to the 'username' value if it succeeded.
|
||||
The default 'None' implies that no authentication happened."""
|
||||
|
||||
|
||||
# Note that cherrypy.url uses "if request.app:" to determine whether
|
||||
# the call is during a real HTTP request or not. So leave this None.
|
||||
app = None
|
||||
"""The cherrypy.Application object which is handling this request."""
|
||||
|
||||
|
||||
handler = None
|
||||
"""
|
||||
The function, method, or other callable which CherryPy will call to
|
||||
|
@ -355,12 +374,12 @@ class Request(object):
|
|||
By default, the handler is discovered by walking a tree of objects
|
||||
starting at request.app.root, and is then passed all HTTP params
|
||||
(from the query string and POST body) as keyword arguments."""
|
||||
|
||||
|
||||
toolmaps = {}
|
||||
"""
|
||||
A nested dict of all Toolboxes and Tools in effect for this request,
|
||||
of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
|
||||
|
||||
|
||||
config = None
|
||||
"""
|
||||
A flat dict of all configuration entries which apply to the
|
||||
|
@ -370,7 +389,7 @@ class Request(object):
|
|||
effect for this request; by default, handler config can be attached
|
||||
anywhere in the tree between request.app.root and the final handler,
|
||||
and inherits downward)."""
|
||||
|
||||
|
||||
is_index = None
|
||||
"""
|
||||
This will be True if the current request is mapped to an 'index'
|
||||
|
@ -378,7 +397,7 @@ class Request(object):
|
|||
a slash). The value may be used to automatically redirect the
|
||||
user-agent to a 'more canonical' URL which either adds or removes
|
||||
the trailing slash. See cherrypy.tools.trailing_slash."""
|
||||
|
||||
|
||||
hooks = HookMap(hookpoints)
|
||||
"""
|
||||
A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
|
||||
|
@ -387,7 +406,7 @@ class Request(object):
|
|||
The list of hooks is generally populated as early as possible (mostly
|
||||
from Tools specified in config), but may be extended at any time.
|
||||
See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
|
||||
|
||||
|
||||
error_response = cherrypy.HTTPError(500).set_response
|
||||
"""
|
||||
The no-arg callable which will handle unexpected, untrapped errors
|
||||
|
@ -397,31 +416,31 @@ class Request(object):
|
|||
via request.error_page or by overriding HTTPError.set_response).
|
||||
By default, error_response uses HTTPError(500) to return a generic
|
||||
error response to the user-agent."""
|
||||
|
||||
|
||||
error_page = {}
|
||||
"""
|
||||
A dict of {error code: response filename or callable} pairs.
|
||||
|
||||
|
||||
The error code must be an int representing a given HTTP error code,
|
||||
or the string 'default', which will be used if no matching entry
|
||||
is found for a given numeric code.
|
||||
|
||||
|
||||
If a filename is provided, the file should contain a Python string-
|
||||
formatting template, and can expect by default to receive format
|
||||
formatting template, and can expect by default to receive format
|
||||
values with the mapping keys %(status)s, %(message)s, %(traceback)s,
|
||||
and %(version)s. The set of format mappings can be extended by
|
||||
overriding HTTPError.set_response.
|
||||
|
||||
|
||||
If a callable is provided, it will be called by default with keyword
|
||||
arguments 'status', 'message', 'traceback', and 'version', as for a
|
||||
string-formatting template. The callable must return a string or iterable of
|
||||
strings which will be set to response.body. It may also override headers or
|
||||
perform any other processing.
|
||||
|
||||
string-formatting template. The callable must return a string or
|
||||
iterable of strings which will be set to response.body. It may also
|
||||
override headers or perform any other processing.
|
||||
|
||||
If no entry is given for an error code, and no 'default' entry exists,
|
||||
a default template will be used.
|
||||
"""
|
||||
|
||||
|
||||
show_tracebacks = True
|
||||
"""
|
||||
If True, unexpected errors encountered during request processing will
|
||||
|
@ -431,23 +450,23 @@ class Request(object):
|
|||
"""
|
||||
If True, mismatched parameters encountered during PageHandler invocation
|
||||
processing will be included in the response body."""
|
||||
|
||||
|
||||
throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
|
||||
"""The sequence of exceptions which Request.run does not trap."""
|
||||
|
||||
|
||||
throw_errors = False
|
||||
"""
|
||||
If True, Request.run will not trap any errors (except HTTPRedirect and
|
||||
HTTPError, which are more properly called 'exceptions', not errors)."""
|
||||
|
||||
|
||||
closed = False
|
||||
"""True once the close method has been called, False otherwise."""
|
||||
|
||||
|
||||
stage = None
|
||||
"""
|
||||
A string containing the stage reached in the request-handling process.
|
||||
This is useful when debugging a live server with hung requests."""
|
||||
|
||||
|
||||
namespaces = _cpconfig.NamespaceSet(
|
||||
**{"hooks": hooks_namespace,
|
||||
"request": request_namespace,
|
||||
|
@ -455,11 +474,11 @@ class Request(object):
|
|||
"error_page": error_page_namespace,
|
||||
"tools": cherrypy.tools,
|
||||
})
|
||||
|
||||
|
||||
def __init__(self, local_host, remote_host, scheme="http",
|
||||
server_protocol="HTTP/1.1"):
|
||||
"""Populate a new Request object.
|
||||
|
||||
|
||||
local_host should be an httputil.Host object with the server info.
|
||||
remote_host should be an httputil.Host object with the client info.
|
||||
scheme should be a string, either "http" or "https".
|
||||
|
@ -468,17 +487,17 @@ class Request(object):
|
|||
self.remote = remote_host
|
||||
self.scheme = scheme
|
||||
self.server_protocol = server_protocol
|
||||
|
||||
|
||||
self.closed = False
|
||||
|
||||
|
||||
# Put a *copy* of the class error_page into self.
|
||||
self.error_page = self.error_page.copy()
|
||||
|
||||
|
||||
# Put a *copy* of the class namespaces into self.
|
||||
self.namespaces = self.namespaces.copy()
|
||||
|
||||
|
||||
self.stage = None
|
||||
|
||||
|
||||
def close(self):
|
||||
"""Run cleanup code. (Core)"""
|
||||
if not self.closed:
|
||||
|
@ -486,43 +505,49 @@ class Request(object):
|
|||
self.stage = 'on_end_request'
|
||||
self.hooks.run('on_end_request')
|
||||
self.stage = 'close'
|
||||
|
||||
|
||||
def run(self, method, path, query_string, req_protocol, headers, rfile):
|
||||
"""Process the Request. (Core)
|
||||
|
||||
r"""Process the Request. (Core)
|
||||
|
||||
method, path, query_string, and req_protocol should be pulled directly
|
||||
from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
|
||||
|
||||
|
||||
path
|
||||
This should be %XX-unquoted, but query_string should not be.
|
||||
They both MUST be byte strings, not unicode strings.
|
||||
|
||||
|
||||
When using Python 2, they both MUST be byte strings,
|
||||
not unicode strings.
|
||||
|
||||
When using Python 3, they both MUST be unicode strings,
|
||||
not byte strings, and preferably not bytes \x00-\xFF
|
||||
disguised as unicode.
|
||||
|
||||
headers
|
||||
A list of (name, value) tuples.
|
||||
|
||||
|
||||
rfile
|
||||
A file-like object containing the HTTP request entity.
|
||||
|
||||
|
||||
When run() is done, the returned object should have 3 attributes:
|
||||
|
||||
|
||||
* status, e.g. "200 OK"
|
||||
* header_list, a list of (name, value) tuples
|
||||
* body, an iterable yielding strings
|
||||
|
||||
|
||||
Consumer code (HTTP servers) should then access these response
|
||||
attributes to build the outbound stream.
|
||||
|
||||
|
||||
"""
|
||||
response = cherrypy.serving.response
|
||||
self.stage = 'run'
|
||||
try:
|
||||
self.error_response = cherrypy.HTTPError(500).set_response
|
||||
|
||||
|
||||
self.method = method
|
||||
path = path or "/"
|
||||
self.query_string = query_string or ''
|
||||
self.params = {}
|
||||
|
||||
|
||||
# Compare request and server HTTP protocol versions, in case our
|
||||
# server does not support the requested protocol. Limit our output
|
||||
# to min(req, server). We want the following output:
|
||||
|
@ -539,30 +564,30 @@ class Request(object):
|
|||
sp = int(self.server_protocol[5]), int(self.server_protocol[7])
|
||||
self.protocol = min(rp, sp)
|
||||
response.headers.protocol = self.protocol
|
||||
|
||||
|
||||
# Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
|
||||
url = path
|
||||
if query_string:
|
||||
url += '?' + query_string
|
||||
self.request_line = '%s %s %s' % (method, url, req_protocol)
|
||||
|
||||
|
||||
self.header_list = list(headers)
|
||||
self.headers = httputil.HeaderMap()
|
||||
|
||||
|
||||
self.rfile = rfile
|
||||
self.body = None
|
||||
|
||||
|
||||
self.cookie = SimpleCookie()
|
||||
self.handler = None
|
||||
|
||||
|
||||
# path_info should be the path from the
|
||||
# app root (script_name) to the handler.
|
||||
self.script_name = self.app.script_name
|
||||
self.path_info = pi = path[len(self.script_name):]
|
||||
|
||||
|
||||
self.stage = 'respond'
|
||||
self.respond(pi)
|
||||
|
||||
|
||||
except self.throws:
|
||||
raise
|
||||
except:
|
||||
|
@ -578,24 +603,24 @@ class Request(object):
|
|||
body = ""
|
||||
r = bare_error(body)
|
||||
response.output_status, response.header_list, response.body = r
|
||||
|
||||
|
||||
if self.method == "HEAD":
|
||||
# HEAD requests MUST NOT return a message-body in the response.
|
||||
response.body = []
|
||||
|
||||
|
||||
try:
|
||||
cherrypy.log.access()
|
||||
except:
|
||||
cherrypy.log.error(traceback=True)
|
||||
|
||||
|
||||
if response.timed_out:
|
||||
raise cherrypy.TimeoutError()
|
||||
|
||||
|
||||
return response
|
||||
|
||||
|
||||
# Uncomment for stage debugging
|
||||
# stage = property(lambda self: self._stage, lambda self, v: print(v))
|
||||
|
||||
|
||||
def respond(self, path_info):
|
||||
"""Generate a response for the resource at self.path_info. (Core)"""
|
||||
response = cherrypy.serving.response
|
||||
|
@ -604,30 +629,30 @@ class Request(object):
|
|||
try:
|
||||
if self.app is None:
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
|
||||
# Get the 'Host' header, so we can HTTPRedirect properly.
|
||||
self.stage = 'process_headers'
|
||||
self.process_headers()
|
||||
|
||||
|
||||
# Make a copy of the class hooks
|
||||
self.hooks = self.__class__.hooks.copy()
|
||||
self.toolmaps = {}
|
||||
|
||||
|
||||
self.stage = 'get_resource'
|
||||
self.get_resource(path_info)
|
||||
|
||||
|
||||
self.body = _cpreqbody.RequestBody(
|
||||
self.rfile, self.headers, request_params=self.params)
|
||||
|
||||
|
||||
self.namespaces(self.config)
|
||||
|
||||
|
||||
self.stage = 'on_start_resource'
|
||||
self.hooks.run('on_start_resource')
|
||||
|
||||
|
||||
# Parse the querystring
|
||||
self.stage = 'process_query_string'
|
||||
self.process_query_string()
|
||||
|
||||
|
||||
# Process the body
|
||||
if self.process_request_body:
|
||||
if self.method not in self.methods_with_bodies:
|
||||
|
@ -636,14 +661,14 @@ class Request(object):
|
|||
self.hooks.run('before_request_body')
|
||||
if self.process_request_body:
|
||||
self.body.process()
|
||||
|
||||
|
||||
# Run the handler
|
||||
self.stage = 'before_handler'
|
||||
self.hooks.run('before_handler')
|
||||
if self.handler:
|
||||
self.stage = 'handler'
|
||||
response.body = self.handler()
|
||||
|
||||
|
||||
# Finalize
|
||||
self.stage = 'before_finalize'
|
||||
self.hooks.run('before_finalize')
|
||||
|
@ -663,7 +688,7 @@ class Request(object):
|
|||
if self.throw_errors:
|
||||
raise
|
||||
self.handle_error()
|
||||
|
||||
|
||||
def process_query_string(self):
|
||||
"""Parse the query string into Python structures. (Core)"""
|
||||
try:
|
||||
|
@ -674,14 +699,15 @@ class Request(object):
|
|||
404, "The given query string could not be processed. Query "
|
||||
"strings for this resource must be encoded with %r." %
|
||||
self.query_string_encoding)
|
||||
|
||||
|
||||
# Python 2 only: keyword arguments must be byte strings (type 'str').
|
||||
for key, value in p.items():
|
||||
if isinstance(key, unicode):
|
||||
del p[key]
|
||||
p[key.encode(self.query_string_encoding)] = value
|
||||
if not py3k:
|
||||
for key, value in p.items():
|
||||
if isinstance(key, unicode):
|
||||
del p[key]
|
||||
p[key.encode(self.query_string_encoding)] = value
|
||||
self.params.update(p)
|
||||
|
||||
|
||||
def process_headers(self):
|
||||
"""Parse HTTP header data into Python structures. (Core)"""
|
||||
# Process the headers into self.headers
|
||||
|
@ -691,15 +717,16 @@ class Request(object):
|
|||
# so title doesn't have to be called twice.
|
||||
name = name.title()
|
||||
value = value.strip()
|
||||
|
||||
# Warning: if there is more than one header entry for cookies (AFAIK,
|
||||
# only Konqueror does that), only the last one will remain in headers
|
||||
# (but they will be correctly stored in request.cookie).
|
||||
|
||||
# Warning: if there is more than one header entry for cookies
|
||||
# (AFAIK, only Konqueror does that), only the last one will
|
||||
# remain in headers (but they will be correctly stored in
|
||||
# request.cookie).
|
||||
if "=?" in value:
|
||||
dict.__setitem__(headers, name, httputil.decode_TEXT(value))
|
||||
else:
|
||||
dict.__setitem__(headers, name, value)
|
||||
|
||||
|
||||
# Handle cookies differently because on Konqueror, multiple
|
||||
# cookies come on different lines with the same key
|
||||
if name == 'Cookie':
|
||||
|
@ -708,7 +735,7 @@ class Request(object):
|
|||
except CookieError:
|
||||
msg = "Illegal cookie name %s" % value.split('=')[0]
|
||||
raise cherrypy.HTTPError(400, msg)
|
||||
|
||||
|
||||
if not dict.__contains__(headers, 'Host'):
|
||||
# All Internet-based HTTP/1.1 servers MUST respond with a 400
|
||||
# (Bad Request) status code to any HTTP/1.1 request message
|
||||
|
@ -720,17 +747,18 @@ class Request(object):
|
|||
if not host:
|
||||
host = self.local.name or self.local.ip
|
||||
self.base = "%s://%s" % (self.scheme, host)
|
||||
|
||||
|
||||
def get_resource(self, path):
|
||||
"""Call a dispatcher (which sets self.handler and .config). (Core)"""
|
||||
# First, see if there is a custom dispatch at this URI. Custom
|
||||
# dispatchers can only be specified in app.config, not in _cp_config
|
||||
# (since custom dispatchers may not even have an app.root).
|
||||
dispatch = self.app.find_config(path, "request.dispatch", self.dispatch)
|
||||
|
||||
dispatch = self.app.find_config(
|
||||
path, "request.dispatch", self.dispatch)
|
||||
|
||||
# dispatch() should set self.handler and self.config
|
||||
dispatch(path)
|
||||
|
||||
|
||||
def handle_error(self):
|
||||
"""Handle the last unanticipated exception. (Core)"""
|
||||
try:
|
||||
|
@ -743,18 +771,18 @@ class Request(object):
|
|||
inst = sys.exc_info()[1]
|
||||
inst.set_response()
|
||||
cherrypy.serving.response.finalize()
|
||||
|
||||
|
||||
# ------------------------- Properties ------------------------- #
|
||||
|
||||
|
||||
def _get_body_params(self):
|
||||
warnings.warn(
|
||||
"body_params is deprecated in CherryPy 3.2, will be removed in "
|
||||
"CherryPy 3.3.",
|
||||
DeprecationWarning
|
||||
)
|
||||
"body_params is deprecated in CherryPy 3.2, will be removed in "
|
||||
"CherryPy 3.3.",
|
||||
DeprecationWarning
|
||||
)
|
||||
return self.body.params
|
||||
body_params = property(_get_body_params,
|
||||
doc= """
|
||||
doc="""
|
||||
If the request Content-Type is 'application/x-www-form-urlencoded' or
|
||||
multipart, this will be a dict of the params pulled from the entity
|
||||
body; that is, it will be the portion of request.params that come
|
||||
|
@ -762,23 +790,31 @@ class Request(object):
|
|||
can be sent with various HTTP method verbs). This value is set between
|
||||
the 'before_request_body' and 'before_handler' hooks (assuming that
|
||||
process_request_body is True).
|
||||
|
||||
|
||||
Deprecated in 3.2, will be removed for 3.3 in favor of
|
||||
:attr:`request.body.params<cherrypy._cprequest.RequestBody.params>`.""")
|
||||
|
||||
|
||||
class ResponseBody(object):
|
||||
|
||||
"""The body of the HTTP response (the response entity)."""
|
||||
|
||||
|
||||
if py3k:
|
||||
unicode_err = ("Page handlers MUST return bytes. Use tools.encode "
|
||||
"if you wish to return unicode.")
|
||||
|
||||
def __get__(self, obj, objclass=None):
|
||||
if obj is None:
|
||||
# When calling on the class instead of an instance...
|
||||
return self
|
||||
else:
|
||||
return obj._body
|
||||
|
||||
|
||||
def __set__(self, obj, value):
|
||||
# Convert the given value to an iterable object.
|
||||
if py3k and isinstance(value, str):
|
||||
raise ValueError(self.unicode_err)
|
||||
|
||||
if isinstance(value, basestring):
|
||||
# strings get wrapped in a list because iterating over a single
|
||||
# item list is much faster than iterating over every character
|
||||
|
@ -788,6 +824,11 @@ class ResponseBody(object):
|
|||
else:
|
||||
# [''] doesn't evaluate to False, so replace it with [].
|
||||
value = []
|
||||
elif py3k and isinstance(value, list):
|
||||
# every item in a list must be bytes...
|
||||
for i, item in enumerate(value):
|
||||
if isinstance(item, str):
|
||||
raise ValueError(self.unicode_err)
|
||||
# Don't use isinstance here; io.IOBase which has an ABC takes
|
||||
# 1000 times as long as, say, isinstance(value, str)
|
||||
elif hasattr(value, 'read'):
|
||||
|
@ -798,18 +839,19 @@ class ResponseBody(object):
|
|||
|
||||
|
||||
class Response(object):
|
||||
|
||||
"""An HTTP Response, including status, headers, and body."""
|
||||
|
||||
|
||||
status = ""
|
||||
"""The HTTP Status-Code and Reason-Phrase."""
|
||||
|
||||
|
||||
header_list = []
|
||||
"""
|
||||
A list of the HTTP response headers as (name, value) tuples.
|
||||
In general, you should use response.headers (a dict) instead. This
|
||||
attribute is generated from response.headers and is not valid until
|
||||
after the finalize phase."""
|
||||
|
||||
|
||||
headers = httputil.HeaderMap()
|
||||
"""
|
||||
A dict-like object containing the response headers. Keys are header
|
||||
|
@ -817,36 +859,36 @@ class Response(object):
|
|||
a case-insensitive manner. That is, headers['Content-Type'] and
|
||||
headers['content-type'] refer to the same value. Values are header
|
||||
values (decoded according to :rfc:`2047` if necessary).
|
||||
|
||||
|
||||
.. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
|
||||
"""
|
||||
|
||||
|
||||
cookie = SimpleCookie()
|
||||
"""See help(Cookie)."""
|
||||
|
||||
|
||||
body = ResponseBody()
|
||||
"""The body (entity) of the HTTP response."""
|
||||
|
||||
|
||||
time = None
|
||||
"""The value of time.time() when created. Use in HTTP dates."""
|
||||
|
||||
|
||||
timeout = 300
|
||||
"""Seconds after which the response will be aborted."""
|
||||
|
||||
|
||||
timed_out = False
|
||||
"""
|
||||
Flag to indicate the response should be aborted, because it has
|
||||
exceeded its timeout."""
|
||||
|
||||
|
||||
stream = False
|
||||
"""If False, buffer the response body."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.status = None
|
||||
self.header_list = None
|
||||
self._body = []
|
||||
self.time = time.time()
|
||||
|
||||
|
||||
self.headers = httputil.HeaderMap()
|
||||
# Since we know all our keys are titled strings, we can
|
||||
# bypass HeaderMap.update and get a big speed boost.
|
||||
|
@ -856,28 +898,36 @@ class Response(object):
|
|||
"Date": httputil.HTTPDate(self.time),
|
||||
})
|
||||
self.cookie = SimpleCookie()
|
||||
|
||||
|
||||
def collapse_body(self):
|
||||
"""Collapse self.body to a single string; replace it and return it."""
|
||||
if isinstance(self.body, basestring):
|
||||
return self.body
|
||||
|
||||
newbody = ''.join([chunk for chunk in self.body])
|
||||
|
||||
|
||||
newbody = []
|
||||
for chunk in self.body:
|
||||
if py3k and not isinstance(chunk, bytes):
|
||||
raise TypeError("Chunk %s is not of type 'bytes'." %
|
||||
repr(chunk))
|
||||
newbody.append(chunk)
|
||||
newbody = ntob('').join(newbody)
|
||||
|
||||
self.body = newbody
|
||||
return newbody
|
||||
|
||||
|
||||
def finalize(self):
|
||||
"""Transform headers (and cookies) into self.header_list. (Core)"""
|
||||
try:
|
||||
code, reason, _ = httputil.valid_status(self.status)
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
|
||||
|
||||
|
||||
headers = self.headers
|
||||
|
||||
self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason)
|
||||
|
||||
|
||||
self.status = "%s %s" % (code, reason)
|
||||
self.output_status = ntob(str(code), 'ascii') + \
|
||||
ntob(" ") + headers.encode(reason)
|
||||
|
||||
if self.stream:
|
||||
# The upshot: wsgiserver will chunk the response if
|
||||
# you pop Content-Length (or set it explicitly to None).
|
||||
|
@ -896,10 +946,10 @@ class Response(object):
|
|||
if dict.get(headers, 'Content-Length') is None:
|
||||
content = self.collapse_body()
|
||||
dict.__setitem__(headers, 'Content-Length', len(content))
|
||||
|
||||
|
||||
# Transform our header dict into a list of tuples.
|
||||
self.header_list = h = headers.output()
|
||||
|
||||
|
||||
cookie = self.cookie.output()
|
||||
if cookie:
|
||||
for line in cookie.split("\n"):
|
||||
|
@ -912,15 +962,12 @@ class Response(object):
|
|||
if isinstance(value, unicodestr):
|
||||
value = headers.encode(value)
|
||||
h.append((name, value))
|
||||
|
||||
|
||||
def check_timeout(self):
|
||||
"""If now > self.time + self.timeout, set self.timed_out.
|
||||
|
||||
|
||||
This purposefully sets a flag, rather than raising an error,
|
||||
so that a monitor thread can interrupt the Response thread.
|
||||
"""
|
||||
if time.time() > self.time + self.timeout:
|
||||
self.timed_out = True
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import warnings
|
|||
|
||||
import cherrypy
|
||||
from cherrypy.lib import attributes
|
||||
from cherrypy._cpcompat import basestring
|
||||
from cherrypy._cpcompat import basestring, py3k
|
||||
|
||||
# We import * because we want to export check_port
|
||||
# et al as attributes of this module.
|
||||
|
@ -12,102 +12,129 @@ from cherrypy.process.servers import *
|
|||
|
||||
|
||||
class Server(ServerAdapter):
|
||||
|
||||
"""An adapter for an HTTP server.
|
||||
|
||||
|
||||
You can set attributes (like socket_host and socket_port)
|
||||
on *this* object (which is probably cherrypy.server), and call
|
||||
quickstart. For example::
|
||||
|
||||
|
||||
cherrypy.server.socket_port = 80
|
||||
cherrypy.quickstart()
|
||||
"""
|
||||
|
||||
|
||||
socket_port = 8080
|
||||
"""The TCP port on which to listen for connections."""
|
||||
|
||||
|
||||
_socket_host = '127.0.0.1'
|
||||
|
||||
def _get_socket_host(self):
|
||||
return self._socket_host
|
||||
|
||||
def _set_socket_host(self, value):
|
||||
if value == '':
|
||||
raise ValueError("The empty string ('') is not an allowed value. "
|
||||
"Use '0.0.0.0' instead to listen on all active "
|
||||
"interfaces (INADDR_ANY).")
|
||||
self._socket_host = value
|
||||
socket_host = property(_get_socket_host, _set_socket_host,
|
||||
socket_host = property(
|
||||
_get_socket_host,
|
||||
_set_socket_host,
|
||||
doc="""The hostname or IP address on which to listen for connections.
|
||||
|
||||
|
||||
Host values may be any IPv4 or IPv6 address, or any valid hostname.
|
||||
The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
|
||||
your hosts file prefers IPv6). The string '0.0.0.0' is a special
|
||||
IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
|
||||
is the similar IN6ADDR_ANY for IPv6. The empty string or None are
|
||||
not allowed.""")
|
||||
|
||||
|
||||
socket_file = None
|
||||
"""If given, the name of the UNIX socket to use instead of TCP/IP.
|
||||
|
||||
|
||||
When this option is not None, the `socket_host` and `socket_port` options
|
||||
are ignored."""
|
||||
|
||||
|
||||
socket_queue_size = 5
|
||||
"""The 'backlog' argument to socket.listen(); specifies the maximum number
|
||||
of queued connections (default 5)."""
|
||||
|
||||
|
||||
socket_timeout = 10
|
||||
"""The timeout in seconds for accepted connections (default 10)."""
|
||||
|
||||
accepted_queue_size = -1
|
||||
"""The maximum number of requests which will be queued up before
|
||||
the server refuses to accept it (default -1, meaning no limit)."""
|
||||
|
||||
accepted_queue_timeout = 10
|
||||
"""The timeout in seconds for attempting to add a request to the
|
||||
queue when the queue is full (default 10)."""
|
||||
|
||||
shutdown_timeout = 5
|
||||
"""The time to wait for HTTP worker threads to clean up."""
|
||||
|
||||
|
||||
protocol_version = 'HTTP/1.1'
|
||||
"""The version string to write in the Status-Line of all HTTP responses,
|
||||
for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
|
||||
this should also limit the supported features used in the response."""
|
||||
|
||||
|
||||
thread_pool = 10
|
||||
"""The number of worker threads to start up in the pool."""
|
||||
|
||||
|
||||
thread_pool_max = -1
|
||||
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit."""
|
||||
|
||||
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit.
|
||||
"""
|
||||
|
||||
max_request_header_size = 500 * 1024
|
||||
"""The maximum number of bytes allowable in the request headers. If exceeded,
|
||||
the HTTP server should return "413 Request Entity Too Large"."""
|
||||
|
||||
"""The maximum number of bytes allowable in the request headers.
|
||||
If exceeded, the HTTP server should return "413 Request Entity Too Large".
|
||||
"""
|
||||
|
||||
max_request_body_size = 100 * 1024 * 1024
|
||||
"""The maximum number of bytes allowable in the request body. If exceeded,
|
||||
the HTTP server should return "413 Request Entity Too Large"."""
|
||||
|
||||
|
||||
instance = None
|
||||
"""If not None, this should be an HTTP server instance (such as
|
||||
CPWSGIServer) which cherrypy.server will control. Use this when you need
|
||||
more control over object instantiation than is available in the various
|
||||
configuration options."""
|
||||
|
||||
|
||||
ssl_context = None
|
||||
"""When using PyOpenSSL, an instance of SSL.Context."""
|
||||
|
||||
|
||||
ssl_certificate = None
|
||||
"""The filename of the SSL certificate to use."""
|
||||
|
||||
|
||||
ssl_certificate_chain = None
|
||||
"""When using PyOpenSSL, the certificate chain to pass to
|
||||
Context.load_verify_locations."""
|
||||
|
||||
|
||||
ssl_private_key = None
|
||||
"""The filename of the private key to use with SSL."""
|
||||
|
||||
ssl_module = 'pyopenssl'
|
||||
"""The name of a registered SSL adaptation module to use with the builtin
|
||||
WSGI server. Builtin options are 'builtin' (to use the SSL library built
|
||||
into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL
|
||||
project, which you must install separately). You may also register your
|
||||
own classes in the wsgiserver.ssl_adapters dict."""
|
||||
|
||||
|
||||
if py3k:
|
||||
ssl_module = 'builtin'
|
||||
"""The name of a registered SSL adaptation module to use with
|
||||
the builtin WSGI server. Builtin options are: 'builtin' (to
|
||||
use the SSL library built into recent versions of Python).
|
||||
You may also register your own classes in the
|
||||
wsgiserver.ssl_adapters dict."""
|
||||
else:
|
||||
ssl_module = 'pyopenssl'
|
||||
"""The name of a registered SSL adaptation module to use with the
|
||||
builtin WSGI server. Builtin options are 'builtin' (to use the SSL
|
||||
library built into recent versions of Python) and 'pyopenssl' (to
|
||||
use the PyOpenSSL project, which you must install separately). You
|
||||
may also register your own classes in the wsgiserver.ssl_adapters
|
||||
dict."""
|
||||
|
||||
statistics = False
|
||||
"""Turns statistics-gathering on or off for aware HTTP servers."""
|
||||
|
||||
nodelay = True
|
||||
"""If True (the default since 3.1), sets the TCP_NODELAY socket option."""
|
||||
|
||||
|
||||
wsgi_version = (1, 0)
|
||||
"""The WSGI version tuple to use with the builtin WSGI server.
|
||||
The provided options are (1, 0) [which includes support for PEP 3333,
|
||||
|
@ -115,13 +142,13 @@ class Server(ServerAdapter):
|
|||
wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
|
||||
You may create and register your own experimental versions of the WSGI
|
||||
protocol by adding custom classes to the wsgiserver.wsgi_gateways dict."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.bus = cherrypy.engine
|
||||
self.httpserver = None
|
||||
self.interrupt = None
|
||||
self.running = False
|
||||
|
||||
|
||||
def httpserver_from_self(self, httpserver=None):
|
||||
"""Return a (httpserver, bind_addr) pair based on self attributes."""
|
||||
if httpserver is None:
|
||||
|
@ -133,20 +160,21 @@ class Server(ServerAdapter):
|
|||
# Is anyone using this? Can I add an arg?
|
||||
httpserver = attributes(httpserver)(self)
|
||||
return httpserver, self.bind_addr
|
||||
|
||||
|
||||
def start(self):
|
||||
"""Start the HTTP server."""
|
||||
if not self.httpserver:
|
||||
self.httpserver, self.bind_addr = self.httpserver_from_self()
|
||||
ServerAdapter.start(self)
|
||||
start.priority = 75
|
||||
|
||||
|
||||
def _get_bind_addr(self):
|
||||
if self.socket_file:
|
||||
return self.socket_file
|
||||
if self.socket_host is None and self.socket_port is None:
|
||||
return None
|
||||
return (self.socket_host, self.socket_port)
|
||||
|
||||
def _set_bind_addr(self, value):
|
||||
if value is None:
|
||||
self.socket_file = None
|
||||
|
@ -164,14 +192,18 @@ class Server(ServerAdapter):
|
|||
raise ValueError("bind_addr must be a (host, port) tuple "
|
||||
"(for TCP sockets) or a string (for Unix "
|
||||
"domain sockets), not %r" % value)
|
||||
bind_addr = property(_get_bind_addr, _set_bind_addr,
|
||||
doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.')
|
||||
|
||||
bind_addr = property(
|
||||
_get_bind_addr,
|
||||
_set_bind_addr,
|
||||
doc='A (host, port) tuple for TCP sockets or '
|
||||
'a str for Unix domain sockets.')
|
||||
|
||||
def base(self):
|
||||
"""Return the base (scheme://host[:port] or sock file) for this server."""
|
||||
"""Return the base (scheme://host[:port] or sock file) for this server.
|
||||
"""
|
||||
if self.socket_file:
|
||||
return self.socket_file
|
||||
|
||||
|
||||
host = self.socket_host
|
||||
if host in ('0.0.0.0', '::'):
|
||||
# 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
|
||||
|
@ -179,9 +211,9 @@ class Server(ServerAdapter):
|
|||
# safest thing to spit out in a URL.
|
||||
import socket
|
||||
host = socket.gethostname()
|
||||
|
||||
|
||||
port = self.socket_port
|
||||
|
||||
|
||||
if self.ssl_certificate:
|
||||
scheme = "https"
|
||||
if port != 443:
|
||||
|
@ -190,6 +222,5 @@ class Server(ServerAdapter):
|
|||
scheme = "http"
|
||||
if port != 80:
|
||||
host += ":%s" % port
|
||||
|
||||
return "%s://%s" % (scheme, host)
|
||||
|
||||
return "%s://%s" % (scheme, host)
|
||||
|
|
|
@ -137,6 +137,7 @@ affects what we see:
|
|||
|
||||
# Threading import is at end
|
||||
|
||||
|
||||
class _localbase(object):
|
||||
__slots__ = '_local__key', '_local__args', '_local__lock'
|
||||
|
||||
|
@ -158,6 +159,7 @@ class _localbase(object):
|
|||
|
||||
return self
|
||||
|
||||
|
||||
def _patch(self):
|
||||
key = object.__getattribute__(self, '_local__key')
|
||||
d = currentThread().__dict__.get(key)
|
||||
|
@ -175,6 +177,7 @@ def _patch(self):
|
|||
else:
|
||||
object.__setattr__(self, '__dict__', d)
|
||||
|
||||
|
||||
class local(_localbase):
|
||||
|
||||
def __getattribute__(self, name):
|
||||
|
@ -204,7 +207,6 @@ class local(_localbase):
|
|||
finally:
|
||||
lock.release()
|
||||
|
||||
|
||||
def __del__():
|
||||
threading_enumerate = enumerate
|
||||
__getattribute__ = object.__getattribute__
|
||||
|
@ -231,7 +233,7 @@ class local(_localbase):
|
|||
try:
|
||||
del __dict__[key]
|
||||
except KeyError:
|
||||
pass # didn't have anything in this thread
|
||||
pass # didn't have anything in this thread
|
||||
|
||||
return __del__
|
||||
__del__ = __del__()
|
||||
|
|
|
@ -2,18 +2,18 @@
|
|||
|
||||
Tools are usually designed to be used in a variety of ways (although some
|
||||
may only offer one if they choose):
|
||||
|
||||
|
||||
Library calls
|
||||
All tools are callables that can be used wherever needed.
|
||||
The arguments are straightforward and should be detailed within the
|
||||
docstring.
|
||||
|
||||
|
||||
Function decorators
|
||||
All tools, when called, may be used as decorators which configure
|
||||
individual CherryPy page handlers (methods on the CherryPy tree).
|
||||
That is, "@tools.anytool()" should "turn on" the tool via the
|
||||
decorated function's _cp_config attribute.
|
||||
|
||||
|
||||
CherryPy config
|
||||
If a tool exposes a "_setup" callable, it will be called
|
||||
once per Request (if the feature is "turned on" via config).
|
||||
|
@ -43,17 +43,21 @@ def _getargs(func):
|
|||
return co.co_varnames[:co.co_argcount]
|
||||
|
||||
|
||||
_attr_error = ("CherryPy Tools cannot be turned on directly. Instead, turn them "
|
||||
"on via config, or use them as decorators on your page handlers.")
|
||||
_attr_error = (
|
||||
"CherryPy Tools cannot be turned on directly. Instead, turn them "
|
||||
"on via config, or use them as decorators on your page handlers."
|
||||
)
|
||||
|
||||
|
||||
class Tool(object):
|
||||
|
||||
"""A registered function for use with CherryPy request-processing hooks.
|
||||
|
||||
|
||||
help(tool.callable) should give you more information about this Tool.
|
||||
"""
|
||||
|
||||
|
||||
namespace = "tools"
|
||||
|
||||
|
||||
def __init__(self, point, callable, name=None, priority=50):
|
||||
self._point = point
|
||||
self.callable = callable
|
||||
|
@ -61,13 +65,14 @@ class Tool(object):
|
|||
self._priority = priority
|
||||
self.__doc__ = self.callable.__doc__
|
||||
self._setargs()
|
||||
|
||||
|
||||
def _get_on(self):
|
||||
raise AttributeError(_attr_error)
|
||||
|
||||
def _set_on(self, value):
|
||||
raise AttributeError(_attr_error)
|
||||
on = property(_get_on, _set_on)
|
||||
|
||||
|
||||
def _setargs(self):
|
||||
"""Copy func parameter names to obj attributes."""
|
||||
try:
|
||||
|
@ -86,28 +91,28 @@ class Tool(object):
|
|||
# but if we trap it here it doesn't prevent CP from working.
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
def _merged_args(self, d=None):
|
||||
"""Return a dict of configuration entries for this Tool."""
|
||||
if d:
|
||||
conf = d.copy()
|
||||
else:
|
||||
conf = {}
|
||||
|
||||
|
||||
tm = cherrypy.serving.request.toolmaps[self.namespace]
|
||||
if self._name in tm:
|
||||
conf.update(tm[self._name])
|
||||
|
||||
|
||||
if "on" in conf:
|
||||
del conf["on"]
|
||||
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Compile-time decorator (turn on the tool in config).
|
||||
|
||||
|
||||
For example::
|
||||
|
||||
|
||||
@tools.proxy()
|
||||
def whats_my_base(self):
|
||||
return cherrypy.request.base
|
||||
|
@ -117,6 +122,7 @@ class Tool(object):
|
|||
raise TypeError("The %r Tool does not accept positional "
|
||||
"arguments; you must use keyword arguments."
|
||||
% self._name)
|
||||
|
||||
def tool_decorator(f):
|
||||
if not hasattr(f, "_cp_config"):
|
||||
f._cp_config = {}
|
||||
|
@ -126,10 +132,10 @@ class Tool(object):
|
|||
f._cp_config[subspace + k] = v
|
||||
return f
|
||||
return tool_decorator
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
|
@ -142,8 +148,9 @@ class Tool(object):
|
|||
|
||||
|
||||
class HandlerTool(Tool):
|
||||
|
||||
"""Tool which is called 'before main', that may skip normal handlers.
|
||||
|
||||
|
||||
If the tool successfully handles the request (by setting response.body),
|
||||
if should return True. This will cause CherryPy to skip any 'normal' page
|
||||
handler. If the tool did not handle the request, it should return False
|
||||
|
@ -151,15 +158,15 @@ class HandlerTool(Tool):
|
|||
tool is declared AS a page handler (see the 'handler' method), returning
|
||||
False will raise NotFound.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
Tool.__init__(self, 'before_handler', callable, name)
|
||||
|
||||
|
||||
def handler(self, *args, **kwargs):
|
||||
"""Use this tool as a CherryPy page handler.
|
||||
|
||||
|
||||
For example::
|
||||
|
||||
|
||||
class Root:
|
||||
nav = tools.staticdir.handler(section="/nav", dir="nav",
|
||||
root=absDir)
|
||||
|
@ -171,14 +178,14 @@ class HandlerTool(Tool):
|
|||
return cherrypy.serving.response.body
|
||||
handle_func.exposed = True
|
||||
return handle_func
|
||||
|
||||
|
||||
def _wrapper(self, **kwargs):
|
||||
if self.callable(**kwargs):
|
||||
cherrypy.serving.request.handler = None
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
|
@ -191,16 +198,17 @@ class HandlerTool(Tool):
|
|||
|
||||
|
||||
class HandlerWrapperTool(Tool):
|
||||
|
||||
"""Tool which wraps request.handler in a provided wrapper function.
|
||||
|
||||
|
||||
The 'newhandler' arg must be a handler wrapper function that takes a
|
||||
'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
|
||||
page handler
|
||||
functions, it must return an iterable for use as cherrypy.response.body.
|
||||
|
||||
|
||||
For example, to allow your 'inner' page handlers to return dicts
|
||||
which then get interpolated into a template::
|
||||
|
||||
|
||||
def interpolator(next_handler, *args, **kwargs):
|
||||
filename = cherrypy.request.config.get('template')
|
||||
cherrypy.response.template = env.get_template(filename)
|
||||
|
@ -208,32 +216,35 @@ class HandlerWrapperTool(Tool):
|
|||
return cherrypy.response.template.render(**response_dict)
|
||||
cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
|
||||
"""
|
||||
|
||||
def __init__(self, newhandler, point='before_handler', name=None, priority=50):
|
||||
|
||||
def __init__(self, newhandler, point='before_handler', name=None,
|
||||
priority=50):
|
||||
self.newhandler = newhandler
|
||||
self._point = point
|
||||
self._name = name
|
||||
self._priority = priority
|
||||
|
||||
def callable(self, debug=False):
|
||||
|
||||
def callable(self, *args, **kwargs):
|
||||
innerfunc = cherrypy.serving.request.handler
|
||||
|
||||
def wrap(*args, **kwargs):
|
||||
return self.newhandler(innerfunc, *args, **kwargs)
|
||||
cherrypy.serving.request.handler = wrap
|
||||
|
||||
|
||||
class ErrorTool(Tool):
|
||||
|
||||
"""Tool which is used to replace the default request.error_response."""
|
||||
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
Tool.__init__(self, None, callable, name)
|
||||
|
||||
|
||||
def _wrapper(self):
|
||||
self.callable(**self._merged_args())
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
|
@ -243,51 +254,53 @@ class ErrorTool(Tool):
|
|||
# Builtin tools #
|
||||
|
||||
from cherrypy.lib import cptools, encoding, auth, static, jsontools
|
||||
from cherrypy.lib import sessions as _sessions, xmlrpc as _xmlrpc
|
||||
from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc
|
||||
from cherrypy.lib import caching as _caching
|
||||
from cherrypy.lib import auth_basic, auth_digest
|
||||
|
||||
|
||||
class SessionTool(Tool):
|
||||
|
||||
"""Session Tool for CherryPy.
|
||||
|
||||
|
||||
sessions.locking
|
||||
When 'implicit' (the default), the session will be locked for you,
|
||||
just before running the page handler.
|
||||
|
||||
|
||||
When 'early', the session will be locked before reading the request
|
||||
body. This is off by default for safety reasons; for example,
|
||||
a large upload would block the session, denying an AJAX
|
||||
progress meter (see http://www.cherrypy.org/ticket/630).
|
||||
|
||||
progress meter
|
||||
(`issue <https://bitbucket.org/cherrypy/cherrypy/issue/630>`_).
|
||||
|
||||
When 'explicit' (or any other value), you need to call
|
||||
cherrypy.session.acquire_lock() yourself before using
|
||||
session data.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
# _sessions.init must be bound after headers are read
|
||||
Tool.__init__(self, 'before_request_body', _sessions.init)
|
||||
|
||||
|
||||
def _lock_session(self):
|
||||
cherrypy.serving.session.acquire_lock()
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
hooks = cherrypy.serving.request.hooks
|
||||
|
||||
|
||||
conf = self._merged_args()
|
||||
|
||||
|
||||
p = conf.pop("priority", None)
|
||||
if p is None:
|
||||
p = getattr(self.callable, "priority", self._priority)
|
||||
|
||||
|
||||
hooks.attach(self._point, self.callable, priority=p, **conf)
|
||||
|
||||
|
||||
locking = conf.pop('locking', 'implicit')
|
||||
if locking == 'implicit':
|
||||
hooks.attach('before_handler', self._lock_session)
|
||||
|
@ -298,15 +311,15 @@ class SessionTool(Tool):
|
|||
else:
|
||||
# Don't lock
|
||||
pass
|
||||
|
||||
|
||||
hooks.attach('before_finalize', _sessions.save)
|
||||
hooks.attach('on_end_request', _sessions.close)
|
||||
|
||||
|
||||
def regenerate(self):
|
||||
"""Drop the current session and make a new one (with a new id)."""
|
||||
sess = cherrypy.serving.session
|
||||
sess.regenerate()
|
||||
|
||||
|
||||
# Grab cookie-relevant tool args
|
||||
conf = dict([(k, v) for k, v in self._merged_args().items()
|
||||
if k in ('path', 'path_header', 'name', 'timeout',
|
||||
|
@ -314,19 +327,18 @@ class SessionTool(Tool):
|
|||
_sessions.set_response_cookie(**conf)
|
||||
|
||||
|
||||
|
||||
|
||||
class XMLRPCController(object):
|
||||
|
||||
"""A Controller (page handler collection) for XML-RPC.
|
||||
|
||||
|
||||
To use it, have your controllers subclass this base class (it will
|
||||
turn on the tool for you).
|
||||
|
||||
|
||||
You can also supply the following optional config entries::
|
||||
|
||||
|
||||
tools.xmlrpc.encoding: 'utf-8'
|
||||
tools.xmlrpc.allow_none: 0
|
||||
|
||||
|
||||
XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
|
||||
appropriate handler must first be performed according to the URL, and
|
||||
then a second dispatch step must take place according to the RPC method
|
||||
|
@ -334,42 +346,42 @@ class XMLRPCController(object):
|
|||
prefix in the URL, supplies its own handler args in the body, and
|
||||
requires a 200 OK "Fault" response instead of 404 when the desired
|
||||
method is not found.
|
||||
|
||||
|
||||
Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
|
||||
This Controller acts as the dispatch target for the first half (based
|
||||
on the URL); it then reads the RPC method from the request body and
|
||||
does its own second dispatch step based on that method. It also reads
|
||||
body params, and returns a Fault on error.
|
||||
|
||||
|
||||
The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
|
||||
in your URL's, you can safely skip turning on the XMLRPCDispatcher.
|
||||
Otherwise, you need to use declare it in config::
|
||||
|
||||
|
||||
request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
|
||||
"""
|
||||
|
||||
|
||||
# Note we're hard-coding this into the 'tools' namespace. We could do
|
||||
# a huge amount of work to make it relocatable, but the only reason why
|
||||
# would be if someone actually disabled the default_toolbox. Meh.
|
||||
_cp_config = {'tools.xmlrpc.on': True}
|
||||
|
||||
|
||||
def default(self, *vpath, **params):
|
||||
rpcparams, rpcmethod = _xmlrpc.process_body()
|
||||
|
||||
|
||||
subhandler = self
|
||||
for attr in str(rpcmethod).split('.'):
|
||||
subhandler = getattr(subhandler, attr, None)
|
||||
|
||||
|
||||
if subhandler and getattr(subhandler, "exposed", False):
|
||||
body = subhandler(*(vpath + rpcparams), **params)
|
||||
|
||||
|
||||
else:
|
||||
# http://www.cherrypy.org/ticket/533
|
||||
# https://bitbucket.org/cherrypy/cherrypy/issue/533
|
||||
# if a method is not found, an xmlrpclib.Fault should be returned
|
||||
# raising an exception here will do that; see
|
||||
# cherrypy.lib.xmlrpc.on_error
|
||||
# cherrypy.lib.xmlrpcutil.on_error
|
||||
raise Exception('method "%s" is not supported' % attr)
|
||||
|
||||
|
||||
conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {})
|
||||
_xmlrpc.respond(body,
|
||||
conf.get('encoding', 'utf-8'),
|
||||
|
@ -379,7 +391,7 @@ class XMLRPCController(object):
|
|||
|
||||
|
||||
class SessionAuthTool(HandlerTool):
|
||||
|
||||
|
||||
def _setargs(self):
|
||||
for name in dir(cptools.SessionAuth):
|
||||
if not name.startswith("__"):
|
||||
|
@ -387,8 +399,9 @@ class SessionAuthTool(HandlerTool):
|
|||
|
||||
|
||||
class CachingTool(Tool):
|
||||
|
||||
"""Caching Tool for CherryPy."""
|
||||
|
||||
|
||||
def _wrapper(self, **kwargs):
|
||||
request = cherrypy.serving.request
|
||||
if _caching.get(**kwargs):
|
||||
|
@ -397,29 +410,29 @@ class CachingTool(Tool):
|
|||
if request.cacheable:
|
||||
# Note the devious technique here of adding hooks on the fly
|
||||
request.hooks.attach('before_finalize', _caching.tee_output,
|
||||
priority = 90)
|
||||
priority=90)
|
||||
_wrapper.priority = 20
|
||||
|
||||
|
||||
def _setup(self):
|
||||
"""Hook caching into cherrypy.request."""
|
||||
conf = self._merged_args()
|
||||
|
||||
|
||||
p = conf.pop("priority", None)
|
||||
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
|
||||
priority=p, **conf)
|
||||
|
||||
|
||||
|
||||
class Toolbox(object):
|
||||
|
||||
"""A collection of Tools.
|
||||
|
||||
|
||||
This object also functions as a config namespace handler for itself.
|
||||
Custom toolboxes should be added to each Application's toolboxes dict.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# If the Tool._name is None, supply it from the attribute name.
|
||||
if isinstance(value, Tool):
|
||||
|
@ -427,16 +440,17 @@ class Toolbox(object):
|
|||
value._name = name
|
||||
value.namespace = self.namespace
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
|
||||
def __enter__(self):
|
||||
"""Populate request.toolmaps from tools specified in config."""
|
||||
cherrypy.serving.request.toolmaps[self.namespace] = map = {}
|
||||
|
||||
def populate(k, v):
|
||||
toolname, arg = k.split(".", 1)
|
||||
bucket = map.setdefault(toolname, {})
|
||||
bucket[arg] = v
|
||||
return populate
|
||||
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Run tool._setup() for each tool in our toolmap."""
|
||||
map = cherrypy.serving.request.toolmaps.get(self.namespace)
|
||||
|
@ -448,21 +462,22 @@ class Toolbox(object):
|
|||
|
||||
|
||||
class DeprecatedTool(Tool):
|
||||
|
||||
|
||||
_name = None
|
||||
warnmsg = "This Tool is deprecated."
|
||||
|
||||
|
||||
def __init__(self, point, warnmsg=None):
|
||||
self.point = point
|
||||
if warnmsg is not None:
|
||||
self.warnmsg = warnmsg
|
||||
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
warnings.warn(self.warnmsg)
|
||||
|
||||
def tool_decorator(f):
|
||||
return f
|
||||
return tool_decorator
|
||||
|
||||
|
||||
def _setup(self):
|
||||
warnings.warn(self.warnmsg)
|
||||
|
||||
|
@ -487,12 +502,16 @@ _d.sessions = SessionTool()
|
|||
_d.xmlrpc = ErrorTool(_xmlrpc.on_error)
|
||||
_d.caching = CachingTool('before_handler', _caching.get, 'caching')
|
||||
_d.expires = Tool('before_finalize', _caching.expires)
|
||||
_d.tidy = DeprecatedTool('before_finalize',
|
||||
"The tidy tool has been removed from the standard distribution of CherryPy. "
|
||||
"The most recent version can be found at http://tools.cherrypy.org/browser.")
|
||||
_d.nsgmls = DeprecatedTool('before_finalize',
|
||||
"The nsgmls tool has been removed from the standard distribution of CherryPy. "
|
||||
"The most recent version can be found at http://tools.cherrypy.org/browser.")
|
||||
_d.tidy = DeprecatedTool(
|
||||
'before_finalize',
|
||||
"The tidy tool has been removed from the standard distribution of "
|
||||
"CherryPy. The most recent version can be found at "
|
||||
"http://tools.cherrypy.org/browser.")
|
||||
_d.nsgmls = DeprecatedTool(
|
||||
'before_finalize',
|
||||
"The nsgmls tool has been removed from the standard distribution of "
|
||||
"CherryPy. The most recent version can be found at "
|
||||
"http://tools.cherrypy.org/browser.")
|
||||
_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
|
||||
_d.referer = Tool('before_request_body', cptools.referer)
|
||||
_d.basic_auth = Tool('on_start_resource', auth.basic_auth)
|
||||
|
|
|
@ -1,108 +1,114 @@
|
|||
"""CherryPy Application and Tree objects."""
|
||||
|
||||
import os
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntou
|
||||
from cherrypy._cpcompat import ntou, py3k
|
||||
from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
|
||||
class Application(object):
|
||||
|
||||
"""A CherryPy Application.
|
||||
|
||||
|
||||
Servers and gateways should not instantiate Request objects directly.
|
||||
Instead, they should ask an Application object for a request object.
|
||||
|
||||
|
||||
An instance of this class may also be used as a WSGI callable
|
||||
(WSGI application object) for itself.
|
||||
"""
|
||||
|
||||
|
||||
root = None
|
||||
"""The top-most container of page handlers for this app. Handlers should
|
||||
be arranged in a hierarchy of attributes, matching the expected URI
|
||||
hierarchy; the default dispatcher then searches this hierarchy for a
|
||||
matching handler. When using a dispatcher other than the default,
|
||||
this value may be None."""
|
||||
|
||||
|
||||
config = {}
|
||||
"""A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
|
||||
of {key: value} pairs."""
|
||||
|
||||
|
||||
namespaces = _cpconfig.NamespaceSet()
|
||||
toolboxes = {'tools': cherrypy.tools}
|
||||
|
||||
|
||||
log = None
|
||||
"""A LogManager instance. See _cplogging."""
|
||||
|
||||
|
||||
wsgiapp = None
|
||||
"""A CPWSGIApp instance. See _cpwsgi."""
|
||||
|
||||
|
||||
request_class = _cprequest.Request
|
||||
response_class = _cprequest.Response
|
||||
|
||||
|
||||
relative_urls = False
|
||||
|
||||
|
||||
def __init__(self, root, script_name="", config=None):
|
||||
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
|
||||
self.root = root
|
||||
self.script_name = script_name
|
||||
self.wsgiapp = _cpwsgi.CPWSGIApp(self)
|
||||
|
||||
|
||||
self.namespaces = self.namespaces.copy()
|
||||
self.namespaces["log"] = lambda k, v: setattr(self.log, k, v)
|
||||
self.namespaces["wsgi"] = self.wsgiapp.namespace_handler
|
||||
|
||||
|
||||
self.config = self.__class__.config.copy()
|
||||
if config:
|
||||
self.merge(config)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__,
|
||||
self.root, self.script_name)
|
||||
|
||||
script_name_doc = """The URI "mount point" for this app. A mount point is that portion of
|
||||
the URI which is constant for all URIs that are serviced by this
|
||||
application; it does not include scheme, host, or proxy ("virtual host")
|
||||
portions of the URI.
|
||||
|
||||
|
||||
script_name_doc = """The URI "mount point" for this app. A mount point
|
||||
is that portion of the URI which is constant for all URIs that are
|
||||
serviced by this application; it does not include scheme, host, or proxy
|
||||
("virtual host") portions of the URI.
|
||||
|
||||
For example, if script_name is "/my/cool/app", then the URL
|
||||
"http://www.example.com/my/cool/app/page1" might be handled by a
|
||||
"page1" method on the root object.
|
||||
|
||||
|
||||
The value of script_name MUST NOT end in a slash. If the script_name
|
||||
refers to the root of the URI, it MUST be an empty string (not "/").
|
||||
|
||||
|
||||
If script_name is explicitly set to None, then the script_name will be
|
||||
provided for each call from request.wsgi_environ['SCRIPT_NAME'].
|
||||
"""
|
||||
|
||||
def _get_script_name(self):
|
||||
if self._script_name is None:
|
||||
# None signals that the script name should be pulled from WSGI environ.
|
||||
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/")
|
||||
return self._script_name
|
||||
if self._script_name is not None:
|
||||
return self._script_name
|
||||
|
||||
# A `_script_name` with a value of None signals that the script name
|
||||
# should be pulled from WSGI environ.
|
||||
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/")
|
||||
|
||||
def _set_script_name(self, value):
|
||||
if value:
|
||||
value = value.rstrip("/")
|
||||
self._script_name = value
|
||||
script_name = property(fget=_get_script_name, fset=_set_script_name,
|
||||
doc=script_name_doc)
|
||||
|
||||
|
||||
def merge(self, config):
|
||||
"""Merge the given config into self.config."""
|
||||
_cpconfig.merge(self.config, config)
|
||||
|
||||
|
||||
# Handle namespaces specified in config.
|
||||
self.namespaces(self.config.get("/", {}))
|
||||
|
||||
|
||||
def find_config(self, path, key, default=None):
|
||||
"""Return the most-specific value for key along path, or default."""
|
||||
trail = path or "/"
|
||||
while trail:
|
||||
nodeconf = self.config.get(trail, {})
|
||||
|
||||
|
||||
if key in nodeconf:
|
||||
return nodeconf[key]
|
||||
|
||||
|
||||
lastslash = trail.rfind("/")
|
||||
if lastslash == -1:
|
||||
break
|
||||
|
@ -110,78 +116,79 @@ class Application(object):
|
|||
trail = "/"
|
||||
else:
|
||||
trail = trail[:lastslash]
|
||||
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def get_serving(self, local, remote, scheme, sproto):
|
||||
"""Create and return a Request and Response object."""
|
||||
req = self.request_class(local, remote, scheme, sproto)
|
||||
req.app = self
|
||||
|
||||
|
||||
for name, toolbox in self.toolboxes.items():
|
||||
req.namespaces[name] = toolbox
|
||||
|
||||
|
||||
resp = self.response_class()
|
||||
cherrypy.serving.load(req, resp)
|
||||
cherrypy.engine.timeout_monitor.acquire()
|
||||
cherrypy.engine.publish('acquire_thread')
|
||||
|
||||
cherrypy.engine.publish('before_request')
|
||||
|
||||
return req, resp
|
||||
|
||||
|
||||
def release_serving(self):
|
||||
"""Release the current serving (request and response)."""
|
||||
req = cherrypy.serving.request
|
||||
|
||||
cherrypy.engine.timeout_monitor.release()
|
||||
|
||||
|
||||
cherrypy.engine.publish('after_request')
|
||||
|
||||
try:
|
||||
req.close()
|
||||
except:
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
|
||||
|
||||
cherrypy.serving.clear()
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.wsgiapp(environ, start_response)
|
||||
|
||||
|
||||
class Tree(object):
|
||||
|
||||
"""A registry of CherryPy applications, mounted at diverse points.
|
||||
|
||||
|
||||
An instance of this class may also be used as a WSGI callable
|
||||
(WSGI application object), in which case it dispatches to all
|
||||
mounted apps.
|
||||
"""
|
||||
|
||||
|
||||
apps = {}
|
||||
"""
|
||||
A dict of the form {script name: application}, where "script name"
|
||||
is a string declaring the URI mount point (no trailing slash), and
|
||||
"application" is an instance of cherrypy.Application (or an arbitrary
|
||||
WSGI callable if you happen to be using a WSGI server)."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.apps = {}
|
||||
|
||||
|
||||
def mount(self, root, script_name="", config=None):
|
||||
"""Mount a new app from a root object, script_name, and config.
|
||||
|
||||
|
||||
root
|
||||
An instance of a "controller class" (a collection of page
|
||||
handler methods) which represents the root of the application.
|
||||
This may also be an Application instance, or None if using
|
||||
a dispatcher other than the default.
|
||||
|
||||
|
||||
script_name
|
||||
A string containing the "mount point" of the application.
|
||||
This should start with a slash, and be the path portion of the
|
||||
URL at which to mount the given root. For example, if root.index()
|
||||
will handle requests to "http://www.example.com:8080/dept/app1/",
|
||||
then the script_name argument would be "/dept/app1".
|
||||
|
||||
|
||||
It MUST NOT end in a slash. If the script_name refers to the
|
||||
root of the URI, it MUST be an empty string (not "/").
|
||||
|
||||
|
||||
config
|
||||
A file or dict containing application config.
|
||||
"""
|
||||
|
@ -192,42 +199,43 @@ class Tree(object):
|
|||
"order to inpect the WSGI environ for SCRIPT_NAME upon each "
|
||||
"request). You cannot mount such Applications on this Tree; "
|
||||
"you must pass them to a WSGI server interface directly.")
|
||||
|
||||
|
||||
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
|
||||
script_name = script_name.rstrip("/")
|
||||
|
||||
|
||||
if isinstance(root, Application):
|
||||
app = root
|
||||
if script_name != "" and script_name != app.script_name:
|
||||
raise ValueError("Cannot specify a different script name and "
|
||||
"pass an Application instance to cherrypy.mount")
|
||||
raise ValueError(
|
||||
"Cannot specify a different script name and pass an "
|
||||
"Application instance to cherrypy.mount")
|
||||
script_name = app.script_name
|
||||
else:
|
||||
app = Application(root, script_name)
|
||||
|
||||
|
||||
# If mounted at "", add favicon.ico
|
||||
if (script_name == "" and root is not None
|
||||
and not hasattr(root, "favicon_ico")):
|
||||
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
|
||||
"favicon.ico")
|
||||
root.favicon_ico = tools.staticfile.handler(favicon)
|
||||
|
||||
|
||||
if config:
|
||||
app.merge(config)
|
||||
|
||||
|
||||
self.apps[script_name] = app
|
||||
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def graft(self, wsgi_callable, script_name=""):
|
||||
"""Mount a wsgi callable at the given script_name."""
|
||||
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
|
||||
script_name = script_name.rstrip("/")
|
||||
self.apps[script_name] = wsgi_callable
|
||||
|
||||
|
||||
def script_name(self, path=None):
|
||||
"""The script_name of the app at the given path, or None.
|
||||
|
||||
|
||||
If path is None, cherrypy.request is used.
|
||||
"""
|
||||
if path is None:
|
||||
|
@ -237,17 +245,17 @@ class Tree(object):
|
|||
request.path_info)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
|
||||
while True:
|
||||
if path in self.apps:
|
||||
return path
|
||||
|
||||
|
||||
if path == "":
|
||||
return None
|
||||
|
||||
|
||||
# Move one node up the tree and try again.
|
||||
path = path[:path.rfind("/")]
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# If you're calling this, then you're probably setting SCRIPT_NAME
|
||||
# to '' (some WSGI servers always set SCRIPT_NAME to '').
|
||||
|
@ -261,19 +269,31 @@ class Tree(object):
|
|||
if sn is None:
|
||||
start_response('404 Not Found', [])
|
||||
return []
|
||||
|
||||
|
||||
app = self.apps[sn]
|
||||
|
||||
|
||||
# Correct the SCRIPT_NAME and PATH_INFO environ entries.
|
||||
environ = environ.copy()
|
||||
if environ.get(u'wsgi.version') == (u'u', 0):
|
||||
# Python 2/WSGI u.0: all strings MUST be of type unicode
|
||||
enc = environ[u'wsgi.url_encoding']
|
||||
environ[u'SCRIPT_NAME'] = sn.decode(enc)
|
||||
environ[u'PATH_INFO'] = path[len(sn.rstrip("/")):].decode(enc)
|
||||
if not py3k:
|
||||
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
|
||||
# Python 2/WSGI u.0: all strings MUST be of type unicode
|
||||
enc = environ[ntou('wsgi.url_encoding')]
|
||||
environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
|
||||
environ[ntou('PATH_INFO')] = path[
|
||||
len(sn.rstrip("/")):].decode(enc)
|
||||
else:
|
||||
# Python 2/WSGI 1.x: all strings MUST be of type str
|
||||
environ['SCRIPT_NAME'] = sn
|
||||
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
|
||||
else:
|
||||
# Python 2/WSGI 1.x: all strings MUST be of type str
|
||||
environ['SCRIPT_NAME'] = sn
|
||||
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
|
||||
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
|
||||
# Python 3/WSGI u.0: all strings MUST be full unicode
|
||||
environ['SCRIPT_NAME'] = sn
|
||||
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
|
||||
else:
|
||||
# Python 3/WSGI 1.x: all strings MUST be ISO-8859-1 str
|
||||
environ['SCRIPT_NAME'] = sn.encode(
|
||||
'utf-8').decode('ISO-8859-1')
|
||||
environ['PATH_INFO'] = path[
|
||||
len(sn.rstrip("/")):].encode('utf-8').decode('ISO-8859-1')
|
||||
return app(environ, start_response)
|
||||
|
||||
|
|