Compare commits

..

1862 Commits
0.7.2 ... 1.2.2

Author SHA1 Message Date
Safihre
555d8418e7 Update text files for 1.2.2 2017-02-25 22:12:34 +01:00
Safihre
8c22e35da4 Script paths were not clipped correctly 2017-02-25 22:07:25 +01:00
shypike
d32cf57c75 Update translations 2017-02-24 10:03:07 +01:00
Safihre
6d9242ebc5 Update text files for 1.2.1 Final 2017-02-23 11:53:56 +01:00
Safihre
cbc4f6a964 When retry of unpack due to Windows bug, send the actual password
We converted the password to "-p<password>" and when we had to retry it due to a Windows long-path fail, it would become "-p-p<password" and would incorrectly report that the password was wrong.
2017-02-21 20:09:07 +01:00
Safihre
2a3b2b9556 Forced diskspace check seperate from cached one
Closes #826. Otherwise the complete/incomplete get out of sync.
2017-02-20 15:43:55 +01:00
Safihre
53a219f12b Tooltip for Download/Incomplete in Status Windows
Closes #827
2017-02-20 09:03:45 +01:00
Safihre
48519dcfa0 Correct mistake in Diskspace calculation for Unix 2017-02-19 16:12:15 +01:00
Safihre
92542c58fe Diskspace for Complete folder was not calculated 2017-02-19 16:12:07 +01:00
Safihre
7eafe730f9 Modify RarFile to properly handle testrar on Windows
Because unrar doesn't support \\?\ notation for the path to the rarfile we used to clip the path. However, the Python functions that RarFile uses then fail on unicode or jobs with '?' in the filename. Now this is handled correctly, at the very last moment before testing the RAR.
2017-02-19 01:47:29 +01:00
Safihre
af92797ba0 Fix small language bug 2017-02-17 22:58:59 +01:00
Safihre
6625365586 Update text files for 1.2.1RC1 2017-02-17 22:43:18 +01:00
shypike
3e88e33397 Update translatable texts 2017-02-17 22:26:36 +01:00
shypike
f155a9d4a4 Update translations 2017-02-17 22:24:33 +01:00
thezoggy
6ae118dcb5 Update self-signed cert code to use SubjectAltName (#822)
* PEP8 fix and spelling fixes

* Update certgen code off latest crypto tutorial including using SubjectAltName instead of legacy Common Name.
Populate SAN with localhost and local v4+v6 ips to make other apps not throw `RemoteCertificateNameMismatch`.
Self signed certs are not allowed to be valid CA, so the user must manually add cert as trusted CA to resolve `RemoteCertificateChainErrors` -- using SAN this allows it to be stored as `SABnzbd` now.

* Add `127.0.0.1` and drop local IPv6
2017-02-17 13:39:33 +01:00
Safihre
b8c4f1a09a Try os.rename 3 times, before using shutil.move() 2017-02-15 13:52:36 +01:00
Safihre
f0602aa6e4 RSS size field could be empty for old feeds 2017-02-15 13:05:19 +01:00
Safihre
61ac750dd7 Par2cmdline doesn't handle \\?\ or \\.\ notation, workaround for special
Linked to #771. par2cmdline doesn't actually support the \\.\ or \\?\ notation. It allows to specify the par2-file in this way, but it ignores any of the other files in the folder: "Ignoring non-existent source file: \\."
So now to prevent it crashing the system on special-Windows-names, we still use the \\.\ notation in case of that. But it will still crash if it's an obfuscated post that gets renamed by par2cmdline to a forbiden name. We can't detect that..
2017-02-15 08:17:58 +01:00
Safihre
23b660df6e CherryPy 8.1.2 - Catch general SSL-errors
Closes #820
2017-02-14 18:40:13 +01:00
Safihre
a5d1c6860c Cache result of disk free/total
#817
2017-02-14 09:32:09 +01:00
Safihre
50fe3f8db0 Correctly handle "jobname / password" notation
Now it was cut wrongly, leaving the / in the name
2017-02-13 09:27:17 +01:00
Safihre
875f878b42 CherryPy 8.1.2 - Another SSL error to catch 2017-02-13 09:03:18 +01:00
Safihre
76e6f6633f RarFile.testrar() needs clipped-paths
But now it won't handle special win-names, so we skip those.
2017-02-12 22:28:13 +01:00
Safihre
caf5adc42d Pass shortend path to scripts on Windows (for now)
Closes #815
2017-02-10 12:11:35 +01:00
Safihre
b70609c047 Don't quit when only 1 of multiple RSS-feeds fails
Closes #816
2017-02-10 08:58:17 +01:00
Safihre
80a2b1685a Add tooltip to CPU name
Closes #814
2017-02-09 08:49:17 +01:00
Safihre
de5c0be2c7 Update text files for 1.2.1Beta1 - Typo 2017-02-08 08:51:45 +01:00
Safihre
a12623dda8 Update text files for 1.2.1Beta1 2017-02-08 08:38:19 +01:00
Safihre
1b3146d69f Restore unrar to _UNPACK_JobName
It works again!
2017-02-07 22:56:52 +01:00
Safihre
b39c89d069 Unrar on Windows with normal-path notation in case of Unicode errors 2017-02-07 18:58:01 +01:00
Safihre
ed31e0952e Use temporary folder for UNPACK
To avoid any unicode problems with command-line tools.
2017-02-07 18:58:01 +01:00
Safihre
28a58433de Don't RAR-verify encrypted jobs 2017-02-07 18:58:01 +01:00
shypike
670b79269d Windows: use long-paths for external tasks
Use long paths as much as possible when running external software.

Both par2cmdline for Windows versions support \\.\ notation.
Unrar handles long paths without the prefix.
Both long-path notations allow long paths and also forbidden words like
"aux." and "con.".
Previously, threads would hang if they tried to open "con.par2".
2017-02-07 18:58:01 +01:00
Safihre
ad8abbc213 Avoid startup crash on possible incorrect localhost-probing 2017-02-07 17:17:45 +01:00
Safihre
8e2ebc84c8 Make message in Server-tests more clear
Remove the -1@news.someserver.com:563 part at the end and make the certificate-errors link clickable.
2017-02-06 16:50:54 +01:00
Safihre
c9fddf2907 CherryPy 8.1.2 - Catch another Safari SSL error 2017-02-06 11:39:34 +01:00
Safihre
207deebfd5 Don't re-evaluate whole RSS feed on manual download 2017-02-03 15:35:13 +01:00
Safihre
641371cd74 Fix TopOnly and don't double check if there are articles for a server
TopOnly setting was ignored after every restart
2017-02-03 15:26:59 +01:00
Safihre
1bf76279c7 Remove TryList at NZBQueue level 2017-02-03 15:26:59 +01:00
Safihre
907a252f74 Always start from first rar for encryption check 2017-02-02 13:19:33 +01:00
Safihre
4ef5c181db Add link to page with text about Certificate errors 2017-02-02 09:02:21 +01:00
Safihre
ae584703e4 Correct spelling error 2017-02-01 08:35:11 +01:00
Safihre
b38250a37e Update README 2017-01-31 16:51:46 +01:00
Safihre
60e00196be RarFile's setpassword() can fail with correct password 2017-01-31 10:14:04 +01:00
Safihre
84d3969ec3 Log Cryptography version, we need >=1.0
Closes #803
2017-01-31 09:28:38 +01:00
Safihre
8ebdc0bd8c Let dupes pass to Queue when Dupe-detection set to Fail
The queue will then send it as failed to history. Otherwise they get paused, instead of failed.
2017-01-30 16:23:21 +01:00
Safihre
c79d99d791 Also log Unrar output in case of failure 2017-01-30 10:09:12 +01:00
Safihre
a31da69771 Use userxbit also to list scripts 2017-01-29 17:58:00 +01:00
Sander Jonkers
50589edcd2 new userxbit() as workaround for certain mounted filesystems 2017-01-29 17:52:54 +01:00
Sander Jonkers
b227a0f388 Changed message in case of problem 2017-01-27 22:28:04 +01:00
Sander Jonkers
277d2e86a9 CPU reporting now also on Windows and MacOS (fka OSX) 2017-01-27 22:28:04 +01:00
Safihre
467d4321f4 Simple HTML-tag removal of script_line
Closes #793
2017-01-26 11:59:42 +01:00
Safihre
24bb90d279 Add QuickCheck-renamer to README 2017-01-24 14:53:39 +01:00
Safihre
4bfc78b984 Use QuickCheck to do fast renaming of obfuscated files 2017-01-24 14:20:00 +01:00
Safihre
ee1005c363 Add 1 more bugfix to README 2017-01-23 20:31:20 +01:00
Safihre
3e4237275c Add bugfixes for 1.2.1 to README 2017-01-23 12:39:43 +01:00
Safihre
fa4a041773 Also show progress with par2 scan of obfuscated files 2017-01-23 12:39:43 +01:00
Safihre
040c42705d Detect version of par2cmdline that needs -B parameter
See also: https://github.com/Parchive/par2cmdline/issues/77
2017-01-23 12:39:43 +01:00
Safihre
f3f5a0bfa2 Only show Source icon for RSS items with a Source
Old ones don't have one
2017-01-23 12:39:43 +01:00
Safihre
875462b619 Auto-downloaded RSS jobs broke RSS display
Closes #787
2017-01-23 12:39:43 +01:00
Safihre
7253cb111b PP-script was not called on Accept&Fail or Dupe detection
Closes #781
2017-01-23 12:39:43 +01:00
Safihre
d318844304 Small cleanup of BPSMeter 2017-01-23 12:39:43 +01:00
Safihre
a59ea3175b Don't add extra space to password when supplied as "name / password" 2017-01-21 10:22:38 +01:00
Safihre
15456f2c2d CherryPy 8.1.2 - Catch bad SSL client connections
Closes #783. Happens when client forces low-end encryption that's not supported. 
Fix will probably be implemented in CherryPy at some point https://github.com/cherrypy/cherrypy/issues/1552
2017-01-20 17:05:05 +01:00
Safihre
cc824a96e0 Check if file exists before moving
Fight possible delays of the OS in actually deleting the file:
https://github.com/sabnzbd/sabnzbd/issues/782#issuecomment-274002201
2017-01-20 16:46:07 +01:00
Safihre
18257e0835 Auto-detect Article cache limit on Unix systems 2017-01-20 12:06:14 +01:00
Safihre
f669a0ed60 Allow 15 seconds during server-test
Some servers (Altopia) use 10 second timeouts with wrong credentials to prevent flooding.
2017-01-18 18:16:06 +01:00
Safihre
e148a057f7 Hide HTTPS options behind 'Advanced' button
Simplicity FTW!
2017-01-18 18:16:06 +01:00
Safihre
799eae65d2 Do not assume a HTTPS port to be set when HTTPS enabled 2017-01-18 18:16:06 +01:00
Safihre
f1dc4108f8 Category-matching failed if list of indexer-tags was given
Oops
2017-01-18 18:16:06 +01:00
Safihre
08b0b9eb92 Remove unpack-check 2017-01-18 18:16:06 +01:00
Safihre
85ba545107 is_rarfile doesn't work with short paths 2017-01-18 18:16:06 +01:00
Safihre
2604e467e8 Only check unwanted extensions when action set 2017-01-18 18:16:06 +01:00
Safihre
bd2c54a38d Prepare text files for 1.2.1 and remove unused stuff 2017-01-18 18:16:06 +01:00
Safihre
5c83c939d3 Show more clear warning about External access
Closes #775
2017-01-18 18:16:06 +01:00
Safihre
f7a7589107 Only check files after unpacking
Closes #779
2017-01-18 18:16:06 +01:00
Safihre
2418c56212 Build safe-guard in newznab attr processing
Closes #777
2017-01-18 18:16:06 +01:00
Safihre
6e89584263 Leave HTTPS port empty so enabeling HTTPS does this on default port
Why open 2 ports?
2017-01-18 18:16:06 +01:00
Safihre
0b696409fc Shorter port-checks on startup 2017-01-18 18:16:06 +01:00
Safihre
6602d13c95 In case HTTPS = HTTP port, save the correct port when occupied 2017-01-18 18:16:06 +01:00
Safihre
f19e637780 Rename Generic Sorting to Movie Sorting
Forgot for 1.2.0
2017-01-18 18:16:06 +01:00
Safihre
7da33b1f93 Show source of a RSS download
Can also sort on it
2017-01-18 18:16:06 +01:00
Safihre
453b5e565c Use a dict instead of list for RSS template output
The list-based output for the template left it unclear what each item was. With a dict, it's much clearer.
2017-01-18 18:16:06 +01:00
Safihre
1827a2487b Sort RSS while building the lists
Instead of in the javascript later on
2017-01-18 18:16:06 +01:00
Safihre
edc01c3e2b Some cache-busting on version upgrade for JS/CSS 2017-01-18 18:16:06 +01:00
Safihre
720215ea05 Cloaked files were not detected 2017-01-18 18:16:06 +01:00
shypike
d5d40857f4 Revert "Use \\?\ long-path notation consistent and use \\.\ notation for commandline."
This reverts commit 94c2d50f70.
2017-01-17 22:30:43 +01:00
shypike
94c2d50f70 Use \\?\ long-path notation consistent and use \\.\ notation for commandline.
Both par2cmdline-multicore and unrar support \\.\ notation.
Both long-path notations allow long paths and also forbidden words like
"aux." and "con.".
Previously, threads would hang if they tried to open "con.par2".
2017-01-17 22:20:57 +01:00
Safihre
1be50ebe59 Fix the workings of RSS matching 2017-01-13 15:11:47 +01:00
Safihre
80404b3148 Set hard limit on Decoder-queue 2017-01-13 10:28:32 +01:00
shypike
0f3c4d7363 Update translations 2017-01-13 06:58:57 +01:00
Safihre
939a5ab7c0 Update text files for 1.2.0 Final 2017-01-12 12:48:53 +01:00
Safihre
722161b6cd Handle Server prio-coloring in template instead of Javascript 2017-01-12 12:48:53 +01:00
Safihre
7c8dc6e0d9 Small improvement of icon-loading in RSS window 2017-01-12 12:48:53 +01:00
Safihre
efdd06b3e9 Add a link with DMCA information to failure messages
So we avoid people asking us on the forum
2017-01-12 12:48:53 +01:00
Safihre
ee689c231c Not having _yenc is bad
The difference in CPU load is just too big (3x fold)
2017-01-12 12:48:53 +01:00
Safihre
9e3db377fb Combine 'Skip' column to get more space for Title in RSS 2017-01-12 12:48:53 +01:00
Safihre
6cb4c93e89 Also support nntmux tags in RSS parser
On top of nZEDb and newznab
2017-01-12 12:48:53 +01:00
Safihre
15dcb274c3 Remove old jQuery
Closes #766
2017-01-12 12:48:53 +01:00
Safihre
d721476986 Update text files for 1.2.0RC1 2017-01-12 12:48:53 +01:00
Safihre
850a762905 Don't error on drag-and-drop Folders
Closes #765
2017-01-12 12:48:53 +01:00
shypike
7c1e4f8e41 Update main POT file 2017-01-08 12:44:51 +01:00
shypike
37182f07fb Update translations 2017-01-08 12:40:42 +01:00
Safihre
30519d92fc Huge recv() size can cause MemoryError
Seems there is some limit here.
2017-01-06 14:36:12 +01:00
Safihre
a1cbd76985 Hide Multicore par2 switch when not available
Only works on Win/OSX.
2017-01-06 14:36:12 +01:00
Safihre
6df8d7a30a Pause-countdown was broken in Glitter 2017-01-06 14:36:12 +01:00
Safihre
09568274b3 Update jQuery to 3.1.1 for Config
Not yet in Glitter, there it causes some side-effects
2017-01-06 14:36:12 +01:00
Safihre
fa6e02ee8e Try to avoid javascript error with many RSS Filters 2017-01-06 14:36:12 +01:00
Safihre
56346a3a64 Update Changelog 2017-01-06 14:36:12 +01:00
Safihre
fb1d9842d4 Fix link to set Max line speed in Glitter 2017-01-06 14:36:12 +01:00
Safihre
4642ff0cdb Update Translatable texts 2017-01-06 14:36:12 +01:00
Safihre
508ca1dbc0 Correct URL's for Config Search pre-loader
Otherwise it does a redirect
2017-01-06 14:36:12 +01:00
Safihre
997ed57188 Tweak to detect size in more cases for RSS 2017-01-06 14:36:12 +01:00
Safihre
8630c712bb Only enable CherryPy logging when enabled from command-line 2017-01-06 14:36:12 +01:00
Safihre
8b7134a650 Config Search improvements 2017-01-06 14:36:12 +01:00
Safihre
1850f47c13 Remove lower() from Notifications Config page
Closes #732
2017-01-06 14:36:12 +01:00
Safihre
2fb44827b0 Add Config Search function 2016-12-27 22:09:53 +01:00
Safihre
f0683e101b Combine RSS feeds to share filters
Closes #680
2016-12-27 22:09:53 +01:00
Safihre
e2fda89ab3 Account for menu-offset when jumping to Config items directly 2016-12-27 22:09:53 +01:00
Safihre
a8b3f721c9 Fix cherrypy_logging
Closes #758
2016-12-27 22:09:53 +01:00
Safihre
5948d6cf33 Use season/episode information from Newznab and store results
Getting it from the feed is much faster (50x)
2016-12-24 22:17:27 +01:00
Safihre
17b71bc7fb Use Newznab 'usenetdate' to determine age
RSS publish date is usually behind
2016-12-24 22:17:27 +01:00
Safihre
dd5fe708ae Don't re-download RSS feed when clicking Apply Filters 2016-12-24 22:17:27 +01:00
Safihre
d705ccbe80 Add support for Newznab/nZEDd attributes to feedparser 2016-12-24 22:17:27 +01:00
Safihre
b53f46e0a0 Adding URL using PP=Default would set PP to only Download
Because pp="" is not the same as pp=None, so the right PP-setting from the Default setting was never applied
2016-12-24 22:17:27 +01:00
Safihre
b497d1ab9a Need new translations for number of functional texts
It also needs to make sense in other languages, not just English
2016-12-24 22:17:27 +01:00
Safihre
2f86292503 Don't check port twice if HTTPS is served on HTTP port 2016-12-24 22:17:27 +01:00
Safihre
e804b157c8 Update text files for 1.2.0Beta1 2016-12-24 22:17:27 +01:00
Safihre
0ac801d00d Catch IncompleteRead in URLGRabber
Closes #754
2016-12-24 22:17:27 +01:00
Safihre
df6b9341ac Move UpdateCheck/AutoBrowser/CertificateCheck to General
That's where they belong
2016-12-24 22:17:27 +01:00
Safihre
749d18308b Catch more ways to correctly redirect after Config update 2016-12-24 22:17:27 +01:00
Safihre
d16166c27a Make 'Security' section in Config General 2016-12-24 22:17:27 +01:00
Safihre
edbd1e7e8f Add default Categories for new users
Same as newznab categories, so it's more clear for user's what is possible
2016-12-24 22:17:27 +01:00
Safihre
8855ce9414 Hide non-regular Server settings behind 'Advanced' button 2016-12-24 22:17:27 +01:00
Safihre
61b2a57925 Move 'Disable API-key' to Specials
Closes #755
2016-12-24 22:17:27 +01:00
shypike
f8d6286d2a Manual update of NL translation 2016-12-24 18:59:36 +01:00
shypike
389e8b6a48 Update translations 2016-12-24 18:32:34 +01:00
Safihre
ae39e754b0 Fixes for new OSX building 2016-12-19 21:48:44 +01:00
Will
3b398a7643 Fixes Tiny 1 Letter Typo (#752)
Fixes the tiniest of a Typo

Missed the s, whoops. ;)
2016-12-19 21:45:58 +01:00
Safihre
9f8784e66a Proper texts for Optional and SSL in Servers Config page 2016-12-16 10:30:15 +01:00
Safihre
7b2d56a4c7 CherryPy 8.1.2 - Catch another SSL error for Safari on OSX
User needs to add execption first
2016-12-16 10:30:15 +01:00
Safihre
f093dbef7a MacOS running from sources can use regular restart-path 2016-12-16 10:30:15 +01:00
Safihre
950f62de85 Fix Windows long-path problems after recursive unpack 2016-12-16 10:30:15 +01:00
Safihre
de159153f5 Include link to SSL Ciphers wiki-page 2016-12-16 10:30:15 +01:00
Safihre
814960c5f0 Also check if running SABnzbd version might be same as the new one
Closes #747
2016-12-16 10:30:15 +01:00
Safihre
ab66abb348 Accidentally removed 'Restarting SABnzbd' text
And small Glitter-Night fixes
2016-12-16 10:30:15 +01:00
Safihre
a5d139a820 Don't send Windows notification when icon already shutdown 2016-12-16 10:30:15 +01:00
Safihre
2c4c34afcf Update README regarding PR #745 2016-12-16 10:30:15 +01:00
Safihre
cbe4840ce2 Update Copyright year to 2017 2016-12-16 10:30:15 +01:00
Safihre
cdf378ff45 Remove unused CherryPy settings 2016-12-16 10:30:15 +01:00
Safihre
7919961b8a Remove NewRotatingFileHandler and move version_check to after startup 2016-12-16 10:30:15 +01:00
Safihre
8d3ddb6ac5 Add option to mark duplicates as failed
So Sonarr/Sickbeard etc can pick another release
Closes #591
2016-12-08 16:11:15 +01:00
Safihre
cf6f850586 Fix accept&fail action of pre-queue scripts
Closes #744
2016-12-08 16:11:15 +01:00
Safihre
4dbf00810f OrderedDict is part of Python 2.7 2016-12-08 14:41:32 +01:00
Safihre
96fc743f6a Log SSL-Context check and show when Cryptograhypy is missing 2016-12-08 14:41:32 +01:00
Safihre
06ac19915c Update wizard
No need to restart. 
Show the download-folders so users know where their files are going.
2016-12-08 14:41:32 +01:00
Safihre
c3b3ba4a9e Direct restarts also in API 2016-12-08 14:41:32 +01:00
Safihre
727aac9811 'Abort when cannot be completed' broke when files are removed from job
Gone is now really gone.
Closes #742
2016-12-08 14:41:32 +01:00
Safihre
6699c58d73 Faster re-connect-check after Config restart
Since we restart faster, we can check faster!
2016-12-08 14:41:32 +01:00
Safihre
d7a0147191 Implement direct restarts (when possible) 2016-12-08 14:41:32 +01:00
Safihre
a7697d4479 Remove web_watchdog
We shouldn't need this
2016-12-08 14:41:32 +01:00
Safihre
d603ad76b8 Stopping of DirScanner and PostProc doesn't need saving of state
save_state() already does this
2016-12-08 14:41:32 +01:00
Safihre
231dcea37e Move IPv6 and SSL checks to start of Downloader-thread
Reducing startup time by another 2 seconds
2016-12-08 14:41:32 +01:00
Safihre
d579c4d167 Shutdown CherryPy directly but gracefully without waiting 5 seconds
CP change to fix bug submitted as PR: https://github.com/cherrypy/cherrypy/pull/1528
2016-12-08 14:41:32 +01:00
Safihre
88e739606b Don't check port on startup twice
We just did it a few lines before!
2016-12-08 14:41:32 +01:00
Safihre
a922da5868 Significantly reduce startuptime by lowering CP check_port timeout 2016-12-08 14:41:32 +01:00
Safihre
5c329803aa Measure effects of slowdown over 10 seconds instead of 5 2016-12-08 14:41:32 +01:00
Safihre
0cb81d0642 Error when no x-bit and only add "python" when no shebang for .py
Closes #741
2016-12-08 14:41:32 +01:00
Safihre
b1b13f9b8b Update CherryPy License file 2016-12-08 14:41:32 +01:00
Safihre
cbfff5feea Update text files (readme/licenses)
Add RSS filter re-evaluation
Remove SSMTPlib license and the msgfmt (also mentioned in PythonParts license)
2016-12-08 14:41:32 +01:00
Safihre
dbe8e64b9e ssmtplib is part of Python 2.7 2016-12-08 14:41:32 +01:00
Safihre
dad46c06fb Update readme to reflect branch-usage better 2016-12-08 14:41:32 +01:00
Safihre
add7cafcb5 Notify if the port was changed
Closes #739
2016-12-08 14:41:32 +01:00
Safihre
e3d229104a CherryPy 8.1.2 - The .exe shouldn't restart with interpreter args 2016-11-30 10:34:38 +01:00
Safihre
ccf2ffb2d5 Update changelog for RSS changes 2016-11-30 10:34:38 +01:00
Safihre
d239b07900 Correct handeling of no Cryptography
When checking passwords and in the Config
2016-11-30 10:34:38 +01:00
Safihre
fe062f0b93 RarFile also requires patching of Crypto when packaging 2016-11-30 10:34:38 +01:00
Safihre
ab4103561f Disable RSS options for rules that don't accept 2016-11-30 10:34:38 +01:00
Safihre
8e447397b2 Add support for username:password in URLs
Closes #737
2016-11-30 10:34:38 +01:00
Safihre
7a746b6779 Show Category detected by SAB in RSS tables 2016-11-30 10:34:38 +01:00
Safihre
8e29b2a481 Add Sorting in RSS tables 2016-11-30 10:34:38 +01:00
Safihre
5434df4868 Show NZB post-age and time of Download 2016-11-30 10:34:38 +01:00
Safihre
780c7b6400 RSS page handles Add NZB on the same page 2016-11-30 10:34:38 +01:00
Safihre
ccca7f05c6 Show size and download-date in RSS Downloaded tab 2016-11-30 10:34:38 +01:00
shypike
489ca46fdf Add "From Show SxxEyy" accept filter.
Small optimization of the season/episode analysis by pre-compiling the regexes.
Show "Apply filters" button only when filters have changed or initial display.
2016-11-30 10:34:38 +01:00
shypike
01e8e64505 Don't re-evaluate RSS filters each time they change, use "Eval" button instead. 2016-11-30 10:34:38 +01:00
shypike
25998f1b0e Change RSS-filter "From-SxxEyy" from a Require to a Match filter
It now supports "Show.name.Sxx.Eyy" for matching show/season/episode in one filter.
This allows multiple independent show filters in one feed.
2016-11-30 10:34:38 +01:00
Safihre
97686dc14f Update files for 1.2.0 beta's 2016-11-25 15:06:31 +01:00
Safihre
6fbe450dcc Require x-bit to be set for scripts on non-Windows 2016-11-25 15:06:31 +01:00
Safihre
5f63b0c935 Test before applying downloader-delay 2016-11-25 15:06:31 +01:00
Safihre
04fa3c3115 MemoryError's should stop decoding 2016-11-25 15:06:31 +01:00
Safihre
36e2f28d51 Freeze-support for certgen
py2exe fails to build otherwise
2016-11-25 15:06:31 +01:00
Safihre
5c77f9d9b3 Rewrite speed-history to be more flexible 2016-11-25 15:06:31 +01:00
Safihre
a99a51e0d4 Re-use the same IP if server still has active threads
To avoid problems when re-connecting after for example a timeout. 

Closes #733
2016-11-25 15:06:31 +01:00
Safihre
2d091568f3 no_penalties now applies to all penalties
Closes #734
2016-11-25 15:06:31 +01:00
Safihre
a449b4b001 work_dir should not end on .par(2)
Otherwise par2cmdline gets angry.
2016-11-25 15:06:31 +01:00
Safihre
aa71dc1103 Improve texts for External access settings
Closes #729
2016-11-25 15:06:31 +01:00
shypike
0b0ff448d5 Update translations 2016-11-10 22:36:00 +01:00
shypike
b08e01f1c3 8th parameter for user-script wasn't passed correctly. 2016-11-03 10:51:26 +01:00
Safihre
fe0439382e Change tabs to 4 spaces in Config/Glitter 2016-11-03 09:28:36 +01:00
Safihre
bf89714926 Clean-up all par2 of a set
Closes #724
2016-11-03 09:28:36 +01:00
Safihre
25a9f9823d par2cmdline on Windows also needs -N 2016-11-03 09:28:36 +01:00
Safihre
bfb23b1dd5 RarFile needs shortened and de-unicoded filepaths
Normally we do this in build_command, but RarFile doesn't.
2016-11-03 09:28:36 +01:00
Safihre
f814f4cd8f QuickCheck would fail unicode files 2016-11-03 09:28:36 +01:00
Safihre
83ada782b8 Only shorten pathname on Windows for par2
Otherwise unicode paths are misformed and not detected. This is also how we do it for unrar.
2016-11-03 09:28:36 +01:00
Safihre
09ae033196 Unicode failed downloads are seen as Orphans due to xml_encoding
Need to force use of unicode, otherwise it incorrectly says they are orphaned
2016-11-03 09:28:36 +01:00
Safihre
ad1a8c302c Improve HTTPS on Config->General and improve Duplicates text 2016-11-03 09:28:36 +01:00
Safihre
c2f861146b Reduce Notifications template even more 2016-11-03 09:28:36 +01:00
Safihre
48ac57281c Significantly simplify Notifications template
They all have the same options..
2016-11-03 09:28:36 +01:00
Safihre
88f557f829 Update Categories page and texts 2016-11-03 09:28:36 +01:00
Safihre
8e0dae4d51 Implement user-sorting of Categories 2016-11-03 09:28:36 +01:00
Safihre
bfdd8e840d Adapt Glitter so it's not overwriting Categories 2016-11-03 09:28:36 +01:00
shypike
e1039f52e5 Improve category matching
When no other matches, try partial match of category name and indexer category.
Add comments and improve code.
2016-11-03 09:28:36 +01:00
shypike
a3e7076073 Update translations 2016-11-02 23:17:47 +01:00
shypike
3e2bded3c3 Update main POT file. 2016-11-02 23:16:50 +01:00
Safihre
a2a851d17f Detect and use all obfuscated par2 files
Using a trick to get handle_par2() to pick them up correctly.
2016-10-27 19:45:14 +01:00
Safihre
580e25de6d Use NZF's to do obfuscated check 2016-10-27 19:45:14 +01:00
Jonathon Saine
658544c305 When no parset defined (possible obfuscated), check for par2 signature.. rename first match. Repair, then repeat logic until all parsets have been processed.. then continue with processing as before. 2016-10-27 19:45:14 +01:00
Jonathon Saine
a872a2d2ee spelling fixes 2016-10-27 19:45:14 +01:00
Safihre
b1c5a28241 cryptography also nessecary to let RarFile open encrypted files 2016-10-27 19:45:21 +02:00
Safihre
df8046287e Check all passwords on first rar for definitive check
Closes #185
2016-10-27 19:45:21 +02:00
Safihre
627f998d25 Implement verification using unrar, when par2 and sfv not available
Closes #621
2016-10-27 19:45:21 +02:00
Safihre
52a30f5804 Log the executed command for Unzip and 7Zip 2016-10-27 19:45:21 +02:00
Safihre
b910574cdb Combine Encrypted/Unwanted files check 2016-10-27 19:45:21 +02:00
Safihre
de6d642b0d Adapt new rarfile for usage within SABnzbd
Closes #594
2016-10-27 19:45:21 +02:00
Safihre
7eb9dddf22 Update RarFile 2016-10-27 19:45:21 +02:00
Safihre
47b5f25dcf Merge pull request #711 from sabnzbd/bugfixes
Bugfixes and new features
2016-10-21 15:21:27 +02:00
Safihre
f5bb7d3cbe Glitter didn't allow removal of a set job-password
Closes #720
2016-10-20 11:04:06 +02:00
Safihre
b4a1d72013 Servertest broke on SSLError
For example when user had selected a bad cipher
2016-10-20 09:10:08 +02:00
Safihre
2faf951592 Revert UnRar licence to original 2016-10-20 08:58:39 +02:00
Safihre
063b2bcc5f Correct RegEx for par2-file detection 2016-10-19 15:26:16 +02:00
Safihre
fc2f527b15 Move SSL Ciphers option to Switches page
People should know it's possible. We put the explenation on the Wiki!
2016-10-19 10:35:29 +02:00
Safihre
47fa33a2a2 Fix on how aborted Par2/Unrar is reported 2016-10-14 15:32:27 +02:00
Safihre
6731d7be59 Enable certificate validation by default
When user has incorrect setup, the connect-to-well-known-host check will disable it anyway.
Closes #465 and #468
2016-10-14 15:03:25 +02:00
Safihre
91ce734d32 Cleanup of whitespace in Glitter/Config/wizzard 2016-10-14 14:54:58 +02:00
Safihre
b89394c819 Allow aborting of Par2/Unrar (Glitter only)
Closes #703
2016-10-14 14:48:53 +02:00
Safihre
2732edce09 Allow also "vol01-03.par" on top of "vol01+03.par" 2016-10-14 14:37:41 +02:00
Safihre
68f1b9234b Make sure we show results when less than 1 page
Closes #710
2016-10-14 08:42:16 +02:00
Safihre
81497e19ce Improve text on Delete-page button 2016-10-14 08:42:16 +02:00
Safihre
a0d3f8aa86 Update 7zip for Windows to 16.04 2016-10-14 08:42:16 +02:00
Safihre
4074ae3271 Update 7zip for macOS to 16.02
Best available for macOS
2016-10-14 08:42:16 +02:00
Safihre
7b197d93bc Update UnRar to 5.40 for Windows and Mac 2016-10-14 08:42:11 +02:00
Safihre
6b5cca4bb9 Avoid duplicates in build_filelists() that break 7zip support
Closes #706
2016-10-12 11:06:51 +02:00
Safihre
62971c0fd0 Remove Solaris Manifest
We don't really support it, nor does it seem in high demand
2016-10-12 10:18:47 +02:00
Safihre
8007d5a5d6 Remove Dockerfile (beter available from https://hub.docker.com) 2016-10-11 22:25:33 +02:00
Safihre
eb9421255d Check for Retry-After header when fetching fails
Closes #707
2016-10-11 09:25:17 +02:00
Safihre
8e97097dfd Only allow binding to IPv6 when ipv6_hosting enabled
For some reason it was always binding to IPv6, even when disabled (on Windows)
2016-10-10 15:59:38 +02:00
Safihre
b6972db5a7 CherryPy update long ago broke correct HTTPS port binding
The attach_server() could only bind HTTP ports because some change in CherryPy. Resulting of serving of HTTP on HTTPS port when set to localhost.
2016-10-10 15:31:07 +02:00
Safihre
755d904136 SSL handshake is performed automatically on connect() 2016-10-10 13:07:04 +02:00
Safihre
1052208d9e Verify the quality of certificate validation 2016-10-10 11:58:04 +02:00
Safihre
e259f624ad Detect when par2cmdline needs -N
Closes #705
2016-10-10 09:01:04 +02:00
Safihre
a77483ee31 Fix retry_all API-call 2016-10-09 17:55:58 +02:00
Safihre
201181ee6f Tiny change to Sorting debug logging
Closes #671
2016-10-07 14:42:41 +02:00
Safihre
2968b3c30e Change par2-classic to par2cmdline on Windows
Closes #702
2016-10-07 14:42:41 +02:00
Safihre
49a9fc7682 Avoid javascript error in Servers page 2016-10-07 14:42:41 +02:00
Safihre
0ff0c63903 Updates to INSTALL/ISSUES/README
Closes #676
2016-10-07 14:42:41 +02:00
shypike
0af084b4eb Fix emailer by converting account data to UTF-8 (#700)
It seems that the used libraries cannot handle Unicode, but only UTF-8.
2016-10-06 15:14:43 +02:00
Safihre
d75278b52b CSS fix for Config menu on mobile 2016-10-06 15:14:43 +02:00
Safihre
cf94729830 "Max line speed" more clear, using a select 2016-10-06 15:14:43 +02:00
Safihre
15d3ad5a96 Don't error out on Retry database errors
In case the job is somehow already gone
2016-10-06 15:14:43 +02:00
Safihre
bb231c7416 Fix ciphers input for older Python 2016-10-06 15:14:43 +02:00
shypike
a75b93699c Update .gitignore 2016-10-04 13:38:45 +02:00
Safihre
bf3e08868b Allow setting SSL cipher-string in Specials
For users who know what they are doing
2016-10-03 13:35:13 +02:00
Safihre
db9db1981c Log when creating new certificates
Closes #695
2016-10-03 13:35:13 +02:00
Safihre
4dff051927 Show SSL protocol and cipher in status window 2016-10-03 13:35:13 +02:00
Safihre
bb92059343 CherryPy 8.1.2 - LF in MIME records and don't crash on IPv6 in error msg
Even though it's against the protocol.
2016-10-03 13:35:13 +02:00
Safihre
1e72304c25 CherryPy 8.1.2 - Fix last SSL-bug 2016-10-03 13:35:13 +02:00
Safihre
692ed8fce8 CherryPy 8.1.2 - Update and set version 2016-10-03 13:35:13 +02:00
Safihre
6a12224b21 Don't choke when cryptography-module is not installed 2016-10-03 13:35:13 +02:00
Safihre
e8c7155c69 Update text-files and fix small CSS mistake 2016-10-03 13:35:13 +02:00
Safihre
a2e7e917d1 Custom error message when server doesn't speak SSL on specified port 2016-10-03 13:35:13 +02:00
Safihre
c73baaa3d2 Make "Certificate validation" option for each server 2016-10-03 13:35:13 +02:00
Safihre
09924a0f9c Fallback to non-verified connection when SSLContext is not available
For example on Python <2.7.9
2016-10-03 13:35:13 +02:00
Safihre
2a9a28cdb4 Improve error when bad certificate 2016-10-03 13:35:13 +02:00
Safihre
db6256c720 Use the correct hostname in case of happyeyeballs 2016-10-03 13:35:13 +02:00
Safihre
440a80c552 Generate certificates using Cryptography
Bye bye pyOpenSSL!
2016-10-03 13:35:13 +02:00
Safihre
a0e5eae244 Add option to set verification level 2016-10-03 13:35:13 +02:00
Safihre
6346ad96e4 First catch SSL errors, then general socket errors 2016-10-03 13:35:13 +02:00
Safihre
886803112d Remove option to choose SSL-level 2016-10-03 13:35:13 +02:00
Safihre
f277b8c534 Log SSL/TLS level and cipher after connecting 2016-10-03 13:35:13 +02:00
Safihre
a8d8db3a10 Remove pyOpenSSL from Config 2016-10-03 13:35:13 +02:00
Safihre
21549ab842 Switch CherryPy to use Python ssl instead of PyOpenSSL 2016-10-03 13:35:13 +02:00
Safihre
e8e2adcee0 Implement NNTPS using python ssl module
Removes need for PyOpenSSL for NNTPS connections, does require OpenSSL to be installed. This is the case on most modern systems (Linux/Windows/OSX), according to the Python docs!
2016-10-03 13:35:13 +02:00
shypike
3d4cdd7230 Merge pull request #693 from Safihre/develop
Safer limits for CPU limitation in Downloader
2016-10-01 16:56:33 +02:00
Safihre
5df3c24313 Fix order-selection box on Plush NZO-details page 2016-09-28 16:28:15 +02:00
Safihre
eb737619d9 RSS icon margin fix 2016-09-28 15:26:52 +02:00
Safihre
b120fb722d Safer limits for CPU limitation in Downloader 2016-09-28 15:02:10 +02:00
shypike
bd199627f9 Merge pull request #683 from Safihre/develop
Generate new HTTPS-certificates and more
2016-09-19 22:03:15 +02:00
Safihre
e27f0c5053 Show example for speedlimit in scheduler 2016-09-18 11:07:28 +02:00
Safihre
792d0ba319 Do not use Google to get RSS favicon 2016-09-17 20:17:12 +02:00
Safihre
e2a6517a89 #684 Update jQueryUi to 1.12.1 2016-09-16 18:52:27 +02:00
Safihre
1764fa29d1 Padding to None in tabbed warning section 2016-09-15 14:34:10 +02:00
Safihre
cdf0b0186a Add button re-generate self-signed certificates in Config 2016-09-15 14:34:10 +02:00
shypike
790bcd7bd6 Prepare text files for 1.2.0 2016-09-14 09:04:08 +02:00
shypike
a10a5a5f63 Fix crash in CherryPy when it reports problems with some IPv6 addresses.
A bug in Python's traceback logging causes a crash when an IPv6 address with an embedded % is reported.
2016-09-14 08:53:28 +02:00
shypike
53051a94ee Accept MIME records that have only LF line endings.
Some tool developers just ignore the rule requiring CRLF.
2016-09-14 08:53:14 +02:00
shypike
8abcf0856e Update translations 2016-09-10 10:37:36 +02:00
shypike
891798ec4d Update README.mkd 2016-09-10 10:34:37 +02:00
shypike
126321e431 Update text files for release 1.1.0 2016-09-09 23:25:31 +02:00
shypike
86348dee11 Update required CherryPy to 6.0.2 and correct INSTALL.txt 2016-09-09 23:09:06 +02:00
shypike
748ca0ea27 Re-use the NZO_ID after the pre-check run.
This lets 3rd party tools follow job progress through the API.
2016-09-06 20:53:33 +02:00
shypike
e63d5be38c Merge pull request #675 from Safihre/develop
Bugfixes after RC4
2016-09-05 21:55:17 +02:00
Safihre
b26a02371c Improve tabbed layout warnings/mobile 2016-09-05 21:37:06 +02:00
Safihre
f76a886622 More conservative decoding article-cache reserving 2016-09-05 17:36:09 +02:00
Safihre
2ee63f6c98 Testing server needs username/password obfuscation removed 2016-09-04 09:47:40 +02:00
Safihre
83164e221f Set can also not exist in partable 2016-09-03 20:43:41 +02:00
shypike
f9131f591f Force fail_hopeless_jobs to "enabled" by renaming it.
It's the best setting nowadays and most people don't change defaults.
2016-09-02 21:46:15 +02:00
shypike
e6b573e5ec Retries URL fetches should retain their Post-processing setting.
Closes #509
2016-09-02 19:42:19 +02:00
shypike
efa0d5590d Fix typos in README.mkd 2016-09-02 19:25:53 +02:00
shypike
e0189f0e6d Merge pull request #670 from Safihre/develop
Show Glitter tips only once
2016-08-30 10:03:40 +02:00
Safihre
4d34742f46 Show Glitter tips only once 2016-08-30 09:07:51 +02:00
shypike
34c24acb20 Update text files for 1.1.0RC4 2016-08-29 21:57:55 +02:00
shypike
7055b0a814 Merge pull request #668 from sanderjo/develop
Happy Eyeballs: Improved logging and comments
2016-08-26 21:06:08 +02:00
Sander
260c6b906c Happy Eyeballs: Improved logging and comments 2016-08-26 07:52:40 +02:00
shypike
adda8c8898 Merge pull request #660 from sanderjo/develop
Improved network stuff: unresponsive DNS and HappyEyeballs-with-memory
2016-08-25 18:14:58 +02:00
shypike
8e612d1945 Merge pull request #663 from Safihre/develop
Prospective par2 was broken
2016-08-25 18:13:51 +02:00
Safihre
1cf2c1a075 Don't count 'Checking' bytes in Queue-count 2016-08-23 20:05:49 +02:00
sanderjo
8db75be5a5 Happy Eyeballs: remember results to avoid redundant lookups 2016-08-23 18:42:14 +02:00
Safihre
732bf0cfa8 Par2 doesn't need to get removed again, done in add_parfile() 2016-08-23 18:17:39 +02:00
Safihre
6e803fa350 Multi-delete would refresh the queue for every deleted item
Wayyyyy too often
2016-08-23 18:17:39 +02:00
Safihre
a515194b1e Par2's were never really gone
This way postproc thinks that there were still par's left to try, putting the job back, causing a stall in the queue
2016-08-23 18:17:39 +02:00
Safihre
2485bf5c74 Don't start par2 with non-existing .par-file
If the first .par2 was corrupt, there can be a whole bunch more
2016-08-23 18:17:39 +02:00
Safihre
181969d4c6 True prevention of autofills 2016-08-23 18:17:39 +02:00
Safihre
4f972eeaf8 Prospective par2 was broken 2016-08-23 18:17:39 +02:00
Safihre
4ffc4d0879 Make bytes-downloaded correct after Retry 2016-08-23 15:55:53 +02:00
Safihre
604472afca Real bytes counter 2016-08-23 15:23:43 +02:00
shypike
96847baf08 Prevent crashes in dirscanner when encountering badly encoded file names.
Closes #644
2016-08-20 00:01:33 +02:00
sanderjo
332d3a9418 Solve delays in case of non-responding DNS 2016-08-19 23:55:28 +02:00
shypike
776a185fc5 Merge pull request #658 from Safihre/develop
Fixes and Rating upgrade
2016-08-19 14:25:26 +02:00
Safihre
0757706ad3 Stop and remove files in assembler when job removed 2016-08-19 13:58:24 +02:00
Safihre
12a622a21f Use NZO.is_gone() for status 2016-08-19 13:12:46 +02:00
Safihre
619f553de1 Don't send rating-apikey if not filled 2016-08-18 23:19:47 +02:00
Safihre
785bf3f2a3 Update Wiki-links to /1.1/ 2016-08-18 12:33:35 +02:00
Safihre
fbee96d62f SSL level will be set by default to v23 using ssl_method 2016-08-17 10:02:32 +02:00
Safihre
2253e7b09f Change Error to Warning for temporary server errors
Keep the Error for failed login, this is bad.
2016-08-17 10:01:56 +02:00
Safihre
eb3801a918 Extend Rating functionality
Fields can now also be submitted as X-RATING headers during the URL-grab. Instead of just supplying a host, it is also possible to send a X-RATING-URL for more flexability
More: https://github.com/nZEDb/nZEDb/issues/2235
2016-08-17 08:34:44 +02:00
Safihre
aaf3bbb631 Fix mistake in rating adding
Would never allow values longer than 1, like a score of 10. And all info is also in nzo_info.
2016-08-16 17:22:50 +02:00
Safihre
1a66372e9b #657 Add days when hours > 23 2016-08-16 11:06:55 +02:00
Safihre
f5a28a2e56 Proper Script-output escaping
urllib.quote() makes everything to go in an URL, that's not what we need. 
Now also there will not be shown a "(more)" button when there is just 1 output line
2016-08-16 09:25:22 +02:00
Safihre
1fd6a8b1ac Remove obsolete part of history 2016-08-16 08:46:47 +02:00
Safihre
e632f8065d Upgrade jQuery (2.2.2->2.2.4) and jQueryUI (1.11->1.12)
No real changes for us, we already didn't support IE lower than 10.
2016-08-15 10:30:28 +02:00
Safihre
2e0e94a164 #656 Move switches to Specials for Unrar/Unzip/7zip/FileJoin/TSJoin 2016-08-15 08:12:16 +02:00
Safihre
7078af7d23 SSL was not set to highest-possible by default 2016-08-13 12:21:48 +02:00
Safihre
0a8a22090c #649 Warnings in seperate tab when Tabbed Layout 2016-08-13 11:20:23 +02:00
shypike
4d193061cc Merge pull request #655 from Safihre/develop
Safhire's fixes.
2016-08-13 10:31:07 +02:00
shypike
14c6eeef69 Prevent issues when decoding unknown 8-bit ASCII names. 2016-08-13 10:13:10 +02:00
Safihre
2e7f2d2272 Downloader optimzation only when #con >= 8 2016-08-12 21:49:57 +02:00
Safihre
803a695c00 Lower CPU usage when maxing out connection 2016-08-12 21:39:52 +02:00
Safihre
cced1b317c Windows tray icon wasn't terminated properly on restart
So if you restart SABnzbd, the icon would stay untill you hover your mouse over it
2016-08-12 21:39:52 +02:00
Safihre
641b6ecf35 Encrypted=2 is wrong status code 2016-08-12 21:39:52 +02:00
Safihre
ba9952ca50 disk2 info would show sometimes due to timing
Sometimes if there's only once a tiny difference between disk1 and disk2, it will always show disk2 because it never gets removed again. This can be caused by slight timing issue when calculating free space for disk1 and disk2. Mostly on very fast connections.
2016-08-12 21:39:52 +02:00
paradix
5aac0999ed Handling archives with multiple NZBs from API add_url call (#502)
* archive file per url support

* archive file handling from url

* append .nzb extension when needed

* Update dirscanner.py
2016-08-12 20:33:10 +02:00
shypike
5c5b23ffb5 Update text files for 1.1.0RC3 2016-08-11 22:43:24 +02:00
shypike
96ba468012 Update translations 2016-08-11 19:17:24 +02:00
shypike
fcc9ac8590 Merge pull request #653 from Safihre/develop
Safihre's improvements.
2016-08-11 15:51:35 +02:00
Safihre
763a5f320d If keep_basic then keep the folder
Minor-minor bug I saw appearing in the log
2016-08-11 15:26:43 +02:00
Safihre
9f4387f003 Use Enclosures also for size in RSS 2016-08-11 11:35:17 +02:00
Safihre
f0e11f3024 History wasn't updated on url-grab-fail 2016-08-11 00:16:32 +02:00
Safihre
dda9e70fea Move Rating-host setting to Switches page
For more visability and to show we support more than just OZnzb, if the indexer supports it.
2016-08-10 18:30:00 +02:00
Safihre
0c012fc71c Remove references to OZnzb 2016-08-10 17:46:50 +02:00
Safihre
f17d3bd6b3 More thread-saftey for NZO 2016-08-10 16:31:13 +02:00
Safihre
6dcfb7c801 Only check first usable rar-file for password 2016-08-10 16:05:07 +02:00
Safihre
8096ff8636 Avoid double saves of NZO 2016-08-10 16:05:05 +02:00
Safihre
475b64abb8 Increase save-delay for NZO 2016-08-10 16:05:02 +02:00
Safihre
6b372d2cd7 Only check rar-contents when UnwantedExtensions enabled
Perfmance boost!
2016-08-10 16:04:55 +02:00
shypike
ed250ec379 Revert "Save totals must use copies of the dictionaries to prevent save errors."
This reverts commit c28a9ea192.
2016-08-10 14:35:32 +02:00
Safihre
d976ecbff4 More FAILED checks to be super-super-sure 2016-08-10 14:35:29 +02:00
shypike
d2588f206e Remove traces of obsolete crash detection. 2016-08-10 13:22:29 +02:00
Safihre
d3a8ad18a8 Allow also multiple files from the Upload-form 2016-08-10 12:16:21 +02:00
Safihre
9e65192918 Show actual working upload-counter 2016-08-10 12:16:21 +02:00
shypike
2cc7120ada Prevent a failed job from saving new admin files. (#652)
Can happen when a job is aborted due to the "abort if cannot be completed" option.
Some outstanding articles may still come in after the job is set to "failed".
2016-08-10 12:15:33 +02:00
shypike
4225e1a9eb make_mo.py now detects warnings about missing arguments in texts.
Program exit code will be 2 when any warning is detected.
2016-08-10 10:30:02 +02:00
shypike
7828258bf4 Fix error in Dutch translation 2016-08-09 23:57:30 +02:00
shypike
31288e570d Update translations 2016-08-09 23:50:39 +02:00
shypike
ee5130f9b2 Previous PyLint cleanup revealed hidden errors
The code used sabnzbd.SCAN_FILE_NAME instead of SCAN_FILE_NAME.
The code used sabnzbd.RSS_FILE_NAME instead of RSS_FILE_NAME.
The option --autorestarted wasn't recognized.
2016-08-09 23:46:54 +02:00
shypike
9e46f8adbc A lot of nonfunctional changes from PyLint advice.
One functional change: failed close file due to missing parenthesis.
2016-08-09 22:55:21 +02:00
shypike
4bdbf63866 Merge pull request #651 from Safihre/develop
Glitter and TryList fixes part 2
2016-08-09 21:20:14 +02:00
Safihre
6506646e00 Catch all needed update-times 2016-08-09 20:01:57 +02:00
Safihre
446e676ec1 "Delete page" also deleted jobs waiting for PP 2016-08-09 19:52:49 +02:00
Safihre
c0c645f366 #650 Only do new request after previous finishes 2016-08-09 19:52:46 +02:00
Safihre
9608702e75 The NZBQueue TryList also needs a reset 2016-08-09 19:47:36 +02:00
Safihre
2558fc33a7 Glitter can sort history without API change
Oops!
2016-08-09 19:47:36 +02:00
shypike
2811e5ae7d Fix potential crash when running user notification script
Also fix some other (minor) issues reported by PyLint.
2016-08-09 16:52:18 +02:00
shypike
3a426e9367 Restore capability of unpacking multi-volume 7Zip archives.
The security-related restriction "-t7z" must be "-tsplit" for multi-volume archives.
2016-08-09 16:20:39 +02:00
shypike
53abb84f5e Ignore .pyd too 2016-08-09 16:00:24 +02:00
Safihre
80e89ee1f0 Add index to history-API so Glitter sorting isn't stupid (#648) 2016-08-09 15:15:29 +02:00
shypike
c28a9ea192 Save totals must use copies of the dictionaries to prevent save errors. 2016-08-09 15:05:30 +02:00
shypike
4837d05217 "Prospective par2" must only reset try-lists of the nzo, not of the whole queue.
No need to reset whole queue.
Nasty side-effect: the queue-reset claims another lock, which can lead to deadlock.
2016-08-09 15:02:08 +02:00
shypike
380b5acaeb Update translatable texts. 2016-08-08 21:15:12 +02:00
shypike
657dc6d00c Thread-safety for NzbObject data.
Multi-threaded access to NZO objects may prevent disk saves from working.
Protect all critical NzbObject-methods with IO_LOCK.
Move bulk of prospective-par2 function to NzbObject.
2016-08-08 19:03:10 +02:00
shypike
620c493839 Add retries to database action, when database is locked.
Remove a few obsolete imports.
2016-08-08 19:03:10 +02:00
shypike
edafbeba8e Ensure script output is always HTML-safe. 2016-08-08 19:03:10 +02:00
shypike
8784faf119 Merge pull request #643 from Safihre/develop
Fixes
2016-08-08 19:01:32 +02:00
Safihre
e16bbc0362 SSL labels should be returned as a function 2016-08-08 17:41:28 +02:00
Safihre
3464450337 Add notification about (new) Glitter features 2016-08-04 13:48:26 +02:00
Safihre
ceefffa7f7 Change text to 'HTTPS certificate verification' 2016-08-01 14:58:27 +02:00
Safihre
a62789c4ea Remove code for v0.5.x
And remove a typo
2016-08-01 14:32:02 +02:00
Safihre
d92664e08d Human readable SSL labels on first Config page
Also set v23 first, since it means negotiate max
2016-08-01 14:17:56 +02:00
Safihre
2d5b885f1f Show Encoding on first Config page 2016-08-01 13:26:19 +02:00
Safihre
c43098d02f Encoding was logged twice (line 1250)
Also remove logging unwanted extensions, that's also included when exporting log
2016-08-01 13:11:27 +02:00
Safihre
c67278e3a5 Fix Wiki link 2016-08-01 13:09:10 +02:00
Safihre
dadef20fc9 Don't show Sanatize for Windows, on Windows. Format AllPar2 2016-08-01 12:55:30 +02:00
Safihre
d6218aa937 Move AM/PM switch to Specials
Only applies to SMPL template
2016-08-01 12:55:19 +02:00
Safihre
855a3393c7 fail_hopeless=True by default
Usefull for new users, since all the posts that are taken down by DCMA nowadays
2016-08-01 12:55:09 +02:00
Safihre
6fc95ada55 Update text to Scripts Folder 2016-08-01 12:54:58 +02:00
Safihre
ef483c7e01 Display message to help translate when user has non-English
Since there is almost no exposure anywhere else, this way we can recruit some translators.
2016-08-01 12:54:47 +02:00
Chris
01e01fb7ef Only redirect browser on restart if port configuration changed (#633)
* Only redirect browser on restart if port configuration changed

* Implement port config change detection & redirect on the client side
2016-07-26 19:00:00 +02:00
shypike
fbc8453483 Merge pull request #639 from Safihre/develop
Add HTTPS checkbox to Switches
2016-07-26 18:54:47 +02:00
Safihre
21c581af1f Fix the job index in the API/templates 2016-07-26 16:00:35 +02:00
Safihre
86504613dd Fix #634 by not HTML-converting the Script-stagelog 2016-07-26 15:11:37 +02:00
Safihre
feb7cd36b6 Add HTTPS checkbox to Switches 2016-07-26 12:45:28 +02:00
shypike
28dd8f374a Update translations 2016-07-23 13:25:20 +02:00
shypike
23b922e805 Add credit for XSS vulnerability discovery 2016-07-21 13:56:36 +02:00
shypike
5fc61f7e6d Update text file for 1.1.0RC2 2016-07-20 19:04:18 +02:00
shypike
9b5ae99e9c Erase server error and warning when an article has been received. 2016-07-20 18:45:21 +02:00
Safihre
fe579f61e0 Fix Notification Script and add Parameters-field (#629)
Improve User Notification script.

- Fix Notification Script and add Parameters-field
- The notification text was never send to the script and the return code and return-text of the script were reversed.
- Errors from the Notification script were not shown; if something is wrong, the user should know
- Not breaking existing translations for Script Error
2016-07-19 12:29:03 +02:00
shypike
ba3021132a Merge pull request #625 from Safihre/develop
XSS and stalling
2016-07-15 19:48:00 +02:00
Safihre
c602e46d8e Allow <br/> tags in History details 2016-07-15 19:15:59 +02:00
Safihre
092cc8141a Another anti stalling fix for Prospective 2016-07-14 22:46:10 +02:00
Safihre
d26b3bc351 Correctly handle HTML in filenames in History 2016-07-14 16:47:38 +02:00
shypike
67ccb721a1 Add the module "six".
This PyPi module is required by CherrPy 6.02+,
but is not available on all platforms.
2016-07-13 18:12:39 +02:00
shypike
d93db1dbd2 Make sure that the invoking window disappears 2016-07-13 08:36:05 +02:00
shypike
7ace55b9a7 Merge pull request #623 from Safihre/develop
Bug fixes after 1.1.0 RC1
2016-07-12 14:33:03 +02:00
shypike
3d45993d60 Fix README.mkd 2016-07-09 15:11:32 +02:00
Safihre
17500097f7 Add PID to shutdown URL 2016-07-08 09:56:32 +02:00
Safihre
be15e29d83 Fix passwords being forgotten on name edit 2016-07-06 11:03:10 +02:00
Safihre
7383c47791 CherryPy's urljoin was moved to different library 2016-07-06 11:02:58 +02:00
shypike
aa12e2190f Update text files for 1.1.0RC1 2016-07-05 21:41:41 +02:00
shypike
f32ba6fda9 Merge pull request #609 from sabnzbd/feature/cherrypy602
Feature/cherrypy602
2016-07-05 21:39:52 +02:00
shypike
c8b4b9db94 Patch CherryPy 6.0.2 : Implement 301 redirection for http-->https
Needed for Bonjour support.
2016-07-05 21:38:43 +02:00
shypike
7893f90d26 Patch CherryPy 6.0.2: Prevent crash when encountering a pathless URI. 2016-07-05 21:38:43 +02:00
shypike
5654569c22 CherryPy 6.0.2 patch: to avoid Unicode bugs in PyOpenSSL 0.14
On some systems this resulted in a crash.
2016-07-05 21:38:43 +02:00
shypike
811b625d52 CherryPy 6.0.2 patch: hard-code the version number 2016-07-05 21:38:43 +02:00
shypike
42bba99831 Move to CherryPy 6.0.2, official distribution 2016-07-05 21:38:43 +02:00
shypike
cb3dba196e Merge pull request #607 from Safihre/develop
Fixes and adding sabnzbd.ini to logging
2016-07-02 14:12:40 +02:00
Safihre
36fb621f44 Empty TryList when adding a new par2 file
Otherwise they don't get tried anymore for this file
2016-07-01 17:06:23 +02:00
Safihre
df02fcf519 Catch abort for history-update 2016-07-01 17:00:47 +02:00
Safihre
07e03bc7b9 Update Wiki-link 2016-06-30 15:59:52 +02:00
Safihre
cc2aab83f0 Anonymize more INI variables 2016-06-24 09:25:57 +02:00
shypike
8e0846a682 Require Python 2.7 only
Also fix a typo and remove obsolete debugging function.
2016-06-23 21:12:34 +02:00
Safihre
ba0a0045db Add anonymized sabnzbd.ini to the end of the log 2016-06-23 13:56:54 +02:00
Safihre
6a30ae0bd1 Fix time-left on second page of every skin 2016-06-23 13:56:52 +02:00
Safihre
68299ed1e7 Sometimes Glitter history wasn't updated after partial updates 2016-06-22 14:44:47 +02:00
Safihre
8324f7713f Small stylistic changes to code 2016-06-22 14:28:38 +02:00
Safihre
118fb1cd60 Show server warnings seperatly 2016-06-22 14:21:54 +02:00
shypike
528c7c1410 Fix missing attribute in NzbObject
Side-effect of previous commit.
2016-06-20 08:17:52 +02:00
shypike
a6cda3fe5c Windows: a job with accented chars would get a malformed name when retried.
The SABnzbd_attrib file is always encoded in UTF-8,
so independent of the file system encoding.
2016-06-18 14:09:33 +02:00
shypike
38f03a0c53 Solve race-condition in delete from history.
If an API call History-Delete wants to delete the running job while it has
just reached FAILED or COMPLETED, but not quite finished, things go wrong.
When the actively processed NZO has status FAILED or COMPLETED,
just set a flag and let the post-processor itself clean up the job afterwards.
2016-06-18 00:01:36 +02:00
shypike
6108f9c703 Prevent crashes when scanning non-conforming file names after unpack action. 2016-06-15 23:00:01 +02:00
shypike
abe35f9100 Restore compatibility with smpl skin 2016-06-15 22:51:00 +02:00
Safihre
2ce125789c Avoid auto-filling from password managers (#596) 2016-06-15 12:28:31 +02:00
shypike
5c88c9ae39 Correct typo in previous commit
Method execute() should return False in most error situations.
2016-06-14 08:43:14 +02:00
shypike
6c24a1c630 Make history database error recovery robust and improve code.
Make more robust:
- Put a lock on HistoryDB() class initialization to prevent re-entrant database creation
- Improved recovery from corrupt databases
- Add some exception handling to potential crash cases

Make code nicer.
- Add method comments
- Replace global variables with class-attributes
2016-06-13 22:13:04 +02:00
shypike
6f65d47360 Update text files for 1.1.0Beta1 2016-06-10 18:59:22 +02:00
shypike
7436ac75d6 Remove unused TryList method 2016-06-10 18:43:55 +02:00
shypike
f377f161c8 Pythonify config.py a bit more. 2016-06-10 18:43:55 +02:00
shypike
6682ba37d4 Prevent post-processor crash when end-of-queue script does not exist. 2016-06-10 18:43:55 +02:00
shypike
84a6b8f400 Log the preferred character encoding 2016-06-10 18:32:06 +02:00
shypike
4f2db339ca Fix NZB association for Windows.
Make sure that the second SABnzbd instance sends an UTF-8 encoded URL to the first instance.
Otherwise CherryPy will reject the API call.
2016-06-03 21:55:30 +02:00
shypike
81c808b369 Handle checksum error reports from unrar. 2016-06-02 23:20:26 +02:00
shypike
7beeb084a5 Prevent job from hanging when adding back par2 files.
Sometimes already completed par2 files are being re-added to the queue as extra par2 files.
Because these files are already complete, there will be no attempt to download them
and as a result they will never leave the queue.

Additionally: prevent a negative number for bytes to download.
2016-06-02 21:50:35 +02:00
shypike
a83e9f3a58 Merge pull request #584 from Safihre/develop
SFV verification fixes
2016-05-30 13:45:25 +02:00
Safihre
23b8685418 Glitter cut the stage-log texts incorrectly
It would put the last lines on top
2016-05-30 13:32:06 +02:00
Safihre
353758227b No status was displayed for SFV verification 2016-05-30 13:23:44 +02:00
Sander
36f3bdec8a Happy Eyeballs: typo's fixed (#583) 2016-05-29 23:21:21 +02:00
shypike
f5a017e82c Merge pull request #581 from Safihre/develop
Small fixes
2016-05-29 15:16:00 +02:00
shypike
4c6fa307a8 Support X-DNZB-PASSWORD header. 2016-05-29 15:13:42 +02:00
Safihre
9a077df6ae Updating cache-text cannot be done in skintext.py
It can only be updated in the places where the text is used, otherwise the translations brake.
2016-05-29 13:25:08 +02:00
Safihre
d5d5a9f2af Use already translated text for Restore Defaults
Show the section name to make clear that we only reset this section
2016-05-29 13:21:49 +02:00
Safihre
baa7c06250 Change icon of 'Restore Defaults' 2016-05-26 14:34:26 +02:00
Safihre
9acfbd616d Send 403 when denied access 2016-05-25 11:11:40 +02:00
Safihre
e70e0f16b1 Never show negative time left 2016-05-24 14:16:03 +02:00
Safihre
fcbf0e6a8c Small fix in README 2016-05-23 15:01:15 +02:00
Safihre
bd7ac070ca Another check to make sure decoder doesn't stall 2016-05-23 15:01:15 +02:00
shypike
26fe174ba9 Merge pull request #576 from Safihre/develop-forpull
Fix error in article cache
2016-05-20 21:25:21 +02:00
Safihre
1f60cf8cd3 Download would halt when cache was filled 2016-05-20 21:04:37 +02:00
Safihre
e095bd9247 Stylistic text changes 2016-05-20 21:04:34 +02:00
Safihre
1e9e2f7d89 Downloader was not activated instantly when unpausing single
Had to wait up to 30 sec for the downloader to notice there was a new job.
2016-05-18 14:57:13 +02:00
Safihre
b741d780f7 Do not notify for resume on startup 2016-05-18 14:57:11 +02:00
shypike
90f0c4e21a Merge pull request #565 from Safihre/develop
Fixes part n+1
2016-05-17 21:32:56 +02:00
Safihre
fa496e45ac #558 Also anonymize username in log 2016-05-16 11:33:36 +02:00
shypike
2dc4e66a02 Update 7zip for Windows to latest release and enforce 7zip format when unpacking (part2)
Forgot the newsunpack.py change.
2016-05-15 12:52:03 +02:00
Safihre
fdeaf140a3 #558 Anonymized Debug Logs 2016-05-15 12:49:54 +02:00
Safihre
43ec0d1baf #568 Add code 482 to check for too-many-connections 2016-05-15 12:49:37 +02:00
Safihre
ed1ae75d66 #572 Update RSS interval directly - Fixed 2016-05-15 12:49:27 +02:00
shypike
462a47c927 Update 7zip for Windows to latest release and enforce 7zip format when unpacking.
To protect against recently discovered 7zip vulnerabilities we took some precautions.
http://blog.talosintel.com/2016/05/multiple-7-zip-vulnerabilities.html

The Windows version of 7za has been updated to a non-vulnerable version.
We force 7za to only unpack actual 7zip archives.
This should make exploitation of the UDF- and HFS-vulnerabilities in the 7zip tools impossible.
2016-05-15 12:47:19 +02:00
shypike
ad18406dd6 Don't add discarded duplicate segments to the total download amount. 2016-05-15 09:20:59 +02:00
Safihre
1849af92f5 Correct calculation of download size
Because files can already be partly downloaded before ending up on the extras-list!
2016-05-14 22:56:01 +02:00
Safihre
836aaa2271 Remove &nbps; from server details report 2016-05-14 20:11:14 +02:00
Safihre
af35ac318e Fix space reserved for decoder cache
Data only represented a list of references, not the size of actual text.
2016-05-14 20:11:14 +02:00
Safihre
f8075270be Add 'Restore Defaults' for Switches page 2016-05-14 20:11:14 +02:00
Safihre
fa18c1bda2 Change name of last_history_call to last_history_update
To make it a little more clear what it does
2016-05-14 20:11:14 +02:00
Safihre
5bc6f2b12c Remove asserts throughout code
These cause tremendous (20%) slowdown when run from Python files, especially in the downloader.
2016-05-14 20:11:14 +02:00
Safihre
a204bbe720 Make decoder-queue part of the article-cache
Especially with small article sizes the decoder queue could fill up very quickly causing the downloader to be delayed, even though the system could easily handle them
2016-05-14 20:11:14 +02:00
Safihre
35607d69e0 Drop support for UUencode and show notice when detected
UUencode would only be interesting if we actually supported multipart-UUencode, but we don't.. Only the (never seen) singlepart one.
2016-05-14 20:11:14 +02:00
Safihre
0b29596aab Show only 1 warning when server connection problem 2016-05-14 20:11:14 +02:00
shypike
80e317cad6 Correct calculation of download percentage.
Extra pars which are queued for actual download should not be subtracted from total.
2016-05-14 19:44:43 +02:00
shypike
7ae3a96fb7 Update main POT file. 2016-05-14 14:56:45 +02:00
shypike
9f89601c82 Prevent download percentage from going over 100%
When all par files are filed for download after a failed QuickCheck,
these par files need also to be removed from the extrapar collection of the job.
2016-05-14 12:44:49 +02:00
shypike
859273dec2 Use value from "Status" class for "Propagating" status. 2016-05-13 17:53:33 +02:00
shypike
414ce01424 Fix API compatibility of queue.
The new status DELETED should not be returned by the API.
Tools may not be able to handle it and it's only useful for internal purposes.
2016-05-11 19:12:05 +02:00
shypike
7b4965a940 Prevent Completed and Failed jobs from getting status Deleted. 2016-05-11 19:08:03 +02:00
shypike
a89c04c409 Prevent stalling at 100% when QuickCheck is Off and "Download-all-pars" is On.
The repair function sent all extra par2 files back to the queue
even though they were already downloaded.
2016-05-11 19:07:50 +02:00
shypike
d184d99026 Set default cache size to 450MB on Windows and OSX. 2016-05-11 19:07:20 +02:00
shypike
e1fc427573 Fix potential race condition in BPSmeter. 2016-05-11 19:04:13 +02:00
shypike
f03d3abf9a Accept MIME records that have only LF line endings.
Some tool developers just ignore the rule requiring CRLF.
2016-05-11 19:03:48 +02:00
shypike
3a42b4d02a Fix handling of changed "ignore_samples" option.
Closes #510
2016-05-11 19:00:45 +02:00
shypike
4db550c22c Fix crash in CherryPy when it reports problems with some IPv6 addresses.
A bug in Python's traceback logging causes a crash when an IPv6 address with an embedded % is reported.
2016-05-11 19:00:30 +02:00
shypike
4723b215e3 Fix --ipv6_hosting option.
Repairs commit 1cbff28
2016-05-11 19:00:05 +02:00
shypike
3723ba7c7d Disable listening on IPv6 addresses by the internal web server.
Setting Config->Special->ipv6_hosting to 1 will enable IPv6 listening.
Command line option --ipv6_hosting allows forcing the choice, should SABnzbd not start.
Closes #492
2016-05-11 18:59:47 +02:00
shypike
4fdd022959 Set default for https verification to off. 2016-05-11 18:43:32 +02:00
Sander
f12d4bb435 Log "Preferred Encoding", useful for debugging Unicode problems 2016-05-06 14:03:28 +02:00
shypike
fd315693a9 Merge pull request #556 from jcfp/patch-1
systemd service: add wiki link, remove group handling, housekeeping
2016-04-29 16:11:37 +02:00
jcfp
d375e7901a systemd service: add wiki link, remove group handling, housekeeping
Removing Group=%I because it (wrongly) assumes every username always has a matching groupname. Not specifying a group will simply make systemd use the default group for the given user.
2016-04-29 00:10:21 +02:00
shypike
de26f63026 NNTP error 502 should not aways be interpreted as bad login.
It can also mean "too many connections".
2016-04-27 11:53:15 +02:00
Safihre
ab5b240bde 'session' should also be allowed instead of just 'apikey'
Because of new check introduced previously
2016-04-26 10:13:03 +02:00
Safihre
a19829055d Move expensive for-loop to downloading proccess 2016-04-25 16:22:27 +02:00
Safihre
30cfa5b67a Fix some links in Plush 2016-04-23 16:31:59 +02:00
Safihre
7555eb4e25 Remove another for-loop and add nzf.subject if no nzf.filename 2016-04-23 16:23:47 +02:00
Safihre
78a999af34 Force MIME types for CSS and JS files
Caused problems on Windows if external programs overwriten it in registery.
See: http://forums.sabnzbd.org/viewtopic.php?f=2&t=20490
And: https://www.reddit.com/r/usenet/comments/4fkmcx/my_sab_interface_is_text_only/
2016-04-22 22:36:57 +02:00
Safihre
86339c4393 Create new ICO files 2016-04-22 22:36:57 +02:00
Safihre
5810d36079 NZB/API key are read-only
They always have been, but now the user won't have the illusion they can change it
2016-04-22 22:36:57 +02:00
Safihre
c677116992 Remove references to SourceForge in RSS 2016-04-22 22:36:57 +02:00
Safihre
69b6d5fead TAPI needs extra check
In case api-key is disabled but password enabled, it would give full access even though it shouldn't.
2016-04-22 22:36:57 +02:00
shypike
073f7afdf7 Remove tabs and trailing spaces.
These things give needless differences in commits.
Contributors: instruct your editor to use spaces instead of tabs and to remove trailing spaces.
2016-04-22 21:59:47 +02:00
shypike
36b7b4b26b Optimize by removing file lists from the queue API calls.
File lists per job are only needed to edit a single job,
so only supply them to the API call "get_files".
2016-04-22 21:59:47 +02:00
Safihre
2cf2406ecd Remove for's in gather_info 2016-04-22 21:59:47 +02:00
Safihre
da832c1a4a Glitter Limited refresh had no item limit or start
Causing slowdown in back-end on big queues
2016-04-22 21:59:47 +02:00
Safihre
55c6a98ab1 Remove queue verbosity option and clean up build_queue 2016-04-22 21:59:47 +02:00
Safihre
5d84067db4 Remove those legacy variables 2016-04-22 21:59:47 +02:00
Safihre
d9239fa0ac Extend CherryPy timeout to 10min for API-calls 2016-04-22 21:59:47 +02:00
Safihre
a62c498298 Only save article cache to disk on pause 2016-04-22 21:59:47 +02:00
shypike
890a093054 Optimize API by removing queue queries for functions that don't need it.
Config pages, History page and related API calls have no need for Queue info,
so don't waste time on it.
Optimize queue report by not formatting unneeded elements.
Reduce at the lowest level, so nzo.gather_info() should only return the requested selection.
Large impact on parameter passing.
It should remain compatible with the current API-s.
2016-04-22 21:59:47 +02:00
shypike
dd066f0fd3 The pre-queue script can now return an accept value of 2, meaning immediate failure.
Supports front-ends which need the signal that an NZB has been
rejected by the pre-queue script.
2016-04-22 21:54:26 +02:00
shypike
52b6db8e93 Add start script for portable Windows installations 2016-04-22 16:38:29 +02:00
shypike
7755928e09 Merge pull request #515 from sabnzbd/feature/cherrypy5
Feature/cherrypy5
2016-04-16 18:14:03 +02:00
shypike
69ce6e3311 Patch CherryPy 5.1.0: Fix CherryPy's header parsing bug.
Due to a bug in its header parsing, uploaded files could not contain semicolons.
CherryPy bug: 1397
2016-04-16 18:13:15 +02:00
shypike
d694184cce Patch CherryPy 5.1.0: Prevent crash when encountering unexpected URI structure. 2016-04-16 18:13:15 +02:00
shypike
5522e59932 Patch CherryPy 5.1.0: Prevent crash when encountering a pathless URI. 2016-04-16 18:13:15 +02:00
shypike
69ea306967 Patch CherryPy 5.1.0: to avoid Unicode bugs in PyOpenSSL 0.14
On some systems this resulted in a crash.
2016-04-16 18:13:15 +02:00
shypike
cc08040774 Patch CherryPy 5.1.0 : Implement 301 redirection for http-->https
Needed for Bonjour support.
2016-04-16 18:13:15 +02:00
shypike
9f1d1c5cb7 Update CherryPy to official 5.1.0 release 2016-04-16 18:13:14 +02:00
shypike
a9a86969ef Merge pull request #544 from Safihre/propegation_delay
Propagation delay & Interfaces fixes
2016-04-16 18:11:39 +02:00
shypike
b28be4e6f2 Revert "#483-#326 Add option for Propagation Delay"
This reverts commit 1b384e8d70.
2016-04-16 18:10:25 +02:00
shypike
5b1dc8b1ff Revert "Harmonize checks in has_articles_for and get_article"
This reverts commit e2cc5ea3ca.
2016-04-16 18:10:14 +02:00
Safihre
e2cc5ea3ca Harmonize checks in has_articles_for and get_article 2016-04-16 15:37:21 +02:00
Safihre
1b384e8d70 #483-#326 Add option for Propagation Delay 2016-04-16 15:37:21 +02:00
Safihre
53b46cb56b Update favicons 2016-04-16 14:28:50 +02:00
Safihre
340b4c98e0 #521 Update iOS/Android logo 2016-04-16 12:06:15 +02:00
Safihre
ba709bd086 #521 Add new logo to wizard 2016-04-16 11:35:17 +02:00
Safihre
7c839b8b65 Remove un-used images 2016-04-16 11:20:33 +02:00
Safihre
173aa430b3 Tiny fix of modal close button in Config 2016-04-15 21:59:33 +02:00
Safihre
5f55b2efeb Clearly show if saving of Config failed 2016-04-14 22:16:04 +02:00
Safihre
17e69db915 Add message to login screen when session wil expire 2016-04-14 22:06:24 +02:00
Safihre
1c98c13b84 Redirect to login for Plush and smpl 2016-04-14 13:10:00 +02:00
Safihre
206d166cf4 Add notification that restart will require login again 2016-04-14 10:56:55 +02:00
Safihre
7c0d4eb77d #521 Add new logo to Plush and Glitter help 2016-04-14 10:56:01 +02:00
Safihre
adde58101b Add Propagation Delay option 2016-04-13 18:49:31 +02:00
Safihre
77c5237f06 Re-introduce label in download name
Turns out I accidentally deleted that in February when removing the
password from the name.. Oops!
2016-04-13 16:55:45 +02:00
shypike
350f8a64fa Merge pull request #541 from Safihre/develop
Safihre's fixes part 1
2016-04-13 12:32:01 +02:00
Safihre
f76e110701 #521 New logo in Glitter and Config preview 2016-04-13 09:39:02 +02:00
Safihre
8c557dfa19 Improve INFO message for verify/repair
Since if QuickCheck succeeds, par2 is not run.
2016-04-13 09:39:01 +02:00
shypike
4f3cd2fc50 Merge pull request #496 from Tremax/handle-ssl-beast
Handle 1/n-1 splitting
2016-04-12 21:25:07 +02:00
shypike
b0f3c8c766 Merge pull request #533 from Safihre/prospective_blocks
Prospective adding of par2 files when articles are damaged
2016-04-12 21:24:53 +02:00
shypike
a8788da698 Prevent creating orphan items in "incomplete" when deleting downloading jobs.
Due to previous issues where articles could be lost,
the nzf.deleted and no.deleted flags were not obeyed.
This could lead to creation of orphans when lost articles would be flushed.

Better solution: drop articles only when job is in a final state.
Also prevent NZO files from being saved when job is in "deleted" state.
2016-04-12 21:21:30 +02:00
shypike
239dcf98e2 Merge pull request #540 from jdfalk/systemdfix
Update sabnzbd@.service with network requirements
2016-04-10 23:09:24 +02:00
jdfalk
fc916a8824 Update sabnzbd@.service
1. Added requirement for network to be up before sab starts.
2. Explicitly set service type to simple.
3. Enabled sabnzbd restart on service failure via systemd.
2016-04-10 13:44:57 -07:00
Safihre
c4ee245934 #448-#126 Forced item with missing articles caused overflow in paused queue
Closes #448
Closes #126
2016-04-10 21:42:39 +02:00
Safihre
b199f83cd5 Incorporate while-loop to make sure we add enough 2016-04-08 10:45:24 +02:00
Safihre
2ad1564879 Prospective adding of par2 files when articles are damaged 2016-04-08 10:45:24 +02:00
shypike
c4eb06ab86 Merge pull request #531 from Safihre/noduplicatefiles
If file occurs twice in NZB, only add the larger
2016-04-07 21:48:50 +02:00
shypike
02cf6d5238 Prevent API crashes when 'mode' or 'name' have double entries. 2016-04-07 21:46:32 +02:00
shypike
59b0fd2a9d Merge pull request #536 from Safihre/develop
Interface fixes part 18
2016-04-07 21:43:15 +02:00
Safihre
a68ced22fe #538 'Add all' for Orphaned Jobs 2016-04-07 20:29:49 +02:00
Safihre
d90e88c2a1 Add notification for succesfull login 2016-04-07 08:58:15 +02:00
Safihre
396f6ecd5d Change of username/password should cause restart
This way the security code is reset so any unwanted guests will be locked out.
2016-04-06 20:47:53 +02:00
Safihre
3e53049f39 #464 Grabbing items don't always have status=grabbing
But now they do!
2016-04-06 19:14:13 +02:00
Safihre
0e8f1b0f01 Tabbed/Compact setting was not remembered 2016-04-06 19:14:13 +02:00
Safihre
8ea69b70b6 #525 Better handeling of PP history events
Not everything was caught by 02eacb17180edd37010287f391a41aa2791f67e7
2016-04-06 19:14:13 +02:00
Safihre
090560671b Add Optional label to Retry password 2016-04-06 19:14:13 +02:00
Safihre
150e117f47 Add background information to Glitter 2016-04-06 19:14:13 +02:00
Safihre
ff65b6ccec #534 fix borders and show active connections 2016-04-06 19:14:13 +02:00
Safihre
9a66c0cb31 #525 Catch more history updates 2016-04-06 19:14:13 +02:00
Safihre
41176bd90f #530 do not ignore files in QuickCheck
Par2 wouldn't ignore them either
2016-04-06 19:14:13 +02:00
Safihre
29cbf1d15c Update cache text to more 2016 values 2016-04-06 19:14:13 +02:00
Safihre
c973d2b527 Split Glitter in sub-files
Was just getting too big of a file!
2016-04-06 19:14:13 +02:00
Safihre
0e71dd26a9 If file occurs twice in NZB, only add the larger 2016-04-02 17:51:36 +02:00
shypike
9ec35d2a25 Fix typo in user notification script code. 2016-04-02 15:30:51 +02:00
shypike
9540089f1b Merge pull request #525 from Safihre/nohistory
Do not calculate history every API call and more fixes
2016-04-01 20:46:25 +02:00
Safihre
2515712f59 Faster refresh on searching 2016-04-01 12:07:12 +02:00
Safihre
8c05cdd28f #521 Preview the new logo in Glitter 2016-04-01 12:07:11 +02:00
Safihre
8b3fb6913e Add Speed as Extra history column 2016-04-01 12:07:10 +02:00
Safihre
f2e2571d11 #529 Fix unicode strip in OptionStr 2016-04-01 12:07:09 +02:00
Safihre
6ce93743d0 Add option for Extra history column 2016-03-31 16:03:10 +02:00
Safihre
ec703f21e9 Stylistic changes to lowercase 2016-03-31 11:10:14 +02:00
Safihre
cbde31ea4b Small fixes of Glitter Compact&Tabbed combi 2016-03-31 11:10:13 +02:00
Safihre
1daa9e66f6 Also update history on end of postproc 2016-03-30 10:27:35 +02:00
Safihre
597efc661a Do not calculate history every API call 2016-03-30 09:52:31 +02:00
shypike
a7fbdfb0a6 Merge pull request #526 from Safihre/develop
Interface fixes part 17
2016-03-29 20:59:31 +02:00
Safihre
68377b9754 Update jQuery 2016-03-29 14:48:34 +02:00
Safihre
03c2d7ef31 Small Glitter Night interface fix 2016-03-29 14:37:38 +02:00
Safihre
2e3fcac656 #527 Fix "Download all par2 files" behavior 2016-03-29 14:18:03 +02:00
Safihre
7ad01052a1 DB Strings should be encapsulated in ' not " 2016-03-29 14:15:23 +02:00
Safihre
f5191bb364 Improve Glitter Compact and Tabbed layouts 2016-03-29 14:15:23 +02:00
Safihre
f4d6776d77 Fix tabbed navigation for Glitter Night 2016-03-29 14:15:23 +02:00
Safihre
0e28bb2098 Fix #520 by removing space 2016-03-29 14:15:23 +02:00
Safihre
a671744873 Change description of 'Tabbed layout; 2016-03-29 14:15:23 +02:00
shypike
80742e4a0d Improve saving of BPSMeter.
Sometimes clearing of the server counters isn't saved to disk.
2016-03-26 16:40:10 +01:00
shypike
3435886fe7 Fix priority issue in PushOver and PushBullet.
Fixes error introduced in 91f5eea7ba
2016-03-24 21:36:13 +01:00
shypike
ac0821e71b Fix PushOver support.
Adjust priority levels to the current PushOver API.
Re-enable device field now that the PushOver API supports readable device labels.
2016-03-24 21:29:46 +01:00
shypike
122ae5ccfa Fix race-condition when deleting an actively downloading job.
Closes #237
2016-03-24 21:07:38 +01:00
shypike
c36e7edf3f Fix race condition when API and postprocessor both want to delete a history item. 2016-03-24 21:07:22 +01:00
shypike
8987265537 When urlgrabber receives a 404, stop trying. 2016-03-24 21:07:06 +01:00
shypike
5e6fb0e694 Prevent incompatibility due to missing 'script_log' field.
Fixes commit c0f2f59fc1
2016-03-24 21:06:51 +01:00
Safihre
3c4e553733 Dynamic sizing of the connections-tab 2016-03-24 20:32:17 +01:00
Safihre
8e6b30092c Add option for tabbed layout 2016-03-24 20:32:17 +01:00
Safihre
b6fcc2a466 Force close tooltips on closing job settings 2016-03-24 20:32:17 +01:00
savef
df2e24e6cd Treat ambiguous numeric values as number of minutes for custom pause time.
Currently if you just type "100" into the custom pause field it'll think you want 143015 minutes, that's useless. A lot of people are probably used to the old Plush behaviour of entering the number of minutes you want to pause for, it's also a much saner default. So in the case that the user just enters some numbers and nothing else, this assumes they want to pause for that many minutes.
2016-03-24 15:36:37 +00:00
shypike
fb6fd1ab20 Bump signature for self-signed certificate to sha256 2016-03-23 23:08:17 +01:00
shypike
fa9b236dc5 Improve running of user's notification script.
Followup of commit ccba56e
2016-03-22 20:12:39 +01:00
shypike
e6887e8a0d extract_pot.py: Make NSIS file presence optional. 2016-03-20 18:32:52 +01:00
shypike
6bbf3f12d9 Update main POT file. 2016-03-20 18:31:24 +01:00
Safihre
07a67521fa Fix breaking Glitter bug with large script_log 2016-03-19 17:09:38 +01:00
Safihre
83b78d8a3f Few more growler->notifier 2016-03-19 17:06:21 +01:00
Dominique Barton
ccba56e073 Added support for custom notification script
Closes #458
2016-03-19 17:00:48 +01:00
Safihre
91f5eea7ba Notifier: Use get_prio function to get priorities 2016-03-19 14:22:39 +01:00
Dominique Barton
4752790081 Bugfix for full disk Pushbullet notifications 2016-03-19 13:57:16 +01:00
shypike
aea04e8406 Improve handling of passwords embedded in the NZB.
Show first password from NZB in the title of the user hasn't set one.
Try them all: user password, NZB-passwords, from password file.
2016-03-19 13:25:21 +01:00
shypike
cc54006aa5 Remove "release" prefix
Don't want to rename existing release branches.
2016-03-19 12:49:44 +01:00
shypike
e10ca1c852 Update the workflow 2016-03-19 12:05:18 +01:00
shypike
1d902885a8 Self-signed certificates are now signed with SHA1 instead of MD5.
Also bump size from 1024 to 2048.
2016-03-19 11:45:46 +01:00
shypike
809aebd2d7 Merge pull request #508 from Safihre/develop
Interface fixes part 16
2016-03-18 16:34:04 +01:00
Safihre
ab0d8e0927 #446 IPv4 embedded in IPv6 for local detection
Closes #446
2016-03-18 09:57:56 +01:00
Safihre
38e74d6d24 Update version numbering in develop branch 2016-03-16 13:58:01 +01:00
Safihre
b2ca779996 Clean up Status and Interface code 2016-03-14 08:39:59 +01:00
Safihre
fba5131d66 #507 Create QR-code internally 2016-03-14 08:39:53 +01:00
shypike
69cef6b006 Merge pull request #501 from sanderjo/ipv4_in_case_of_ipv6
Because of dual IPv4/IPv6 clients, finding the public ipv4 needs special attention
2016-03-10 22:51:09 +01:00
shypike
c52d122984 Fix display of SABnzbd's icon by OSX Notification Center.
El Capitan doesn't accept the -sender parameter, so just omit it.
2016-03-08 16:40:17 +01:00
shypike
709a317f55 Suppress errors/warnings about bad "Rating" files when the feature is disabled. 2016-03-08 16:01:34 +01:00
Sander Jonkers
205b908b8d Because of dual IPv4/IPv6 clients, finding the public ipv4 needs special attention 2016-03-06 01:39:41 +01:00
shypike
69f77ac733 Merge pull request #499 from Safihre/develop
Interfaces fixes part 15 (for the final)
2016-03-05 11:21:09 +01:00
Safihre
5a70d3f52e Fix Plush dashboard 2016-03-05 11:01:36 +01:00
Safihre
5b11d1c719 Fix Glitter display in <1MB/s range 2016-03-05 11:01:36 +01:00
shypike
61239a8f42 Update translations 2016-03-04 18:54:13 +01:00
Max
25a54fb593 handle 1/n-1 splitting to prevent Rizzo/Duong-Beast 2016-03-04 04:23:16 +02:00
shypike
64b182ca9a Update main POT file 2016-03-01 22:00:57 +01:00
shypike
056fb39380 Merge pull request #493 from Safihre/develop
Interface fixes part 15
2016-03-01 21:58:43 +01:00
Safihre
7f03e2800e Visual feedback when testing server/notifications
Nice little spinner
2016-03-01 16:40:50 +01:00
Safihre
427661589a Use the Status-API in Glitter instead of fake-JSON 2016-02-29 21:41:15 +01:00
Safihre
417a333cdd Move Status-info to API like queue/history 2016-02-29 21:41:14 +01:00
Safihre
90131327c6 #489 wrongly encoded & in the preload 2016-02-29 21:41:13 +01:00
Safihre
121fd45bb8 Add notice when no 'Default' server-category
To make users aware of accidents
2016-02-27 12:40:26 +01:00
Safihre
f0121d526b 'Default' not translated in Server-category 2016-02-27 12:40:24 +01:00
Safihre
bfdf0b453c Unify config testing reports 2016-02-26 14:32:47 +01:00
Safihre
01a725d8bf Server status tweaks 2016-02-26 14:32:46 +01:00
Safihre
6b38ad9d37 Small config improvements 2016-02-26 14:32:44 +01:00
shypike
bd991e1010 Fix IP test at startup.
Correction of problem introduced by commit afff88b "Use self-test.sabnzbd.org for connection tests".
2016-02-24 22:28:41 +01:00
shypike
c58774b341 Update translations 2016-02-24 21:16:22 +01:00
Chris Thorn
176affe115 Parse bandwidth limit as a float instead of an integer so that non-integer values can be used as bandwidth limit 2016-02-24 19:59:12 +01:00
shypike
30161c5f6c Merge pull request #486 from Safihre/develop
Interface fixes part 14
2016-02-24 19:53:19 +01:00
shypike
21320d9e22 Changing server priorities during a download could lead to unexpected results and lockups.
Target priority of articles must be kept at the TryList level so that they
are reset when the try_list is reset.
Closes #378
2016-02-24 19:37:46 +01:00
Safihre
03a918b93b Plush dashboard local IPv4 wouldn't show if external failed
Typo!
For 1.0.0
2016-02-24 15:36:04 +01:00
Safihre
058de93b36 Improve compact option in Glitter 2016-02-23 18:38:22 +01:00
Safihre
afff88be57 Use self-test.sabnzbd.org for connection tests 2016-02-23 18:38:21 +01:00
Safihre
699a518144 Add link to information about SSL/yEnc 2016-02-23 18:38:19 +01:00
Safihre
fd6fdd224c Tweak first Config page 2016-02-23 09:13:15 +01:00
Safihre
8b00762a98 Improve display of message on blocked server 2016-02-23 09:13:14 +01:00
Safihre
ac9d4f7451 Add trailing "/" to href's 2016-02-23 09:13:13 +01:00
Safihre
d8da78662a Connection refresh improvement
Make sure Connections get refreshed also after open->close->open
2016-02-22 11:23:02 +01:00
shypike
6c7cdfcf75 Update main POT file 2016-02-19 22:46:47 +01:00
shypike
a3bed4689f Prevent UI errors due to history_db handle not being available. 2016-02-19 22:17:08 +01:00
Jonathon Saine
f0852e192d Add Issues link back to config base page. Make sorting Key's look more like a button (not touching presets). Fix minor cosmetic issue with paths text being cut off. Remove bottom shadow from buttons when attached to inputs so it doesnt look so odd. 2016-02-19 21:56:01 +01:00
Safihre
e95b3c6eca Display password in skins 2016-02-19 11:17:40 +01:00
Safihre
b93f68146e Remove password from title 2016-02-19 11:17:39 +01:00
Safihre
d07fcee923 Connections tab auto-refresh 2016-02-19 11:17:38 +01:00
Safihre
d2a1a72fbb Filegrabber did not sanatize filename 2016-02-19 11:17:37 +01:00
Safihre
040d4c0454 Catch failing Windows Notification
I assumed Windows notifications could not fail, but clearly they can:
http://forums.sabnzbd.org/viewtopic.php?f=11&t=20211&p=104438
When no tray-icon is available, it will fail and in the case of
completed NZB message it will restart the whole of SABnzbd.
2016-02-17 21:23:39 +01:00
Safihre
f6fe2247fc Add Glitter option for Compact Layout
There is still some work to be done, but this is the first version!
2016-02-17 20:59:32 +01:00
Safihre
acb1fdb530 Notifications in Night-skin were not colored 2016-02-17 20:59:32 +01:00
Safihre
c5dcd5581f Orphan add/remove notification differs 2016-02-17 20:59:32 +01:00
Safihre
9a466e9180 Limit to max 250 items per page (realistic limit) 2016-02-17 20:59:32 +01:00
Safihre
d680aae4ec Make 'Show connections' persist after page refresh 2016-02-17 20:59:32 +01:00
Safihre
84bcdf2cd6 Add Pystone score to Glitter status 2016-02-17 20:59:32 +01:00
Safihre
7672a57209 Show sysload in Glitter
Kind of forgot about it since I'm a windows user..
2016-02-17 20:59:32 +01:00
Jonathon Saine
4905cd0cc4 Add pyOpenSSL info to startup/utility/config base. Add OpenSSL & yEnc to config base as well. 2016-02-17 09:11:07 -06:00
shypike
c7bc25b847 Disable https verification when uploading NZB to running instance of SABnzbd. 2016-02-13 16:11:17 +01:00
shypike
366725451d Update main POT file. 2016-02-12 21:37:43 +01:00
shypike
23208ab71a Merge pull request #471 from Safihre/let-the-sparks-fly
Adding overlay-notifications to Glitter
2016-02-12 21:11:49 +01:00
Safihre
1b5dada461 Adding overlay-notifications to Glitter 2016-02-12 15:09:04 +01:00
shypike
41dad8b379 Update translations 2016-02-11 01:04:33 +01:00
Safihre
c82e586b05 Glitter broke on empty preload strings
See
http://forums.sabnzbd.org/viewtopic.php?f=11&t=20192&p=104368#p104368
In case of UnicodeError
2016-02-10 22:48:18 +01:00
Safihre
8cfb605931 Don't allow name change or filesview on grabbing 2016-02-09 09:16:31 +01:00
Safihre
184c350e16 Make disabled servers look more disabled 2016-02-07 13:22:04 +01:00
Safihre
1f116940fc #408 Show red when Add-NZB left empty 2016-02-07 10:54:59 +01:00
shypike
456fe124a9 Update main POT file. 2016-02-05 23:38:36 +01:00
Safihre
fdddcc9ab7 Do not confirm clearing warnings 2016-02-05 23:17:28 +01:00
Safihre
4afc23cd21 Link in message about Orphans was broken 2016-02-05 09:42:50 +01:00
Safihre
85aaf539e3 Message labels were not translated
INFO and WARNING
2016-02-05 09:32:25 +01:00
Safihre
9860e67ca0 Improve message about no localStorage
It happens more than I expected, so better make a proper message.
2016-02-05 09:22:44 +01:00
shypike
386b59a755 Solve file name encoding issues for OSX.
- Names returned by unrar-commandline need to be Unicoded.
- For yEnc embedded names, only test for UTF-8 and CP1252, but not the local codepage.
- Suppress some bogus warnings
- Log the output of the unrar tool
2016-02-01 21:30:36 +01:00
shypike
7e95a7cd88 Fix trouble with disk speed meter (especially on Windows) Part 2.
Improve the benchmark by reducing Python overhead by writing larger blocks.
2016-02-01 20:08:54 +01:00
shypike
2c9232272a Bump default release to 1.1.x 2016-02-01 19:29:47 +01:00
shypike
3d0e7bf2a1 Fix links in text files to pooit to 1.0.0 entries in the Wiki. 2016-02-01 19:13:52 +01:00
shypike
e6bf540057 Fix all links in the templates to point to 1.0.0 (or 1-0) entries in the Wiki. 2016-02-01 19:13:16 +01:00
shypike
917b18ab09 Fix trouble with disk speed meter (especially on Windows) Part 2.
Move fix outside of the measurement loop.
Don't remove the test-file within the loop, but only after it's finished,
otherwise we'll still get a "Permission denied".
2016-01-30 10:55:35 +01:00
shypike
c6687163f0 Fix trouble with disk speed meter (especially on Windows).
Better handling when folder is not available or writable.
Windows requires some access outside of the Python code to avoid "permission denied".
2016-01-29 23:54:07 +01:00
shypike
e12378eae4 Update main POT file. 2016-01-29 19:56:56 +01:00
shypike
6d2e7a0b96 Add a link on the main Config page, to the "issues" page on the Wiki 2016-01-29 19:54:39 +01:00
shypike
1c60b48c32 Merge pull request #443 from Safihre/develop
Interface fixes part 11
2016-01-29 19:00:55 +01:00
shypike
755420ba7a Merge pull request #442 from Safihre/newlogin
Add HTML login page
2016-01-29 18:57:42 +01:00
shypike
3def1d76d2 Fix typos that prevented notifications about disk full being sent. 2016-01-28 23:18:51 +01:00
Safihre
006d10bd55 #408 Firefox-fix: word-wrap instead of word-break 2016-01-28 12:15:44 +01:00
Safihre
1a0cd88eca Update Glitter jQuery to 2.2.0 2016-01-27 22:25:58 +01:00
Safihre
d5f7827805 Grey out disabled Config sections when unchecked 2016-01-27 10:34:18 +01:00
Safihre
db0450509f #447 only show arguments-field for speedlimit 2016-01-26 10:57:45 +01:00
Safihre
6f5d6fcf33 Use SHA1 instead of Base64 for hashing cookie
A very obvious mistake I made! Base64 anyone could have 'cracked' the
code so easily, not with SHA1. I stuck with SHA1 and not better one
because it's much faster and safe enough for our application.
2016-01-26 10:35:02 +01:00
Safihre
1a2ee47901 Adding a link to the Login-info page 2016-01-25 14:54:14 +01:00
Safihre
62fd4cc838 Remember state of the 'Remember me' checkbox 2016-01-25 12:42:06 +01:00
Safihre
88d72d23b6 Logout link only when needed
So not when we have only external login turned on and we are internal.
2016-01-25 12:31:42 +01:00
Safihre
f5f3507626 Make HTML authentication default (can be disabled in specials)
Because the 'Only external access requires login' only works with the
HTML login.
2016-01-25 12:31:41 +01:00
Safihre
83e4cc1a4e Implement 'Remember me' for HTML login
Although after a restart or change of IP, it will be lost
2016-01-25 12:31:40 +01:00
Safihre
cc36843f9a #447 Servers in scheduler more clear 2016-01-24 23:19:17 +01:00
Safihre
bebe41a07a #445 Reduce statusinfo timeout on startup 2016-01-24 22:37:48 +01:00
Safihre
2a3ec43adc Add option to only require login for external IP's 2016-01-24 22:34:18 +01:00
Safihre
8918595776 #444 HTTPS instead of HTTP for RSS favicon 2016-01-24 20:46:30 +01:00
Safihre
e23d5234c5 Fix breaking RSS page
Fixes http://forums.sabnzbd.org/viewtopic.php?f=11&t=20114
2016-01-24 20:46:29 +01:00
Safihre
1e3c9e5576 Make cookie-secret more secret
The process-ID was too easy to 'guess', the new ID won't be that easy to guess.
2016-01-24 18:09:36 +01:00
Safihre
4afd0f2d84 Add HTML login page
Based on Cookies. We create an unique cookie based on user-IP,
SAB-process-ID and a salt to facilitate also log-out.
Yes, this cookie can be stolen when on a public wifi-network (same IP)
when not on HTTPS, but so can basic-auth requests.
2016-01-24 18:09:34 +01:00
shypike
12801c57b6 Update translations 2016-01-24 16:30:36 +01:00
shypike
199f174adc Allow "None" as selection for secondary skin. 2016-01-24 16:15:25 +01:00
shypike
f5f982ed6d Prevent multiple resume notifications.
Only report "Resume" when coming from Paused mode.
2016-01-24 15:09:36 +01:00
shypike
d8abc2ad4b Notifications template contained two instances of "ncenter_enable", leading to problems.
The result was a list instead of a single value.
2016-01-24 14:53:13 +01:00
shypike
1cb9bbe7cc Improve handling of an old queue when upgrading to 0.8.0+
Warn only for a non-empty queue.
For Windows, when using a relative "download_dir" based on the user profile,
prepend the path with "Documents".
No action when the -f parameter was used.
This way it will be easier to restore existing jobs.
2016-01-23 23:54:53 +01:00
shypike
c2022e8fa4 Merge pull request #441 from Safihre/develop
Firefox workarounds
2016-01-23 17:34:22 +01:00
Safihre
24e075bf24 Firefox needs more time to process the Server AJAX 2016-01-23 16:41:04 +01:00
Safihre
2965e262a6 #438 Try to stop Firefox from checking checkboxes 2016-01-22 11:06:34 +01:00
shypike
2b71ce8927 Fix problem where a stray RAR file would cause a failed unpack run.
When a job of which the RAR files are renamed by par2,
needs repair of one more more files, the original damaged files will stay behind.
This will cause SABnzbd to try a doomed attempt at unpacking.
SABnzbd should keep track of such files and delete them after repair.
2016-01-21 22:25:41 +01:00
shypike
5a872c62df Fix potential crash when reverting par2 renames.
Can happen when files have been modified outside of SABnzbd's control.
2016-01-20 21:49:28 +01:00
shypike
db64ce75ad For SSL protocol choice, default to auto-negotiation.
The "V23" method is interpreted by OpenSSL as "negotiate the highest available protocol"
and should therefor be a safe choice (best chance of working and best security).
If a user wants to, it is possible to fix the protocol, to prevent interference in the negotiation.
We cannot just assume that we can use our highest fixed protocol, because some Usenet servers
are being slow with implementing TLS1.2
2016-01-20 20:47:06 +01:00
shypike
9457883707 Perform IPv6 test on port 443 instead of 80.
Works better with some proxies.
Closes issue #274
2016-01-19 18:12:55 +01:00
shypike
c5767b061d When trying to connect to another SABnzbd instance over HTTPS, don't verify certificates.
Very few SABnzbd installations will have valid certificates.
2016-01-19 18:04:09 +01:00
Safihre
7e64c3af5f Re-order Switches page 2016-01-19 09:18:12 +01:00
Safihre
af36b81cd2 Config fixes 2016-01-17 23:31:50 +01:00
Safihre
e891d14873 #408 Refresh on Config Special save to update the * 2016-01-17 22:23:15 +01:00
Safihre
783e56d00d #408 Also close history-details on history-row click
Before it would only open
2016-01-17 22:19:54 +01:00
Safihre
2fbeb0ab1e #408 Browser navbar to black on mobile 2016-01-17 22:04:42 +01:00
Safihre
3df68ac9f9 #408 Extra space next to Folder icon 2016-01-17 22:02:44 +01:00
Safihre
911af303bf #432 Change filename to name in Add NZB 2016-01-17 21:57:44 +01:00
Safihre
2a380ad0db End of queue script was forgotten in Glitter 2016-01-16 19:26:52 +01:00
shypike
fc1ce199ca The compiled OSX build wasn't restarted with original command line arguments.
Rare use case where the App was originally started with parameters.
Essential for correct preservation of the -f parameter.
2016-01-16 16:09:01 +01:00
shypike
af6a50c27e Add Special option "fixedd_ports" to prevent auto search for a free web port.
Useful in debugging situations where there might be stray processes running.
The option will prevent moving to other ports, but will instead terminate SABnzbd.
2016-01-16 15:10:07 +01:00
shypike
b7a83eda68 Replace awkward PNFO/QNFO/ANFO list indexes with named tuples. 2016-01-16 13:02:28 +01:00
shypike
714cd9621c Use any() and all() functions where appropriate.
Better than creating a list with only True-s.
2016-01-16 12:33:19 +01:00
shypike
e309492e2a Improve code for version check.
Don't use a semicolon-separated string, but just a tuple.
2016-01-16 12:31:48 +01:00
shypike
1760a20671 Sometimes the "latest release" label shown wasn't the correct one.
Add some extra logging too.
2016-01-16 12:24:19 +01:00
Safihre
f682f68966 Growl wrongly enabled on Linux 2016-01-13 21:50:30 +01:00
Safihre
2a849c0e97 Default skin not set correctly in Wizzard
On Linux 'Glitter - Night' is top of the list in Config, so on the first
opening of the Config it would switch the layout to 'Glitter - Night'
2016-01-13 21:50:30 +01:00
shypike
e1e05b132d Update translations 2016-01-13 20:15:34 +01:00
shypike
04ef0beacf Update to 2016. 2016-01-13 20:04:21 +01:00
shypike
060815dabe Sometimes an uploaded filename contains the full URL, strip it down.
Closes #422
2016-01-12 21:44:11 +01:00
Safihre
c7d9e5ce90 FR #63: Option to show hidden folders on Unix 2016-01-12 21:21:27 +01:00
Safihre
8bc9fd821f Missing INCOMPLETE and UNWANTED for filter 2016-01-12 21:21:27 +01:00
Safihre
5a46c8f691 192x192 favicon causes problems in IE 2016-01-12 21:21:27 +01:00
jcfp
aced9d9241 Also update warning msg in part 1 of the wizard 2016-01-12 20:01:19 +01:00
jcfp
cdf8c42d3d Correct openssl package name in log message
Noticed someone using the incorrect package name in https://forums.sabnzbd.org/viewtopic.php?f=2&t=20018
2016-01-12 11:47:38 +01:00
shypike
5e81b85f21 Enable the use of extra par2 parameters in other platforms than Windows. 2016-01-09 17:08:23 +01:00
Safihre
9b1d44f98a Category names with " cause HTML problems 2016-01-07 21:23:51 +01:00
shypike
1b477f4885 CherrpPy patch: prevent crash on some special URI-s. 2016-01-07 21:09:21 +01:00
Jeff George
1618e029a7 Relocate the wizard ad to the new resources.sabnzbd.org subdomain, also switch it to HTTPS 2016-01-07 20:49:33 +01:00
Safihre
cc34656ffa Update README
New link to Multi-threaded par2.
configobj and feedparser are already included in source so not a
dependency.
2016-01-07 20:43:24 +01:00
Safihre
e045533d2b Display Filejoin stage 2016-01-07 20:43:24 +01:00
Safihre
d8047eaa12 Update multiedit when job finishes
To keep the counter correct
2016-01-07 20:43:24 +01:00
Safihre
bcf5a75d14 Correctly handle UnicodeDecodeError except 2016-01-07 20:43:24 +01:00
Safihre
a2afe43540 Split Glitter into files
It was getting way too long for 1 file
2016-01-07 20:43:24 +01:00
Safihre
1c419c7151 Instant-load for Chrome
Trick was to place the JavaScript before any DOM, so it would load the
JS first
2016-01-07 20:43:24 +01:00
Safihre
610e10d18e Preload the queue and history JSON
This makes the first load intant(!) in Firefox and IE, but Chrome is not impressed.
Template rendering is only few msec slower and size only increases little.
2016-01-06 00:02:23 +01:00
Safihre
1e5d44ba9e Fix error in previous pagination improvement 2016-01-06 00:02:17 +01:00
shypike
5a60c3f345 Make sure the download report has a sensible order of stages. 2016-01-05 21:17:06 +01:00
Safihre
18bd3e925c Increase pagination speed for large lists
Just a little bit.
2016-01-02 12:52:49 +01:00
Safihre
35f2b14216 Don't break on localStorage problems
Now uses the check like Modernizr does, more robust against special
cases.
2016-01-02 12:52:49 +01:00
Safihre
faf90c36d8 Change buttons in Status-window
The buttons only fitted in English, in all other languages it always
wrapped around to second line.
2016-01-02 12:52:49 +01:00
Safihre
d17f339d9a Allow manual passwords by name/pw or name{{pw}} 2016-01-02 12:52:49 +01:00
Safihre
7682e593e3 Job-details window improvements
Now has the same color as the job's progress bar and fixed exotic bug
with identical filenames.
2016-01-02 12:52:49 +01:00
Safihre
9aa927159a History status info More-link failed
Implemented proper solution
2016-01-02 12:52:49 +01:00
Safihre
ea87c29ad4 Decrease history loading time
Performance impact is smaller than with the queue, but still helps.
2016-01-02 12:52:49 +01:00
Safihre
e94fa7e710 Use strict Javascript setting 2016-01-02 12:52:49 +01:00
Safihre
e9200d2d89 Stage-log texts of 3+ lines behind More button 2016-01-02 12:52:49 +01:00
Safihre
779560789c Adding password for pre-labeled jobs was faulty
It would make 'DUPLICATE / Title' into: 'DUPLICATE / DUPLICATE / Title'
2016-01-02 12:52:49 +01:00
Safihre
45eccebd52 Mobile/Tablet CSS fixes 2016-01-02 12:52:49 +01:00
Safihre
cbacac1934 Properly hide tooltips on job-prio/cat change
Otherwise they stay forever
2016-01-02 12:52:49 +01:00
Safihre
91736f069c 'None' in list of script wasn't translated 2016-01-02 12:52:49 +01:00
Safihre
e4bf3acd90 CSS improvements 2016-01-02 12:52:48 +01:00
Safihre
92f9bcb61e Make RSS favicon work with more domains
Did not work when indexers use for example api.nzbgeek.com, since
there's no favicon there, only at the root domain.
2016-01-02 12:52:48 +01:00
Safihre
8fab1aa00d Improve initialization
Hopefully reducing the number of DOM-updates
2016-01-02 12:52:48 +01:00
Safihre
caf0d3606a Proper CSS for button alignment 2016-01-02 12:52:48 +01:00
Safihre
b4f369b213 Reduce flickering of queue and history
On deleting of entire pages of jobs/items or toggle of showing failed
jobs in history
2016-01-02 12:52:48 +01:00
Safihre
bb0ee57e1c Refresh on deleting of warnings
Otherwise they don't disappear on long refresh-rates
2016-01-02 12:52:48 +01:00
Safihre
a83e89c8b4 Handle single delete when multi-editing 2016-01-02 12:52:48 +01:00
Safihre
e4554192f7 Re-implement the Check All
Previous implementation with the trigger click caused slowdown on (un)check-all and many API calls in certain scenarios.
2016-01-02 12:52:48 +01:00
Safihre
57498c3fb3 Use 'indeterminate' Check All state
I did not even know this indeterminate state existed until yesterday.
Nice solution to better visualization of the state
2016-01-02 12:52:48 +01:00
Safihre
ac3b9951e0 Fixing the RSS config page (again) 2016-01-02 12:52:48 +01:00
Safihre
3aae3e7e54 Reduce size of MomentJS 2016-01-02 12:52:48 +01:00
Safihre
f3252d8156 Decrease the little element shifts during sorting 2016-01-02 12:52:48 +01:00
Safihre
66450d2b2b Only hide actually finished files in job-modal 2016-01-02 12:52:48 +01:00
Safihre
14beeb712c Set RAR-verson reporting to debug
Since the rar-version reporting at unpack is also at Debug only
2016-01-02 12:52:47 +01:00
shypike
3a1de11e42 Fix Unicode issue in date/time localization. 2016-01-02 12:47:23 +01:00
shypike
2cea65f36b Fix README.mkd 2015-12-28 09:05:11 +01:00
shypike
ac72568e4d Update text files for 0.8.0Beta
Also remove CHANGELOG.txt
2015-12-24 15:26:05 +01:00
shypike
f28bcbf801 Make sure that the INI backup file has restricted access rights. 2015-12-24 15:01:21 +01:00
shypike
3d78329dd3 Update translations 2015-12-24 11:31:43 +01:00
shypike
b1f99990fc If the highest SSL protocol is selected for server, set it to empty.
Should a higher protocol become available in a later release,
it will be automatically selected.
2015-12-18 18:46:54 +01:00
Safihre
2f00a878a6 Log RAR-binary and RAR-file versions 2015-12-18 17:51:47 +01:00
shypike
583c233bd2 Disable "device" field for Pushover notifications.
Not implemented.
2015-12-18 17:23:00 +01:00
shypike
3b6e9775b0 Config->Notifications: refer to 0.8.0 Help page 2015-12-18 15:29:26 +01:00
shypike
f000212b4e Fix potential crash with Windows notifications. 2015-12-18 14:00:51 +01:00
shypike
68c66de77c Handle \" constructions in filenames coming from headers. 2015-12-17 22:35:31 +01:00
shypike
bd33a729ad Fix CherryPy's header parsing bug.
Due to a bug in its header parsing, uploaded files could not contain semicolons.
CherryPy bug: 1397
2015-12-17 22:34:46 +01:00
shypike
e96597b417 Correct potential sizing error in misc.sanitize_foldername(). 2015-12-17 20:39:47 +01:00
shypike
4d9eb56d01 Update main translation file. 2015-12-17 17:12:03 +01:00
shypike
289ccba8d3 "Resume" should also produce a notification. 2015-12-17 17:10:54 +01:00
Safihre
39516d61ea Keep dropdowns open properly 2015-12-17 16:55:57 +01:00
Safihre
da9f11a7c0 Thou shalt not delete processing/unpacking jobs
Glitter allowed deleting of jobs that weren't done from history, causing
problems!
Didn't know this was not allowed.
2015-12-17 16:55:57 +01:00
Safihre
f4388d2d8a Glitter fixes
Did not show free space correctly and did not show path for downloads
still processing.
2015-12-17 16:55:57 +01:00
Safihre
4fba40b624 RSS table fix 2015-12-17 16:55:57 +01:00
Safihre
503b965bb0 New Norwegian texts 2015-12-17 16:55:56 +01:00
Safihre
4ea9eaea54 Small English updates 2015-12-17 16:55:56 +01:00
Safihre
c3b108abf2 Improve Notifications config page 2015-12-16 21:57:59 +01:00
Safihre
164114ab82 Enable Windows Notifications 2015-12-15 11:35:26 +01:00
shypike
33f45a4256 Update translations 2015-12-12 12:31:14 +01:00
Safihre
51b160da3f Sorting only worked on first page 2015-12-11 18:27:43 +01:00
Safihre
d1c1819e16 Forgot the .png 2015-12-11 13:38:04 +01:00
shypike
3adff9ea6b Update translatable text source files. 2015-12-10 16:01:18 +01:00
Safihre
3496f93cce INFO message when cache not enabled
SAB needs this cache, otherwise CPU/mem usage gets out of hand. Note: on
Windows and Mac it is turned on by default already (200M), so only for
installs from source or linux users.
2015-12-10 12:18:22 +01:00
Safihre
dcac685cee Remove leftover texts of old Wizzard 2015-12-10 12:18:18 +01:00
Safihre
a0cdf79e23 Config fixes 2015-12-10 12:18:15 +01:00
Safihre
a54ccb1f71 Use proper M/G/T/P formatting for disksizes 2015-12-10 12:18:12 +01:00
Safihre
ebeb950794 Fix multiedit bug
Would force refresh for every item when using Check All
2015-12-10 12:18:06 +01:00
Safihre
3b58aff728 Glittery things - part 3
Possible fix for retry error, generalized links, remove debug-ID
2015-12-10 12:18:00 +01:00
Safihre
8ad5ff7b8c Display passworded jobs ignored labels put by Sab
Can't rebase because already so many changes..
2015-12-07 00:49:31 +01:00
Safihre
c210636952 Decrease Glitter startup for large queues by 40%
Turns out that the <select> in the options dropdown are a hugggeeee
slowdown on initial load! Only initializing on click cuts half the speed
(especially on more than 100 items)
2015-12-07 00:23:02 +01:00
Safihre
91895d8765 Glittery things - part 2
Fix sortable, remove ugly outlines on links for Firefox, click on
search-icon focus on search-box, mobile fixes
2015-12-06 13:00:25 +01:00
Safihre
90019494ce Allow multi-edit for grabbing jobs
Requested: #404
2015-12-05 19:27:40 +01:00
Safihre
9e99d39f2c Adding RSS button to main page
User request and seemed useful to me
2015-12-05 19:27:37 +01:00
Safihre
a70addca55 Adding RSS icon through SVG
Since Bootstrap doesn't come with RSS icon..
2015-12-05 19:27:34 +01:00
Safihre
d825b40b73 Stop retrying on auth (401) errors 2015-12-05 19:27:31 +01:00
Safihre
bc3d22a06b Click on status and completed time shows details 2015-12-05 19:26:00 +01:00
Safihre
ac1d1cf24d Make RSS errors more reasonably sized 2015-12-05 19:25:58 +01:00
Safihre
05c39b2de8 Purge history items on current page
So you can use the search to remove a specific set of history items
2015-12-05 19:25:55 +01:00
Safihre
298303a239 Updating Glitter - Night
Kind of forgot about it..
2015-12-05 19:25:52 +01:00
Safihre
46d1ba7037 Drag-and-drop not on accident 2015-12-05 19:25:49 +01:00
Safihre
9d5d1d90fa Update mobile-icons
Only necessary ones
2015-12-05 19:25:47 +01:00
Safihre
58f7d563c2 Glitter CSS name update 2015-12-05 19:25:44 +01:00
Safihre
83bd8cb3a0 Improve display/editing of passworded jobs
Did not use to work in all languages!
2015-12-05 19:25:41 +01:00
Safihre
c821b583ee Select full name when clicking edit button 2015-12-05 19:25:38 +01:00
Safihre
2898d045de Highlight disabled servers 2015-12-05 19:25:35 +01:00
Safihre
f9160d286d Glittery things 2015-12-05 19:25:32 +01:00
shypike
4e20004f7b Fixes in encoding.py 2015-12-05 17:15:29 +01:00
shypike
555aadc83e Windows: convert explicit INI-path to Unicode.
On Windows, the -f parameter passes an 8bit ASCII string instead of Unicode.
2015-12-05 16:07:56 +01:00
shypike
f1029836b0 Upgrade JSON problem logging to "info" level. 2015-12-05 13:51:46 +01:00
shypike
14319ad9f5 Remove debug statement from last commit. 2015-12-05 12:09:28 +01:00
shypike
c9a4e61a3a Automatically revert to safe (but slow) JSON-encoder when encountering encoding errors.
In some situations the standard JSON encoder bails out.
In such cases, switch to safe and slow JSON encoder (from 0.7.x) for the rest of the session.
Additional logging in json.py to find out what the source of the faulty strings is.
2015-12-05 10:22:43 +01:00
shypike
d8d261132c In Config->Servers, show active servers before inactive ones. 2015-12-03 21:08:42 +01:00
shypike
a043e78a9a Do not require presence of Python's webbrowser support.
On embedded systems, the module may be missing.
Handle this gracefully.
2015-12-03 20:56:28 +01:00
shypike
4b2533d8d9 Update translatable texts. 2015-11-30 21:47:13 +01:00
Safihre
32074b9be0 Drop support for IE8 in Glitter and Config
It's almost 2016..
2015-11-30 16:48:12 +01:00
Safihre
0aba54fcf3 Change menu-icon to actual menu icon 2015-11-30 16:48:08 +01:00
Safihre
22133e4a53 Colorize server priority labels 2015-11-30 16:48:04 +01:00
Safihre
aa698d0591 Make custom pause timer more clear 2015-11-30 16:48:01 +01:00
Safihre
1e4fb79349 Increase hit targets for checkboxes and dropdowns 2015-11-30 16:47:58 +01:00
Safihre
c01e091c52 Orphaned jobs with special chars couldn't be deleted 2015-11-30 16:47:55 +01:00
Safihre
b9205d98f6 Reload Servers page on name and priority change 2015-11-30 16:47:51 +01:00
Safihre
dccf961d79 Config improvements 2015-11-30 16:47:48 +01:00
Safihre
217fe143c3 Autocomplete everywhere and for relative paths 2015-11-29 11:01:03 +01:00
shypike
69a872eac0 Update unrar for OSX to 5.30
Closes #396
2015-11-26 11:33:42 +01:00
shypike
0b6395fc1d Update unrar for Windows and OSX to 5.30
Closes #396
2015-11-25 20:31:23 +01:00
Safihre
6e5909d283 Fix enable/disable server 2015-11-25 18:35:22 +01:00
Safihre
3b48ff7bab Add folderbrowser to Categories
And make it a proper-plugin. It now handles relative paths.
2015-11-25 14:31:10 +01:00
Safihre
038a6cca6b Updating Bootstrap 2015-11-25 09:07:02 +01:00
Safihre
102eaa966b Porting FolderBrowser and AutComplete to Bootstrap
Getting rid of 2x jQuery and jQueryUI that was present in Config
2015-11-25 08:51:07 +01:00
Safihre
6bb9f3ca94 Source code formatting of Config 2015-11-25 08:51:03 +01:00
Safihre
d37cecce09 Config fixes 2015-11-25 08:50:59 +01:00
shypike
83b582d503 Correct "Movie Sorting" text. 2015-11-24 22:00:54 +01:00
shypike
53bb66ffab Correct "Enable Generic Sorting" text. 2015-11-24 21:21:55 +01:00
shypike
1b6de48ac1 Update translations 2015-11-21 12:25:40 +01:00
shypike
fe3a0cc7f7 Update text files for 0.8.0Beta3 2015-11-21 12:25:06 +01:00
Safihre
2dd26027f7 Adding help-icons to config sections 2015-11-20 20:45:29 +01:00
Safihre
7e329e8196 Small fixes 2015-11-20 16:05:01 +01:00
shypike
8f1d81cc2c Repair setting speeds in OSX top menu.
limit_speed() call no longer accepts "int" input.
Closes #392
2015-11-19 21:28:54 +01:00
Safihre
e901f3bc35 Adding selected-counter to multi-edit 2015-11-19 15:51:22 +01:00
Safihre
8d85e2887d Added folder-browser to System Folders config
And update KnockoutJS to 3.4.0 stable.
2015-11-19 13:14:00 +01:00
Safihre
fdd8775564 Error in rating reporting 2015-11-18 13:41:06 +01:00
Safihre
3f174c86a8 Re-style the speedlimit input for Plush 2015-11-18 11:35:45 +01:00
Safihre
c80a897ea0 Percentage speedlimit correct in all skins
By @shypike
2015-11-17 08:29:08 +01:00
Safihre
b913a7be09 Filelist popup correct color for Checking downloads 2015-11-16 13:45:19 +01:00
Safihre
be17f051bf Correctly display speedlimit 2015-11-16 13:45:18 +01:00
shypike
1c388faf42 Fix option to enable duplicate checking from backup folder.
Fixed error introduced by 8a166729d4
Closes issue #390
2015-11-14 13:34:21 +01:00
Safihre
3e9188c81b CSS fixes 2015-11-09 17:14:51 +01:00
Safihre
244a7a4c27 Prevent flickering in History 2015-11-09 17:14:51 +01:00
shypike
4c19fcbbfa Undo change to translatable text. 2015-11-09 17:09:16 +01:00
Jonathon Saine
01fe99dbf2 code cleanup and spelling corrections 2015-11-08 14:11:26 -06:00
shypike
90b999b8e8 Fix errors in Danish translation. 2015-11-08 16:46:36 +01:00
shypike
55ede71b4d Update translations 2015-11-08 16:40:02 +01:00
shypike
0bd6cc08dd Update translations 2015-11-06 19:27:36 +01:00
shypike
2117964ce6 Update main POT file 2015-11-06 19:26:17 +01:00
shypike
97862a941c Update text files for 0.8.0Beta2 2015-11-06 19:21:16 +01:00
Safihre
7513f230ba Small Glitter fixes 2015-11-06 16:37:35 +01:00
Safihre
6b48b1806c Adding delete all orphaned in Python
Instead of sending x times a delete request, do it internally.
2015-11-06 16:37:35 +01:00
Safihre
adb07d8c44 Add schedule option to remove completed jobs
FR #118
2015-11-06 16:37:34 +01:00
shypike
8a166729d4 Add option to enable duplicate checking from backup folder. 2015-11-06 16:36:12 +01:00
Safihre
8f054add39 Updating RSS Config pages 2015-10-31 20:01:54 +01:00
shypike
a2ccc98228 One-time conversion of "speedlimit" schedules by appending "K" to speed.
The old definition was in 1K units, while we now use KMGT notation.
This calls for a one-time conversion of scheduled speeds.
2015-10-31 19:45:20 +01:00
shypike
72c37f0b7c Disable "device" field for PushBullet.
Will be fixed once we query for devices.
2015-10-31 18:44:02 +01:00
Safihre
2ee7e638d1 Fix for clear counters and delete server 2015-10-29 19:51:45 +01:00
Safihre
5addc56d36 Limit check of IPv4 IP when not needed 2015-10-29 13:18:32 +01:00
shypike
56a89cedc1 Make sure "permissions" always result in user-writable folders and files.
Required for "incomplete" folder and for renaming output files.
2015-10-28 16:46:32 +01:00
Safihre
1a594cbabd Fix confirm on enable/disable server button 2015-10-28 15:51:07 +01:00
Safihre
badb32e959 Make Config bit more uniform
We don't use this class anymore
2015-10-28 15:51:06 +01:00
Safihre
479cab1a83 Bugfix in Config save 2015-10-28 15:51:06 +01:00
shypike
d3d5f37d49 Prevent reserved Windows device names from ending up in file names. 2015-10-28 15:45:02 +01:00
shypike
7174e3fa51 Support older PyOpenSSL versions.
Older versions miss SSLEAY_VERSION.
2015-10-28 14:18:32 +01:00
shypike
dcaccad358 Show available SSL protocols for a Usenet server.
Determine available protocols.
Log information about SSL libraries.
2015-10-27 17:07:17 +01:00
Safihre
7aa5087976 Adding Post age as extra column option 2015-10-27 14:08:11 +01:00
Safihre
4936337e84 Streamlining error/warning display 2015-10-27 14:08:08 +01:00
Safihre
e968ff5617 Fix for Orphans with special char's (Chinese) 2015-10-26 16:55:16 +01:00
Safihre
98f826a88c Stop breaking when localStorage not available 2015-10-26 16:27:27 +01:00
Safihre
95673a7d51 Better date-time formatting 2015-10-26 16:27:24 +01:00
Safihre
204bc9758b Prevent zoom on tablets when opening modal 2015-10-25 10:51:20 +01:00
Safihre
911277e1d0 Adding a hide button to the feedback window 2015-10-25 10:51:18 +01:00
shypike
e96e64320a Series renaming wasn't working properly on Windows.
Renaming based on partially shortened Windows paths didn't work.
Restore original paths of unpacked files first.
2015-10-25 01:06:44 +02:00
shypike
2257a4e6f7 Correct quoting in Chinese email templates. 2015-10-24 19:43:51 +02:00
Safihre
7012fbf4b7 Config navbar did not fit for all languages 2015-10-23 22:17:01 +02:00
Safihre
6c05066c8d Uncheck check-all on delete or page switch 2015-10-23 22:16:56 +02:00
shypike
0cb3872fb5 Yet another patch in CherryPy 2015-10-23 20:11:50 +02:00
Safihre
237b35dbdb Removing bandwith question from Wizard 2015-10-23 20:04:12 +02:00
Safihre
77262f7479 Clear searchterm in queue/history by hitting escape
Small usability bonus.
2015-10-23 20:04:12 +02:00
Safihre
8c20776860 Making modals closable by pressing escape 2015-10-23 20:04:12 +02:00
Safihre
6356ddee67 Adding confirmation if not saving changes in Config 2015-10-23 20:04:12 +02:00
Safihre
f445c37ca8 autocomplete=off for any forms with passwords
It might stop some browsers of autofilling stuff they shouldn't. However
if the user chose to save a username/password all modern browsers ignore
autocomplete=off.

https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
2015-10-23 20:04:12 +02:00
Safihre
e52f73b0e6 Updating the wizard
Make everything more user-friendly and streamlined. Port and and # of
connections are now placed under 'Show more'. Clicking SSL will change
the port to 563.
Username and option for SSL removed from the wizard, they can be enabled
in the Config.
2015-10-23 20:04:12 +02:00
Safihre
d8b6f69074 File wasn't found for new users 2015-10-23 20:04:11 +02:00
Safihre
0d964dfeef Improving showing of special chars 2015-10-23 20:04:11 +02:00
Safihre
ff14b3953c Feedback button on the side for beta/alpha versions
It shows a list of ways to contact us. Hopefully we get some more
feedback this way.
The button only shows for beta/alpha/Github versions and so I did not
add extra translations since these users know what they are getting in
to.
2015-10-23 20:04:11 +02:00
Safihre
75cedebc8d Visual fixes 2015-10-23 20:04:11 +02:00
Safihre
0b56ce29c0 Check all should only check visible items 2015-10-23 20:04:11 +02:00
shypike
453c7dd3cb PushBullet settings not handled properly in UI
Booleans coming from HTML forms need special treatment.
Closes #372
2015-10-23 15:10:53 +02:00
SanderJ
8e90325942 SSL: cleaning up 2015-10-21 20:56:55 +02:00
SanderJ
25070cc857 Print SSL system info in sabnzbd.log: try/except added 2015-10-21 20:56:55 +02:00
SanderJ
35781bfdc8 Print SSL system info in sabnzbd.log 2015-10-21 20:56:55 +02:00
shypike
758cac6336 Another patch of CherryPy.
Prevent crash when encountering a pathless URI.
2015-10-21 20:53:51 +02:00
shypike
c38cecb240 Further limit "incomplete" paths on Windows.
Limit base path to 40.
Limit job folder path to 70.

Closes #368
2015-10-16 22:39:10 +02:00
shypike
cf1d57e6e0 Override IPv6 test when Special ipv6_test_host is empty.
Assume proper working of IPv6.
Closes #367
2015-10-15 10:20:46 +02:00
shypike
396449664b Update translations 2015-10-14 22:37:47 +02:00
shypike
6fcb656292 Prevent crash when reporting overlong Windows download-folder path. 2015-10-14 21:43:18 +02:00
shypike
71d41bf0df Fix problem with duplicate checking.
Fixes commit 74e5f19252
2015-10-11 17:11:50 +02:00
shypike
b27280c752 Remove noofslots_total from History.
Variable is for queue, not history.
2015-10-10 18:02:09 +02:00
shypike
51db79d970 Update main POT file. 2015-10-10 17:35:22 +02:00
shypike
62c066e3f8 Fix error in handling ZIP files retrieved from an indexer. 2015-10-10 17:12:30 +02:00
shypike
8fcb5cf1c1 Update translations 2015-10-10 16:50:37 +02:00
shypike
ed091dd43e Server priority should ignore blocked servers. 2015-10-10 15:29:48 +02:00
shypike
5c29852033 Replace --no-api-log command line switch with the "special" option "api_logging".
More elegant code and automatically persistent.
2015-10-10 12:10:08 +02:00
Jonathon Saine
65f8fa7b5d Removed addID from api, changed reference in Plush to use addurl instead. Tested and ensured plush still worked fine. 2015-10-10 11:56:30 +02:00
shypike
74e5f19252 Series duplicate action and normal duplicate action were not independent. 2015-10-10 11:54:34 +02:00
Jonathon Saine
811e93e926 Fix dupe check logic, re-order so md5sum is used first (quickier / less prone for false positives). Since the same NZB release can actually be different depending on how the indexer obtained it (provided from user, vs indexed from usenet manually), added some debug logging. 2015-10-10 00:29:58 -05:00
shypike
4ce3c89928 Current post-processing job was shown as an orphan.
Side effect of an unneeded change in commit 6eedd99 "Redesign of retry for failed URL fetches".
2015-10-09 20:38:40 +02:00
Safihre
b6d4d7ab92 When searching, go to page 1 of results 2015-10-08 22:27:32 +02:00
Safihre
43abe40579 Added text to Plush speedlimit 2015-10-08 22:27:30 +02:00
Safihre
5857a60ceb Use localStorage smarter in Knockout 2015-10-08 22:27:29 +02:00
Safihre
c038395d17 Option to disable confirmation on removal
For Queue (queue items & warnings) and History (history-items and
orphaned jobs)
2015-10-08 22:27:27 +02:00
Safihre
655d623040 Adding more date formats 2015-10-08 22:27:25 +02:00
Safihre
1809a5a1fc Less translations 2015-10-08 22:27:23 +02:00
Safihre
43f9350491 Fix the enable button in Server-config 2015-10-08 22:27:21 +02:00
Safihre
08d4cb3204 Fixing tooltips for new Bootstrap 2015-10-08 22:27:20 +02:00
Jonathon Saine
6fb4d7ece3 More pep8 cleanup, (revert) unused variables by import / duplicate imports / misspelling / some minor renames to avoid using built-in names (list/set/id/etc). Will cleanup addID in another pull. 2015-10-08 22:04:20 +02:00
shypike
e840b8251b Queue size not influenced by filtering. Add total number of queue slots.
The "to do" size of the queue is not reduced by any search criteria.
Add template variable noofslots_total to reflect the total number of un-paused items.
2015-10-08 22:01:00 +02:00
shypike
466bbf2694 Corrections in duplicate checking.
Fix incorrect duplicate decision logic.
Remove "fetch" entry after duplicate job has been ignored.
2015-10-05 21:35:50 +02:00
shypike
3293a8b91d Sort servers in Status view too.
And correct an error in the Config->Servers sort.
2015-10-05 20:40:02 +02:00
shypike
6b547b35ae Fix access issue with folder browser.
Folder browsers should use TAPI calls instead of API calls.
Would fail when a username/password is set, while API-key disabled.
Solves #353
2015-10-05 09:40:33 +02:00
Safihre
48520c1015 Making the logo a link to reload the page 2015-10-04 20:42:39 +02:00
Safihre
147f909495 Adding optional column for extra information 2015-10-04 20:42:39 +02:00
Safihre
a97e7c86f1 Adding tabs to the Status and interface options 2015-10-04 20:42:39 +02:00
shypike
cfed5f8978 Fix issues with speed limiter. 2015-10-04 20:37:39 +02:00
shypike
37f8114d3f Accept both percentage and absolute speedlimit in Scheduler.
Also accept KMG notation for absolute limits.
2015-10-03 22:45:37 +02:00
shypike
189dca1e76 Sort servers on priority and then on display name. 2015-10-03 19:40:26 +02:00
Safihre
4f5ba027ba Resolving scrollbar issue 2015-10-03 16:27:11 +02:00
Safihre
460c51896b Updating bootstrap
However, not updating the Glyphicons because the new ones are not as
sharp as the old ones. Especially on Windows.
2015-10-03 16:14:54 +02:00
Safihre
3acfdc3e56 Adding search function to the queue 2015-10-02 14:04:06 +02:00
Safihre
f739d86112 Limit upload file types 2015-10-02 13:44:23 +02:00
Safihre
275f8d7ff8 Bugfix for long names and 10 video/audio ratings 2015-10-01 15:46:05 +02:00
Safihre
23bf17135f Adding server-priority to main server-view 2015-10-01 13:05:08 +02:00
Safihre
282b3c00c5 Removing obsolete template variables
They are really not used anymore in the interface.
2015-10-01 02:07:36 +02:00
shypike
4e5badf832 Add absolute speed limit to output of API-calls "qstatus" and "queue".
Add parameter "speedlimit_abs" to API-call "queue".
Add parameters "speedlimit" and "speedlimit_abs" to API-call "qstatus".
2015-09-30 22:20:06 +02:00
shypike
80326e3898 Remove https/ipv6 work-around for Python 2.5 on Windows.
We no longer support Python 2.5
2015-09-30 21:23:49 +02:00
Jonathon Saine
f75cb44a24 moved regex define before use (save memory when code branch isnt ran), calculate expire time once (save cpu cycles), reserved word (only some) & undefined & unused variables, depreciated has_key, more spelling and docstring cleanup 2015-09-30 21:03:21 +02:00
Safihre
da300e0676 Removing last bits of Mobile template 2015-09-30 16:52:24 +02:00
shypike
dd3ec6ab7a Merge pull request #348 from shypike/happy-eyeballs
Happyeyeballs improvements.
2015-09-30 08:42:25 +02:00
sanderjo
b883875025 Happyeyeballs improvements. 2015-09-29 21:41:53 +02:00
shypike
47c33467ee Merge pull request #346 from thezoggy/develop--pep8_pt3
More pep8 non-agressive fixes.
2015-09-28 21:46:37 +02:00
Jonathon Saine
cc0b15eb46 More pep8 non-agressive fixes. 2015-09-28 12:58:30 -05:00
shypike
31f60a5395 Fix typo in interface.py 2015-09-28 16:44:13 +02:00
shypike
ed3633c8d7 Merge pull request #343 from Safihre/develop
One-time notice of new skin
2015-09-28 16:24:53 +02:00
Safihre
036d7cd8a0 One-time notice of new skin
Only for existing users
2015-09-28 16:09:37 +02:00
shypike
86a7c0a593 Merge pull request #342 from Safihre/develop
Fixes and adding option for custom pausing
2015-09-28 13:37:54 +02:00
Safihre
75b7602e0c Adding an option for custom pause duration 2015-09-28 12:19:43 +02:00
shypike
511a96fbb4 Merge pull request #341 from thezoggy/develop--pep8_pt2
Develop  pep8 pt2
2015-09-28 10:03:13 +02:00
shypike
1d49f8380b Update skin & color table
Remove mobile and add Glitter.
2015-09-28 09:47:21 +02:00
Safihre
d8573e6bd5 Fixing Unblock server button 2015-09-28 09:26:42 +02:00
Safihre
9bf3c68702 Fixing warnings on queue > HD space 2015-09-28 08:42:02 +02:00
Jonathon Saine
40afd2e4d1 More PEP8 non-agressive whitespace cleanup. 2015-09-27 11:05:49 -05:00
Jonathon Saine
c015ac2656 Remove legacy nzbsrus logic, pep8 cleanup (non-agressive) 2015-09-27 10:57:31 -05:00
shypike
28c805ffde Update main POT file. 2015-09-25 21:36:44 +02:00
shypike
3c4b09af75 Make more error and warning strings translatable. 2015-09-25 21:35:47 +02:00
shypike
b6aff4272f Don't send a notification when reloading a pre-checked job. 2015-09-25 21:16:04 +02:00
shypike
8567fd71a2 Merge pull request #337 from thezoggy/develop--pep8
sab 0.8.x -- dev pep8
2015-09-25 20:24:09 +02:00
shypike
449111ccf8 Merge pull request #338 from Safihre/develop
Adding dark mode to Glitter
2015-09-25 20:22:37 +02:00
Safihre
b6681d4b67 Adding dark mode to Glitter 2015-09-24 18:51:07 +02:00
Jonathon Saine
565f5fd762 Some pep8 cleanup (whitespace/docstring only), focus on root dir along with scripts/tools/util. 2015-09-23 08:47:18 -05:00
shypike
bcf5343617 Merge pull request #332 from Safihre/develop
Interfaces fixes part 5
2015-09-20 19:59:20 +02:00
Safihre
026bab8249 Adding clear button to search field 2015-09-19 23:06:01 +02:00
Safihre
745c8b904e CSS improvements 2015-09-19 23:06:00 +02:00
Safihre
200b293135 Showing 'Default' instead of * 2015-09-19 23:05:58 +02:00
shypike
1ba588f530 Text files for 0.8.0Beta1 2015-09-17 19:08:15 +02:00
shypike
bcbe15e8c9 Update translations 2015-09-17 18:54:22 +02:00
shypike
04198f6606 Merge pull request #331 from sanderjo/geiranger-sab-better-test_ipv6
Geiranger commit: better test_ipv6()
2015-09-16 10:13:46 +02:00
sanderjo
26712d85bb Geiranger commit: better test_ipv6() 2015-09-15 19:18:09 +02:00
shypike
687aab7f90 Windows: prevent temp paths from ending with a period
The misc.trim_win_path() function failed to remove trailing spaces and periods.
2015-09-14 11:40:26 +02:00
shypike
c6dece737f Move sample scripts to a separate folder. 2015-09-11 21:16:04 +02:00
shypike
d3fb46433b Merge pull request #325 from Safihre/develop
Interface fixes part 4
2015-09-11 20:32:23 +02:00
Safihre
fa49a26ca6 Fixing load-balancing setting in Config 2015-09-11 18:28:38 +02:00
Safihre
b10344545b Style fixes for Config 2015-09-11 16:17:42 +02:00
shypike
f7e69b3603 Fix typo in commit 7bb3dd39d4 2015-09-11 07:15:01 +02:00
shypike
2f88a2085e Update translations 2015-09-10 21:06:49 +02:00
shypike
6737937f93 Merge pull request #322 from Safihre/develop
Interface fixes part 3
2015-09-10 21:06:00 +02:00
shypike
7bb3dd39d4 Fix first script parameter when running on Windows.
Bug in Python's path library on Windows.
os.path.normpath() does not convert long-path notation "\\?\d:\tv\bla\bla\.." to "\\?\d:\tv\bla".
Instead use os.path.abspath() in tvsort's function move_to_parent_folder().
2015-09-10 21:01:28 +02:00
Safihre
378ed873b7 CSS fixes for Glitter 2015-09-10 16:45:10 +02:00
Safihre
63fd88a04f Adding back text that should not have been removed
Oops, my fault!
2015-09-10 13:44:46 +02:00
Safihre
97343a9318 Auto-scroll to Multi-edit form
In case the queue is so long the MultiEdit is out of view, it will
scroll to it.
2015-09-10 13:44:44 +02:00
Safihre
858e19fffd Fixing amount left in title 2015-09-09 11:49:34 +02:00
Safihre
dd4fe01036 Moving sort button to top of queue 2015-09-09 11:42:57 +02:00
shypike
355ae2d60d Merge pull request #321 from Safihre/develop
Interface fixes part 2
2015-09-08 17:12:31 +02:00
Safihre
7f94f7856b Adding age on queue hover 2015-09-08 14:32:02 +02:00
Safihre
3618182483 Fixing freeze after sorting 2015-09-08 10:27:05 +02:00
Safihre
5d72e2a208 Pagination fix, human-readable queue size left 2015-09-08 10:27:01 +02:00
Safihre
7718af966d Fancy file button in retry dialog of Glitter 2015-09-08 10:26:57 +02:00
Safihre
7f1a007136 Correctly display long server-names in Config 2015-09-08 09:36:28 +02:00
shypike
986eb039a2 Fix IPv6 test.
Scope issue prevented correct result.
Solves #319
2015-09-07 23:35:09 +02:00
shypike
bde88cba7a Merge pull request #317 from Safihre/develop
Interface fixes
2015-09-07 14:29:16 +02:00
shypike
74168869cf Make the host used for testing IPv6 user configurable. 2015-09-07 13:49:41 +02:00
Safihre
302ecbe7fd Adding icon when a Special is different from default 2015-09-07 11:48:12 +02:00
Safihre
bb2bb5374a Show indicator of number of errors on top
This in case of very long queue the errors might be hidden
2015-09-07 10:33:25 +02:00
Safihre
18b5900226 Removing Info sign from history-info
Overlapped too much with the text
2015-09-07 10:33:22 +02:00
Safihre
ec1ea2e51d Update script log modal 2015-09-07 10:33:19 +02:00
Safihre
38ddef5cd9 Hide notifications options that cannot be used anyway 2015-09-07 10:33:15 +02:00
Safihre
70716e47be Adding more tooltips 2015-09-07 10:33:11 +02:00
shypike
891100343d Make error detection in urlgrabber more flexible.
Different platform have different exceptions for incorrect SSL certificates.
2015-09-07 10:03:15 +02:00
shypike
42cad7309a Make https certificate verification the default. 2015-09-07 09:26:01 +02:00
shypike
d19c65ae8f Return correct disk usage even if destination folder doesn't exist yet.
This prevents error messages in the UI.
2015-09-07 09:25:11 +02:00
shypike
ba6376f453 Update translations 2015-09-07 08:50:23 +02:00
shypike
940e1a1d69 Update main POT file. 2015-09-07 08:50:05 +02:00
shypike
9c51acdc0b Merge pull request #309 from Safihre/develop-skinny
Cleaning skin texts
2015-09-07 08:45:31 +02:00
shypike
75da736f64 Merge pull request #314 from Safihre/develop
Update Mime-type to Gzip also fonts
2015-09-06 13:21:24 +02:00
Safihre
c05459d71c Config Server Toggle now has label
For @thezoggy
2015-09-06 00:09:42 +02:00
Safihre
13b97203fc Update Mime-type to Gzip also fonts 2015-09-06 00:05:26 +02:00
Safihre
bb01be4af5 Fix for language not being saved on Back in Wizard 2015-09-05 23:25:25 +02:00
Safihre
6c9b074cd4 Adding translations to wizard page 1 2015-09-05 23:25:24 +02:00
Safihre
6c98332b78 Cleaning skin texts 2015-09-05 23:25:22 +02:00
shypike
adb95c365e Add --no-api-log to prevent a polluted log during testing.
Will not log API calls.
2015-09-05 15:29:31 +02:00
shypike
72547abe06 Merge pull request #311 from sanderjo/develop
A 404's redirect now keeps the FQDN in place
2015-09-05 10:17:15 +02:00
shypike
ed27256d44 Fix logging of call to user script.
Also fix an incorrect comment.
2015-09-05 10:11:15 +02:00
sanderjo
2c461ce2ed sabnzbd.BROWSER_URL still needed 2015-09-04 21:32:57 +02:00
sanderjo
5ed9f6f30c A 404's redirect now keeps the FQDN in place 2015-09-04 19:10:01 +02:00
Safihre
1c5b9c650f Comment update IPv6 2015-09-04 10:26:01 +02:00
shypike
62e80940a0 Merge pull request #308 from Safihre/develop
IPv6 testhost and HappyEyeballs
2015-09-03 22:47:07 +02:00
Safihre
dbac3a463d Making HappyEyeballs default 2015-09-03 17:12:09 +02:00
Safihre
2d671883e8 Changing IPv6 testhost to test-ipv6.sabnzbd.org 2015-09-03 17:11:57 +02:00
shypike
f224b265d4 Merge pull request #307 from Safihre/develop
Improve CPU usage on Firefox
2015-09-03 12:27:36 +02:00
Safihre
02b655ae50 Improve CPU usage on Firefox
Firefox uses very high CPU for anything animated.
Disables animations on progress-bar and make the History-'processing' a
block animation
Can be removed if it's performance gets better in the future..
2015-09-03 12:17:54 +02:00
shypike
1fae29f125 Merge pull request #306 from Safihre/develop
Glitter improvements.
2015-09-03 10:20:31 +02:00
Safihre
aeeb2e9421 Adding host:port text to SMTP in Config 2015-09-03 10:07:03 +02:00
Safihre
7d73799f5b Remove Json.py license 2015-09-03 09:46:09 +02:00
Safihre
c19fb43f80 Better handling of removed orphaned jobs in GUI of Glitter 2015-09-03 09:46:05 +02:00
shypike
d0409ea706 Update main POT file. 2015-09-01 21:32:41 +02:00
shypike
4314787283 Merge pull request #305 from Safihre/develop-plushfix
Plush fix for reporting
2015-09-01 08:09:14 +02:00
shypike
1d1eddabea Merge pull request #300 from Safihre/develop-newage
Removing Mobile template
2015-08-31 22:54:42 +02:00
Safihre
b4bbb27c55 Plush fix for reporting 2015-08-31 13:58:02 +02:00
shypike
ce55e0f4b2 Merge pull request #304 from Safihre/develop-plushfix
Fix for hover problems in Plush
2015-08-31 13:28:08 +02:00
Safihre
c0c2037a52 Fix for hover problems in Plush 2015-08-31 11:16:19 +02:00
shypike
76bdef4573 Merge pull request #303 from Safihre/develop
Adding translations to dashboard in Plush + bugfix
2015-08-30 22:40:27 +02:00
Safihre
7956e9f5d1 Updating Knockout 2015-08-30 11:26:57 +02:00
Safihre
693f79f09c Adding translations to Dashboard in Plush 2015-08-30 11:13:33 +02:00
Safihre
d81c389904 Bugfix for sortable in Glitter 2015-08-30 10:10:32 +02:00
shypike
6cc9e8e661 Fix crash error in logging
Fix problem introduced in commit 5f1c9cc21a
2015-08-28 14:15:56 +02:00
shypike
4d19f756cb Merge pull request #299 from Safihre/develop
Adding animations to Glitter
2015-08-28 08:53:02 +02:00
shypike
4cd99e0446 Update main POT file. 2015-08-27 22:13:34 +02:00
Safihre
13ec1cd2aa Changing dashboard text to 'Local IPv6'
Different thing!
2015-08-27 21:08:28 +02:00
Safihre
c0a8feb324 Remove empty directories of Classic/Mobile template 2015-08-27 20:41:37 +02:00
Safihre
34d4776f30 Removing Mobile template
Glitterrified!
2015-08-27 20:41:32 +02:00
Safihre
b54af908b1 Adding animations to Glitter
A little more fun!
2015-08-27 20:27:52 +02:00
shypike
13cee2dd99 Merge pull request #297 from Safihre/develop
Fix for toggle server in config
2015-08-26 22:03:57 +02:00
Safihre
afbfe809ad Fix for toggle server in config 2015-08-26 08:44:46 +02:00
shypike
a703873652 Improve logging of failed urlgrabber attempts. 2015-08-25 21:47:40 +02:00
shypike
5f1c9cc21a Log unpack passwords that are read from a file. 2015-08-25 21:24:21 +02:00
shypike
2df2bfff3e Merge pull request #296 from sanderjo/happy-eyeballs-gui
GUI for Happy Eyeballs
2015-08-25 20:47:10 +02:00
sanderjo
091756aa31 GUI for Happy Eyeballs 2015-08-25 20:10:51 +02:00
shypike
ac89838369 Improve error messages when using incorrect login date for Usenet server.
Replace cryptic server message with readable text.
2015-08-25 20:04:59 +02:00
shypike
a339ef53d3 Merge pull request #295 from Safihre/develop
Updating KnockoutJS to latest version
2015-08-25 18:42:14 +02:00
Safihre
97fd225eaf Updating KnockoutJS to latest version
Performance improvements for Firefox and Chrome.
2015-08-25 15:23:04 +02:00
shypike
77537d2dc7 Merge pull request #294 from jcfp/patch-1
README.md: fix command for bg process
2015-08-25 13:19:03 +02:00
jcfp
6ecdcedc35 README.md: fix command for bg process
re: https://forums.sabnzbd.org/viewtopic.php?f=11&t=19403
2015-08-25 12:51:47 +02:00
shypike
f3f3842545 Merge pull request #290 from Safihre/develop-unite
Removing skin-specific config pages
2015-08-25 12:03:06 +02:00
shypike
0841ebcdfc Merge pull request #288 from Safihre/develop
Adding info message for orphaned folders and updates.Fixing clear-speedlimit and storage/path in history.
2015-08-25 11:54:05 +02:00
Safihre
504097027f Adding info message for orphaned folders and updates
If more than 3 orphaned folders are found, display a message. Check this
every other day.
2015-08-24 11:45:30 +02:00
Safihre
f07897a34c Fixing clear-speedlimit and storage/path in history
Clear speedlimit icon was only clickable on Chrome. History only needed
path, not storage.
2015-08-22 20:43:58 +02:00
Safihre
f4b8524b76 Removing skin-specific config pages
All should use Uni-config.
2015-08-21 13:41:36 +02:00
shypike
6c2ce3be64 Merge pull request #284 from Safihre/develop
Adding missing options to Glitter
2015-08-18 22:13:30 +02:00
Safihre
91f4f47df4 Adding missing options to Glitter
Options that were in Plush but not yet in Glitter
2015-08-16 22:01:46 +02:00
shypike
48d83974f9 Merge pull request #281 from Safihre/develop
Adding toggle Show All/Show Failed to Glitter History. Setting CherryPy default favicon.
2015-08-16 17:15:24 +02:00
Safihre
ee39ec8139 Setting default favicon for CherryPy to SABnzbd 2015-08-16 10:11:00 +02:00
Safihre
a71bc99bc0 Adding toggle Show All/Show Failed to Glitter History. 2015-08-15 19:07:18 +02:00
shypike
e331a57303 API-call "history" now accepts one or more "category" parameters in order to filter.
api?mode=history&category=TV&category=*
This will select for category "TV" AND "Default"
2015-08-15 15:10:41 +02:00
shypike
b8889756d4 API-call "addurl" now returns a list of nzo_id's, which will be valid for the actual jobs.
The nzo_id of a "future" entry will now be re-used for the actual job.
Rename api.addid to api.addurl because API-call addid is deprecated.
2015-08-15 10:54:20 +02:00
shypike
c823de5d91 Fix selection of wrong category when the default category of an RSS stream has been removed from categories. 2015-08-14 23:42:59 +02:00
shypike
4ec8b47cf1 Improve error handling in url grabber. No retries on unresolvable errors. 2015-08-14 23:42:48 +02:00
shypike
e1c91ab001 Don't return a work_path for future jobs, because there is none. 2015-08-14 23:42:37 +02:00
shypike
6eedd99deb Redesign of retry for failed URL fetches.
Remove the clumsy embedded link in the error message.
Instead use the Retry for normal jobs.
Use the obsolete "report" field in the database to store the type of the job, "future" for failed fetches.
2015-08-14 23:42:26 +02:00
shypike
87949e25b3 Another patch for CherryPy 3.8.0
This error keeps coming back.
https://bitbucket.org/cherrypy/cherrypy/issues/1296/attributeerror-module-object-has-no
2015-08-14 21:06:01 +02:00
shypike
c0dcc4df12 Merge pull request #280 from Safihre/develop
Integrating icon font Woff as Base64 into CSS for Config and Glitter. Adding file-size to Job-details popup in Glitter.
2015-08-14 20:04:57 +02:00
Safihre
d9d27df77d Adding file-size of each file to job-popup in Glitter
Filenames get truncated in the center for longer names.
2015-08-14 15:07:40 +02:00
Safihre
72b1f31f44 Integrating icon font Woff as Base64 into CSS for Config and Glitter
This fixes any problems that Chrome has with fonts on auth/https
connections that don't have valid certificates. Both Firefox and Chrome
use Woff font's, so only for IE it will have to download 20kb extra of
the CSS while for FF/Chrome it will lower the number of requests by 1.
2015-08-14 11:28:20 +02:00
shypike
05d2f9f2f9 Fix URL for version retrieval. 2015-08-13 21:36:06 +02:00
shypike
7c9c0aa52e Improve check_version() and clean code
Remove unused functions.
Move bad_fetch() from misc to url grabber.
Move OrderedDict port from misc to its own file.
Improve check_version().
2015-08-13 21:26:14 +02:00
shypike
edf39f8e7d Bump required CherryPy to 3..8.0
Avoid CP settings that will become obsolete.
2015-08-13 19:49:57 +02:00
shypike
a7c38434d7 Patch CherryPy to avoid Unicode bugs in PyOpenSSL 0.14
On some systems this resulted in a crash.
This was patched before in the previous CherryPy in
commit d1a87c4564
2015-08-12 22:38:25 +02:00
shypike
30be2c447a Add patch file for CherryPy 3.8.0 2015-08-12 22:24:06 +02:00
shypike
da9e39367d Patch CherryPy to support 301 redirection.
Needed to support the broken Bonjour/ZeroConfig protocol that
only allows an HTTP address to set, even for a HTTPS-only server.
2015-08-12 22:24:06 +02:00
shypike
1889ecd6fb Add official CherryPy 3.8.0 release. 2015-08-12 22:24:06 +02:00
shypike
23815b6a8e Allow all CherryPy releases, starting with 3.2.2 2015-08-12 22:24:06 +02:00
shypike
3c5fe11299 Update main POT file. 2015-08-12 22:20:51 +02:00
shypike
3c442966c4 Replace own JSON module by the standard library. 2015-08-12 21:52:47 +02:00
shypike
c589b23708 Merge pull request #275 from Safihre/develop
Opt-out for global options in Glitter. RSS config page fixup. Config restart SAB fixup. Fixes for mobile/tablet.
2015-08-12 14:54:47 +02:00
Safihre
be96b84e7b Improving Glitter and Config for mobile/tablet 2015-08-12 14:30:43 +02:00
shypike
826173cf5f Merge pull request #278 from sanderjo/one-traceback-less
less traceback logging in cases like ipv6-only server on ipv4 connection
2015-08-10 20:50:37 +02:00
Safihre
c601168d07 Config restart update
Config restart now shows a splash screen. On the General-tab choosing
'Save settings' will now ask if you also want to Restart SAB as well.
2015-08-10 12:57:17 +02:00
Safihre
1cc6e323b9 RSS Config page fixup 2015-08-10 12:57:15 +02:00
Safihre
cf8ddb6e7c Making global interface settings optional in Glitter 2015-08-10 12:57:13 +02:00
SanderJ
f940d61c52 less traceback logging in cases like ipv6-only server on ipv4 connection
https://github.com/sabnzbd/sabnzbd/issues/277
2015-08-09 16:52:40 +02:00
shypike
a706820ee7 Merge pull request #270 from Safihre/develop
Saving Glitter refresh-rate and page-limits in general config. Adding a central speedhistory. Adding icons to Config.
2015-08-07 20:38:18 +02:00
shypike
755deb94d9 Update unrar to release 5.21 2015-08-06 20:42:07 +02:00
Safihre
29cbc6e10b Adding central speedhistory
Adding a central speed history within SAB, a feature many download
programs have and I think SAB could use! This way you can actually see
what happened when the browser was not open.
When there is no download-activity nothing really happens, it only calls
and updates (in case 0's need to be added) when the main.tmpl file is
called. In Glitter this is only once.
It does add consecutive numbers to a list, but this should not be a
memory hog.
2015-08-06 20:39:27 +02:00
shypike
ccf7060932 Merge pull request #271 from sanderjo/HappyEyeballs
Implement HappyEyeballs
2015-08-06 19:51:21 +02:00
sanderjo
4571ea414a Implement HappyEyeballs 2015-08-05 20:00:40 +02:00
Safihre
b1bb6d2a10 Adding icons to Config
TODO: RSS-details page
2015-08-05 10:41:19 +02:00
Safihre
20b98e95f5 Saving refresh-rate and page-limits in general config
refresh_rate and history_limit were already in the config but not used.
Added queue_limit.
This way these settings are the same no matter what device or browser
you use to connect to your SAB system.
2015-08-05 10:41:15 +02:00
shypike
bfa06e79c5 Windows: fix Unicode crash when encountering odd names in par2 file sets.
On Windows, file names coming from console output of par2.exe needs to be converted to Unicode.
2015-08-04 23:09:52 +02:00
shypike
f9ab3f193d Merge pull request #267 from Safihre/develop
Added 'Browse' to translation, graphic fixes. Replacing tabs by spaces in Glitter.
2015-08-04 20:45:53 +02:00
Safihre
6f7adb8e29 Replacing tabs by spaces in HTML/CSS of Glitter 2015-08-03 13:52:14 +02:00
Safihre
f0cc967c0c Config - Added 'Browse' to translation, graphic fixes
Both in Glitter (Add-NZB) and in Config added the word 'Browse' so that
is now also in Config multi-languages.
2015-08-02 19:22:17 +02:00
shypike
b48db17f37 Update main POT file. 2015-08-01 21:25:36 +02:00
shypike
76b9971d06 Merge pull request #266 from Safihre/develop
Customized Add-NZB in Glitter, added extra tooltips
2015-08-01 21:17:11 +02:00
Safihre
3564fc3c5c Customized Add-NZB in Glitter, added extra tooltips 2015-08-01 16:06:42 +02:00
shypike
b9bc0cf344 Decrease size of preamble in Config->Special. 2015-08-01 15:20:18 +02:00
shypike
1b4e30bcee Update main POT file. 2015-08-01 15:12:44 +02:00
shypike
c8e11549d1 Some touch-ups of Config. 2015-08-01 15:11:45 +02:00
shypike
993733cc8b Remove tab characters. 2015-08-01 15:01:11 +02:00
shypike
19ff1db3c4 Merge pull request #260 from Safihre/develop-newconfig
Redesigning the Config look.
2015-08-01 14:30:57 +02:00
Safihre
ac7607e952 Redesigning the Config 2015-08-01 14:08:08 +02:00
shypike
10d345f9c4 Enable renaming of Usenet servers.
- Renaming by showing a separate display name
- Backwards compatible: no damage to schedules and server counters
- Add multi-line field for personal server notes
2015-08-01 10:43:33 +02:00
shypike
d56c19ae1d The 301 redirection message should use the IP of the caller.
301 should not use "localhost" but the actual IP of the calling client.
Needed to support Bonjour/Zerconfig.
2015-07-31 20:53:39 +02:00
shypike
d1a631f801 Fix broken Speedlimit in Scheduler. 2015-07-30 21:09:00 +02:00
shypike
bd953c5e9f Update main POT file. 2015-07-29 19:48:14 +02:00
shypike
87dd4f1975 Fix inconsistent casing in scheduler events. 2015-07-28 22:28:01 +02:00
shypike
639114dbb5 Add English pseudo translation PO files.
Enables correction of English texts without invalidating existing translations.
These files are not maintained in the translation service, but are edited directly.
2015-07-28 19:11:53 +02:00
shypike
3a8b01e5c1 Plush main page, Queue menu: move "Retry All Failed" before Sort.
Prevents fold-out of Sort from obscuring "Retry All Failed".
2015-07-28 19:06:39 +02:00
shypike
7642d2a1e4 Fix Dutch translation. 2015-07-28 19:01:58 +02:00
shypike
68b44a7310 Drop support for Python 2.5
Also for SABHelper.py
2015-07-28 08:30:27 +02:00
shypike
17130c5208 Merge pull request #256 from Safihre/develop
Adding changing of loglevel in Glitter
2015-07-27 23:30:32 +02:00
shypike
3b0ed33598 Merge pull request #254 from thezoggy/develop--smpl_white-navigation
smpl - remove trailing | in nvaigation
2015-07-27 23:26:40 +02:00
shypike
0cb68f6f81 Merge pull request #252 from thezoggy/develop--minor
Minor fixes/spelling mistakes.
2015-07-27 23:26:19 +02:00
shypike
b2a44c66f1 Fix typo in Config->Notifications
Incorrect text type file.
2015-07-27 16:13:54 +02:00
Jonathon Saine
f2a332d3f9 Removing trailing | from navigation. 2015-07-26 03:02:34 -05:00
Jonathon Saine
8dd55b8d81 Minor fixes/spelling mistakes. 2015-07-25 21:42:53 -05:00
shypike
fe5da44602 Don't send a test notification when required data is missing. 2015-07-25 19:02:43 +02:00
Safihre
284dc10cc5 Adding changing of loglevel in Glitter 2015-07-25 17:58:16 +02:00
shypike
19d97a3c3a When auto-launching the Wizard, use the "/sabnzbd/wizard" URL.
Better compatible when configured behind another HTTP server.
2015-07-25 14:34:23 +02:00
shypike
500906e802 Update main POT file 2015-07-25 14:09:16 +02:00
shypike
7d162c2111 Add Pushbullet support. 2015-07-25 14:08:14 +02:00
shypike
f12011468d Implement Pushover support.
Users have to register their own Application key.
2015-07-24 18:04:23 +02:00
shypike
16b358bdb5 Merge pull request #248 from Safihre/develop
Updating setup wizard and Glitter upgrades
2015-07-24 11:19:53 +02:00
Safihre
2921f0825d Adding NZB-name option, minor fixes 2015-07-24 09:21:20 +02:00
Safihre
822a6103bb Delete All for Orphaned jobs in Glitter
In status-window
2015-07-23 11:06:15 +02:00
shypike
05dc4e4177 Remove potential XSS vulnerability in the Status-QueueRepair when folder names contain "<", ">" and "&".
Status did not XML-ify names.
2015-07-23 09:13:30 +02:00
shypike
496c0fd928 Remove potential XSS vulnerability in the History when folder names contain "<", ">" and "&".
Unlike build_queue, build_history did not XML-ify names.
Patch required for Plush.js, due to the way the download-report pop-up title is created.
2015-07-23 09:07:54 +02:00
Safihre
0e62433af1 Updating Wizzard
Updating Wizzard to be proper HTML and look like Glitter
2015-07-22 13:05:29 +02:00
Safihre
cc41c4881b Bugfixes to Glitter 2
Adding the right HTML-language attribute and fixing for big screens.
2015-07-22 13:05:26 +02:00
Safihre
26564b55e4 Minor bug fixes in Glitter 2015-07-22 13:05:24 +02:00
Safihre
11d1729a33 Make Glitter the default skin for new users.
Also supports config-less skins by setting the standard "Config"-skin.
2015-07-20 21:13:06 +02:00
Safihre
574f176615 Adding OZnzb to Glitter
Changed OZnzb so it will allow changing of up/down voting.
For out of retention it will only list the user's servers, not all of them.
In Glitter you can only give a audio/video score once (just like on the website).
Plush allows changing of audio/video rating and shows that change, but it is not actually changed on the website.
2015-07-19 22:08:05 +02:00
shypike
bf46ba035c Support the use of an unpatched release of CherryPy.
Use the '"redirect_url" patch only when actually present.
Only needed to support https on zeroconfig/Bonjour systems.
2015-07-19 16:57:34 +02:00
shypike
729b368b56 Remove obsolete references to indexers. 2015-07-19 16:37:15 +02:00
shypike
548f448b9d Add support for processing .nzb.bz2 files.
Support the BZIP2 format.
2015-07-18 13:00:28 +02:00
shypike
915a444a3a Get rid of non-standard indenting.
Tabs, yuk!
2015-07-18 12:37:34 +02:00
oznzb
c7ac896125 Rating functionality
Port rating functionality from 0.7.x to develop.
2015-07-15 19:49:46 +02:00
shypike
f238c8bb15 Prevent crash in zero-config/Bonjour service due toi unexpected errors. 2015-07-14 21:48:15 +02:00
shypike
d90ca686c1 Improve logging of API-calls "addlocalfile" and "addurl". 2015-07-13 21:22:50 +02:00
shypike
a7264c92d1 Fix change that disabled password recognition in the Watched folder.
Fixes side-effect of "Setting a password in Retry dialog did not work"
2015-07-13 20:58:16 +02:00
shypike
0566510b07 Prevent crash when URL grab runs into failing server. 2015-07-08 17:37:16 +02:00
shypike
41869e115c Merge pull request #240 from Safihre/develop
Fixing the favicons in all templates
2015-07-08 17:14:10 +02:00
Safihre
ac26de4578 Fixing the favicons in all templates 2015-07-06 17:56:58 +02:00
shypike
78a9c8512b Setting a password in Retry dialog did not work.
Race-condition prevented password from being added to NZO in time.
Instead set password before adding job to the queue.
2015-07-05 16:23:20 +02:00
shypike
c4618bbafb Fix skintext.py
.lower() construction used by Glitter, is not supported by translation tools.
2015-07-04 14:46:43 +02:00
shypike
3b72d5a8cf Update Dutch translation. 2015-07-04 14:45:44 +02:00
shypike
e75aad12b1 Update translations 2015-07-04 14:29:57 +02:00
shypike
a6eb72abb1 Update Dutch translation. 2015-07-04 14:29:38 +02:00
shypike
d1b49d10f1 Update text files for 0.8.0Alpha3 2015-07-04 14:06:10 +02:00
shypike
9e555cb3dd Merge pull request #236 from Safihre/develop
Updating Glitter for fixed retry in API and new icons
2015-07-04 13:03:47 +02:00
Safihre
cd2ba68f4f Fixing Favicon 16x16 in all templates 2015-07-04 12:54:08 +02:00
shypike
1994e0535e On Windows, mass-purging failed jobs from History left some files behind.
Cause: path check did not take long-path notation into account.
Solves issue #227
2015-07-04 12:39:49 +02:00
Safihre
36f2e2037f Merge remote-tracking branch 'sabnzbd/develop' into develop 2015-07-04 12:08:51 +02:00
shypike
4577390775 Update main POT file. 2015-07-04 12:01:23 +02:00
Safihre
f99e768572 Adding the icons of @MasterRoot24 to Glitter
Also changed browserconfig, should include a . in the path.
2015-07-04 11:52:30 +02:00
shypike
ec866c0daa Always show server statistics for a job, also when only one server was used. 2015-07-04 11:49:03 +02:00
shypike
5b93657e83 Remove left-over "fillserver" fields in original Plush and smpl Config->Server pages. 2015-07-04 11:30:13 +02:00
shypike
acee0a30d7 Merge pull request #228 from cdheiser/develop
Add support for restricting servers by category
2015-07-04 11:23:12 +02:00
Safihre
a8fec539ba Merge remote-tracking branch 'sabnzbd/develop' into develop 2015-07-04 11:04:05 +02:00
Safihre
47a2a44f5a Fixing retry option now it's fixed in API 2015-07-04 11:02:00 +02:00
shypike
4011dba8d5 Merge pull request #233 from sanderjo/CheckForReceptionOf-ip-address
Dashboard: if we get anything else than a plain IPv4 address, return None
2015-07-04 10:06:19 +02:00
shypike
a2c1b47a8f Fix crash when unrar is missing on Linux/Unix systems.
Solves issue #234
2015-07-04 10:02:12 +02:00
shypike
a1040a144d On Windows, failed jobs removed from History left some files behind.
Cause: path check did not take long-path notation into account.
Solves issue #230
2015-07-04 09:52:36 +02:00
cdheiser
7d790a37a0 Issue #225: Add support for restricting servers by category.
- Config support updated to save categories by server.  Any server without categories defaults to the Default category.
- Downloader now checks categories before selecting a server to use.
  - Servers with the Default category are always eligible.
  - Otherwise, only servers with a matching cagegory are eligible.
  - Servers without categories are not used (this can happen if you delete the last category for a server).
  - Properly skip servers with higher priority if they have no matching category
- Upgrades from previous versions should retain the expected behavior that all servers are eligible until configured otherwise.
2015-07-03 15:05:42 -07:00
sanderjo
501fdb9dd7 if we get anything else than a plain IPv4 address, return None 2015-07-03 23:33:15 +02:00
shypike
d689cb234f Fix the table with the pattern key for Series Sorting.
The table mixed up the Season functions for original and case-adjusted titles.
2015-07-03 23:02:36 +02:00
shypike
f6f923e060 Make sure OSX/Unix and Windows read each others INI files.
The resulting INI file is converted to the native format.
2015-07-03 22:06:52 +02:00
shypike
db5cc40b87 Remove double unicode conversion in server test error messages.
CherryPy can handle Unicode without xml_name() conversion.
2015-07-03 21:17:36 +02:00
shypike
f190d27ff4 Merge pull request #175 from MasterRoot24/icons
Update favicon, apple-touch-icon and mstiles
2015-07-03 20:40:35 +02:00
shypike
296042a491 Make the speed boxes and logo center again in Plush top bar. 2015-07-03 20:34:42 +02:00
shypike
bd3cba4dc4 One time conversion of a "fillserver" to priority 1.
Prevents all servers from getting priority 0 when upgrading to 0.8.0
Also fix explanation text.
2015-07-03 20:28:00 +02:00
shypike
823431b705 Merge pull request #226 from Safihre/develop
Adding Glitter
2015-07-02 21:35:44 +02:00
shypike
f4dcd0ef3b No longer recognize "something-s.avi" constructions as samples. 2015-07-02 21:18:47 +02:00
shypike
1f65507b39 On Windows UNC paths were not handled correctly.
Trimming UNC paths went wrong.
2015-07-02 20:59:49 +02:00
shypike
2f4cebf37a On Windows, failed jobs didn't show the "Retry" button.
Cause: path comparison didn't account for long-path notation.
(Thank you, Safihre)
2015-07-02 20:18:58 +02:00
shypike
5d2ea32217 Try to fall back to 7z if 7za is missing
Only for systems other than Windows and OSX.
2015-07-02 15:06:43 +02:00
Safihre
4c1e63f4ce Bugfixes 2015-07-01 22:17:45 +02:00
shypike
34670d0523 Merge pull request #229 from sanderjo/sab-Dashboard
Sab dashboard: Dashboard cleaning up
2015-06-30 20:23:01 +02:00
Safihre
473492cd31 Added queue sorting and more bugfixes 2015-06-28 23:55:23 +02:00
Safihre
8bc13447a7 Feature and bug update
See Forum!
2015-06-25 23:43:06 +02:00
Safihre
04089dd159 Revert "TEST"
This reverts commit 9902d11770.
2015-06-24 21:28:04 +02:00
Safihre
9902d11770 TEST 2015-06-24 21:27:38 +02:00
Safihre
0c3bc8b75d Add multi-lang 2015-06-24 21:18:10 +02:00
sanderjo
248bfbde99 Dashboard cleaning up 2015-06-23 20:23:00 +02:00
Safihre
dfd3573e99 Phone/tablet fixed, retry function, status window 2015-06-14 22:25:04 +02:00
shypike
f0bac2d1b2 Correct typo 2015-06-12 08:36:01 +02:00
Safihre
78312b00f5 Adding licensing 2015-06-11 16:13:11 +02:00
Safihre
0864fd9a6f Adding Glitter 2015-06-11 15:40:27 +02:00
shypike
6f644bf850 Update text files for 0.8.0Alpha2 2015-06-06 11:46:34 +02:00
shypike
9ed35bd259 Update French translation. 2015-06-06 11:45:51 +02:00
shypike
aba448dcad Update translations 2015-06-06 11:37:24 +02:00
shypike
9b4102da42 Send username/password as 8bit ASCII to Usenet servers to avoid Unicode bug in PyOpenSSL 0.14 2015-06-06 11:13:17 +02:00
shypike
d1a87c4564 Patch CherryPy to avoid Unicode bugs in PyOpenSSL 0.14 2015-06-06 10:30:05 +02:00
shypike
623c2706b7 Scheduling speedlimit=0 (which means disable) didn't evaluate properly. 2015-06-06 10:06:07 +02:00
shypike
b24ec3e2b7 Remove long-path notation from Windows paths before using in errors/warnings. 2015-06-04 23:22:05 +02:00
shypike
aeea78bb07 Add "action_size" parameter to file bulk operations.
To support drag-and-drop of the NZO page.
2015-06-04 23:01:34 +02:00
sanderjo
75f2d23ef2 Dashboard enhancements, among which Pystone 2015-05-29 23:59:57 +02:00
shypike
5b022d0409 Fix Config->Switches to show availability of unrar, unzip and 7zip.
Also, remove warning for missing 7zip binary.
2015-05-28 21:38:52 +02:00
shypike
e7807119ec Merge pull request #218 from sanderjo/REVfiles
Handle Rev-files in the correct way and add Dashboard.
2015-05-28 21:36:45 +02:00
sanderjo
d14d6e4ed1 Handle REV files in the correct way 2015-05-25 19:17:47 +02:00
SanderJ
797dda1e5c More stuff in the Dashboard, among which: diskspeed measuring 2015-05-21 19:48:26 +02:00
SanderJ
0d8c3420ad Creation of "Dashboard" in Plus Status tab 2015-05-19 21:01:40 +02:00
SanderJ
9dbf41d8f2 Move IP address determination into a module sabnzbd.utils.getipaddress 2015-05-19 19:42:48 +02:00
shypike
351028cb06 Remove temporary debug aid. 2015-05-19 18:40:52 +02:00
shypike
fc53885b2e Experimental server priority.
Core code changes donated by user Fmstrat.
The current implementation is bit rough as the calculation of alternative servers isn't done at the right level in the download/decoding chain.
This needs a redesign, although it works.
2015-05-16 16:41:14 +02:00
shypike
145afc44f5 Fix UI for notifications.
"Notification Center", "Growl" and "NTFOSD" should have boolean options instead of numerical.
Otherwise the user interface will not properly handle disabled options.
2015-05-14 19:13:36 +02:00
shypike
70d1c917f0 encoding.platform_encode() should only try to convert "str" types. 2015-05-14 19:11:26 +02:00
shypike
1d8a912acb New version of par2-tbb for Windows.
Supports repair for files over 2G.
2015-05-07 20:41:48 +02:00
shypike
2caf0ee1a0 Update translations 2015-05-05 20:11:19 +02:00
shypike
ff858fad48 Log the output of the pre-queue script. 2015-05-05 20:04:23 +02:00
shypike
30ee3a9840 Fix location of 7Zip by OSX binary. 2015-04-25 20:59:29 +02:00
shypike
1925097deb Point to new Wiki pages for RSS and Scheduling. 2015-04-25 20:29:22 +02:00
shypike
f5a8946d95 Update source texts. 2015-04-25 11:42:38 +02:00
shypike
ef3fb69c03 Correct language base strings.
Avoid complex formatting in translatable texts.
Remove obsolete text.
2015-04-25 11:40:49 +02:00
shypike
49d0a8bd32 Update language source files. 2015-04-25 11:14:43 +02:00
shypike
830c04fc60 Move option "script_can_fail" from Specials to Switches. 2015-04-25 11:13:34 +02:00
shypike
601db26f62 Require Python 2.6 or 2.7 2015-04-24 19:57:53 +02:00
shypike
2421bd4da6 Update translations 2015-04-24 19:31:42 +02:00
shypike
82c35d208b Update translations 2015-04-22 22:15:04 +02:00
shypike
175ed12a30 Fix translation issues. 2015-04-22 21:58:37 +02:00
shypike
9862a06f94 On Unix systems not all files were sent to the par2 tool.
As a side-effect of changes in globber() supporting Windows long paths, its behaviour changed.
The code in PAR_Verify wasn't changed accordingly.
2015-04-22 21:39:37 +02:00
shypike
587749437e After pre-check completes successfully, prevent the nzb.gz file from being deleted. 2015-04-19 18:37:08 +02:00
shypike
86f7c4b54d Make sure unpack problems result in a "failed" job status.
Incorrect rerun of failed unpacking job, masked the error status of the first one.
Don't rerun if the previous run fails.
2015-04-19 16:28:25 +02:00
shypike
687ab5d4f5 Update text files and copyright year. 2015-04-11 19:16:17 +02:00
shypike
07a16811c4 Update translations 2015-04-11 18:52:34 +02:00
shypike
d5d90765a9 Update language source file. 2015-04-03 23:59:55 +02:00
shypike
c9871c224e Improve Python version check. 2015-04-03 23:50:38 +02:00
shypike
f120265acf Make IP checking more anonymous by using unassigned addresses.
Also remove unused procedure.
2015-04-01 22:44:37 +02:00
shypike
970dbf3408 Make sure folder options always contain the right slashes for the platform. 2015-04-01 21:44:07 +02:00
shypike
c17ba7cdc3 Replace os.rename with shutil.move.
This should prevent cross-device rename errors.
2015-04-01 21:17:08 +02:00
shypike
c2329a8a9f Point several Config pages to a new Wiki page instance. 2015-04-01 20:46:11 +02:00
shypike
f92c6e1c1f Improve unrar recommendation text 2015-03-28 14:36:40 +01:00
shypike
50447f736f Correct floating point conversion 2015-03-28 11:44:08 +01:00
shypike
7ab83c0629 Log an error message when RSS feed uses bad certificate. 2015-03-26 21:12:37 +01:00
shypike
b95aa735ac Point Special-Help to a new Wiki page instance. 2015-03-26 21:09:42 +01:00
shypike
27ff8da056 Add Special to enable/disable HTTPS certificate verification.
Also: improve error messages.
2015-03-25 21:42:41 +01:00
shypike
a60bbff449 Report bad server certificates for URL-based NZB fetch
Prepare for Python's 2.7.9+ certificate verification.
Previous code did not give clear information.
2015-03-21 18:13:30 +01:00
shypike
fb97ca8cdc Correct translation errors. 2015-03-21 17:51:21 +01:00
shypike
759d601ec4 Prevent crash when the API-call "addfile" sends a file name without a proper extension. 2015-03-20 21:24:01 +01:00
shypike
48fd9d53ab Improve logging of unrar version. 2015-03-13 20:46:59 +01:00
shypike
b319ca964b Improve unrar check by recommending a minimum version.
Only for non-OSX and non-Windows.
2015-03-12 22:27:06 +01:00
shypike
20108c885e Update translations 2015-03-03 22:23:15 +01:00
shypike
3339ffc7f1 Update translation source file. 2015-02-27 21:01:57 +01:00
shypike
6840a2ffa0 Remove "do not download samples" option.
It doesn't work when samples are part of the par2 set.
2015-02-20 22:04:19 +01:00
shypike
64ca554746 Remove "fail_on_crc" option and move "overwrite_files" from Special to Switches. 2015-02-20 22:00:27 +01:00
shypike
eb761ed95b Prevent CherryPy error when trying to delete an non-existing server. 2015-02-16 20:36:12 +01:00
shypike
9cc20356af When "enable_multicore" is off, verification may fail on Linux systems.
Make sure "par2-classic" variable is always set, so that enable_multicore's value doesn't matter.
2015-02-16 13:04:18 +01:00
shypike
64bf6c5cf1 Add "password" field to the "Retry from History" function.
This way you can retry a job with a different password.
2015-02-12 21:37:16 +01:00
shypike
b4232ff589 Merge pull request #199 from sanderjo/develop
Determine and show public IPv4 address (if +Debug is on)
2015-02-09 22:22:33 +01:00
sanderjo
787a4b04f5 Determine and show publick IPv4 address (if +Debug is on) 2015-02-09 13:08:48 +01:00
shypike
7883bb6bbd Remove obsolete build files. 2015-02-04 21:11:48 +01:00
shypike
c148b990fc Default base folder for Windows is now the %USERPROFILE% folder.
Before, %USERPROFILE%\Documents was used.
The default download folders should be based on %USERPROFILE%\Downloads.
The other folders will land in %USERPROFILE%.
Also removed the obsolete option to have the INI file in the program folder. The latter now needs an explicit -f parameter.
2015-01-30 21:16:08 +01:00
shypike
236fbb9595 Update par2-tbb for Windows to version 0.4 2015-01-24 15:49:40 +01:00
shypike
ffbcc46fa4 Add 64bit only par2-sl for OSX. 2015-01-24 15:44:20 +01:00
shypike
2da41b3e51 Enable multicore choice for OSX too.
Also improve the code for par2 choice and option handling.
2015-01-24 15:11:41 +01:00
shypike
00e257fe74 Remove obsolete build support files. 2015-01-24 14:02:01 +01:00
shypike
677607d9f1 Default host is now 127.0.0.1
"localhost" gives too many problems on too many systems.
2015-01-19 20:49:55 +01:00
shypike
5731bc2ab0 Rename "local_range" to "local_ranges" to avoid breaking 0.7.x. compatibility.
Also remove default range, not needed.
2015-01-19 17:13:25 +01:00
shypike
9edb809773 Adjust Windows 8bit character mapping to work in a Unicode app. 2015-01-17 19:19:07 +01:00
shypike
f0c75504d7 Prevent crash when encountering unpack issues. 2015-01-17 18:37:58 +01:00
shypike
0f93545773 Prevent crash on systems without SSL support. 2015-01-17 18:36:41 +01:00
shypike
41ede96538 When no local range is set, allow all access. 2015-01-16 17:40:28 +01:00
shypike
7fc84ba6d0 Do not require the script folder to be writable. 2015-01-13 23:07:00 +01:00
shypike
74d3cfff20 Prevent crash when a duplicate file is detected.
Undefined function name.
2015-01-13 21:22:53 +01:00
shypike
956bd6374f Fix little problem in extension detection and fix some PyLint complaints. 2015-01-13 21:13:03 +01:00
shypike
0395dac2eb Windows long-style paths should not be damaged by trimming and cleaning. 2015-01-11 19:25:16 +01:00
shypike
03c9a3330e Make sure Windows long-style paths are not sent to par2. 2015-01-11 19:24:35 +01:00
shypike
a0536c8fed Add "Retry All" function.
Sends all failed jobs back to the queue.
2015-01-11 15:54:55 +01:00
shypike
4db46f9c62 Prevent CherryPy warning by making sure host name is in ASCII instead of Unicode. 2015-01-11 13:37:39 +01:00
shypike
3ea0bbcae5 API calls change_opts, change_script, change_cat now return "false" in case of errors.
Previously they always returned "true".
2015-01-11 13:28:59 +01:00
shypike
c1a11e26c1 Moved pystone module to sabnzbd/utils 2015-01-11 12:55:04 +01:00
sanderjo
6233ce1109 moved pystone module to sabnzbd/utils 2015-01-04 08:53:12 +01:00
sanderjo
c4feba200d Better handling if import of pystone module does not work 2015-01-02 12:25:35 +01:00
sanderjo
fef680a399 Update SABnzbd.py 2015-01-01 22:52:34 +01:00
sanderjo
76f40af514 Better handling if import of pystone module does not work 2015-01-01 22:43:22 +01:00
shypike
b7236af9e2 Log exception info when urlgrabber's URL retrieval fails. 2015-01-01 21:14:53 +01:00
shypike
8eed05d221 Correct logging of refused API calls. 2014-12-30 11:51:38 +01:00
shypike
b7294a69f1 Add "Read all RSS feeds" to system tray menus. 2014-12-22 19:54:28 +01:00
shypike
7a070fdc4a Support extra meta fields in NZB file.
What is now supported as X-DNZB-YYY header is now also accepted as meta field YYY.
'failure', 'details', 'episode', 'propername', 'year'.
2014-12-20 18:25:56 +01:00
hansvqp
678f9c7374 Add System Tray menu for Linux systems with GTK support. 2014-12-20 13:33:20 +01:00
shypike
aaa2105eaa Make notification behavior consistent with the way the UI shows the options.
Also correct the "startup" defaults for ncenter, ntfosd and growl.
2014-12-20 12:50:39 +01:00
shypike
32c15771b4 Correct error in Notification Center options. 2014-12-20 12:31:58 +01:00
shypike
5019681d81 Reduce risk of crashing on bad pystone library. 2014-12-20 11:50:26 +01:00
shypike
80bc55f8c7 Support of a list for "local_range" option. 2014-12-10 20:53:37 +01:00
shypike
2b209e8a1f Detect major OSX versions.
Affects defaults for Growl and NotificationCenter before and after MountainLion (8).
Affects top menu icon on Yosemite (10) and higher.
2014-12-08 21:41:25 +01:00
shypike
9079c05b97 Implement "send_group" and "ssl_type" per server instead of global. 2014-12-03 20:13:51 +01:00
shypike
5bed2e6b68 Enable TLS V1 for SSL server connections.
Default to value "t1" (TLS V1), except when obsolete option ssl_type was set to "v2".
2014-12-01 20:52:32 +01:00
shypike
bb6eeacaac Move some options from Specials to Switches and vv.
Also add some missing options to smpl and Plush.
2014-12-01 14:26:15 +01:00
shypike
96c72fa4c5 Show the "enable_meta" option in Config->Switches.
And remove from "Specials".
2014-12-01 12:32:27 +01:00
shypike
d9e2868a7b Remove encoding.titler()
Work-around no longer needed for Unicode.
2014-11-22 19:17:04 +01:00
shypike
e184f730e2 Remove traces of 8bit ASCII support.
Remove or replace calls to latin1().
Replace all Ta() calls with T().
2014-11-22 19:10:08 +01:00
shypike
f30059e07a Additional logging for folder renaming. 2014-11-22 13:51:54 +01:00
shypike
b0146a393a Update some license files. 2014-11-22 13:44:00 +01:00
shypike
d2e69fa005 Update unrar to 5.11 for (Snow)Leopard on Intel, but not for PPC.
There's no unrar 5.* release for PPC.
2014-11-22 13:43:28 +01:00
shypike
30f022d933 Measure and log Pystone performance, and - if possible - CPU type 2014-11-22 13:23:31 +01:00
shypike
0d6606a4a7 Let the API-call "Retry" return the new nzo_id of the job.
Also change the status of a doomed re-fetch from "Failed" to "Fetching".
2014-11-22 13:20:50 +01:00
shypike
50fe8baeef Update copyright year. 2014-11-22 13:20:31 +01:00
shypike
7e74169113 Added an issue and made some language corrections. 2014-11-22 13:18:02 +01:00
shypike
c7735a5be7 OSX Yosemite: make top-menu icon compatible with "Dark Mode". 2014-11-22 13:17:34 +01:00
shypike
88f0b6d9ac Fix problem of testing email server with existing parameters.
In existing email parameters the password consists only of asterisks.
In that case, get the password from storage.
Also improve the logging of failed authentication attempts.
2014-11-22 13:17:05 +01:00
shypike
4dc58c8229 Update unrar for OSX to 5.11 2014-11-22 13:16:30 +01:00
shypike
a139f7dc49 Support double quotes to delineate parameters in category match lists.
"a b", "c d"
is now properly handled. The double quotes needed HTML-quoting.
2014-11-22 13:13:14 +01:00
shypike
3a80fed408 Correct spelling of OSX release names. 2014-11-22 13:12:55 +01:00
shypike
5d2cb81604 When sanitizing names, preserve "." and ".." elements in paths.
"sanitize_foldername()" was too eager to remove "." characters, thus removing "." and "..".
When using a single folder, the final "." element would be replaced with "unknown".
2014-11-22 13:12:37 +01:00
shypike
d44c2265ff The after-unrar-check needs to take the "flat_unpack" option into account. 2014-11-22 13:12:15 +01:00
shypike
59a1256d5b When a comma is present in a file name, quotes are needed when passed to a user script.
The replacement list2cmdline() should handle commas.
2014-11-22 13:11:50 +01:00
shypike
f3507b5355 Update OSX DMG image.
Make MountainLion/Mavericks/Yosemite the default choice.
Separate folders for SnowLeopard and Lion.
2014-11-22 13:11:25 +01:00
shypike
6675ef85df Update OSX signing.
Lion build can only be signed on Lion.
ML/Mav build must be signed on Mav.
2014-11-22 13:10:58 +01:00
shypike
f9ef3e73fd Fix problem of a job's destination path getting damaged on Windows.
"D:\folder\map" would become "D:folder\map", giving nasty side-effects.
2014-11-22 13:10:32 +01:00
shypike
fdb64576eb Change renaming of duplicate files from file.ext-->file.ext.1 to file.ext-->file.1.ext
Works better when encountering multiple rar/par sets with identically named files.
2014-11-22 13:10:01 +01:00
shypike
064d97a190 Make OSX MountainLion build compatible with Mavericks. 2014-11-22 13:09:21 +01:00
shypike
01306fb8ac Do not remove a leading dot in a path element.
"/folder/.hidden/" must be preserved.
"/folder/hidden./" must be converted to "/folder/hidden/"
2014-11-22 13:06:31 +01:00
shypike
7eebbdeff7 Windows UNC paths, used as final destination, were damaged.
misc.sanitize_and_trim_path() did not handle UNC paths properly.
2014-11-22 13:06:11 +01:00
shypike
0413d3075a Update OSX signing method 2014-11-22 13:03:51 +01:00
shypike
0fe1637ef9 Treat RAR CRC errors like "incorrect password"
Older versions of unrar report wrong passwords as CRC errors.
Therefor, try the next password (if available) when a CRC error is reported.
2014-11-22 13:03:23 +01:00
Johannes 'fish' Ziemke
3cfa03d30f Add Dockerfile
Usage:

        docker build -t sabnzbd .
        docker run -p 127.0.0.1:8080:8080 sabnzbd
2014-11-22 12:59:01 +01:00
sanderjo
d7154c2743 Extra logging in case of Loading .../.sabnzbd/admin/Rating.sab failed
Will print the exact except error message in the error message.
2014-11-22 12:58:19 +01:00
shypike
fee13a1fb2 OSX Signing is now only possible on OSX Mavericks, so check this. 2014-11-22 12:57:50 +01:00
shypike
18f9037816 Prevent folder trimming from removing embedded passwords in filenames.
Reduce the number of calls to sanitize_foldername() and rely on NzbObject to do its work.
Also allow "oversized" incomplete folders to be sent back to the queue, by not sanitizing/trimming again.
2014-11-22 12:57:15 +01:00
shypike
663b90d664 Fix potential problem with timestamps in RSS. 2014-11-22 12:53:04 +01:00
shypike
d7463ff607 Make sure the final destination path is always sanitized and trimmed.
Titles coming from RSS and processed by Sorting escaped sanitation.
2014-11-22 12:52:43 +01:00
shypike
9442823b38 When matching SFV files with RAR-sets, do this case-insensitive. 2014-11-22 12:48:47 +01:00
shypike
6d88b84e8a Small code improvement "unwanted extensions". 2014-11-22 12:47:55 +01:00
sanderjo
0e8d41ef4d cfg.unwanted_extensions() is a list so check must be cfg.unwanted_extensions() != []
Signed-off-by: sanderjo <sander.jonkers+github@gmail.com>
2014-11-22 12:47:18 +01:00
sanderjo
d58c1e43c3 Put the last rar immediately the first rar, so that unwanted extensions will get detected earlier.
Signed-off-by: sanderjo <sander.jonkers+github@gmail.com>
2014-11-22 12:46:43 +01:00
shypike
867bb9bf8e Limit article cache to 1G to prevent a memory size bug in the _yenc module. 2014-11-22 12:46:17 +01:00
shypike
7e6e5e12b7 Upgrade unrar to version 5.11 (Windows) 2014-11-22 12:45:56 +01:00
shypike
3be0670d3d Sort order of RSS feeds incorrect due to UI using wrong time field. 2014-11-22 12:43:27 +01:00
shypike
7b1f798586 Prevent further pauses by "unwanted extension", once the user has resumed the job after the first stop.
Part of this code was accidentally included in commit 081010d50b
This is the missing part.
2014-11-22 12:42:50 +01:00
shypike
45a8ec6306 Change renaming of duplicate files from file.ext-->file.ext.1 to file.ext-->file.1.ext
Works better when encountering multiple rar/par sets with identically named files.
2014-11-22 12:42:27 +01:00
shypike
bf2c23e79a Fix for "Range" selection of queue.
Not a really good fix.
The code looks identical to the code for selecting files within an NZB (in config.js) , but it has some subtle error.
This is a fix which will work reasonably well.
Should be fixed properly later.
2014-11-22 12:38:55 +01:00
shypike
5635b20b90 Prevent crash when Windows SysTray function hits PyWin bug. 2014-11-22 12:38:09 +01:00
shypike
be7706d508 Sort queue on now visible name instead of original name. 2014-11-22 12:37:43 +01:00
shypike
d16404ab61 Remove special URL handling for nzbclub indexer, no longer needed. 2014-11-22 12:24:58 +01:00
shypike
3972e438ad Improve "unwanted extension" text. 2014-11-22 12:24:26 +01:00
shypike
c9697a3f22 Allow "nzbname" parameter with just the password (like "/password).
Works also in AddNZB dialog.
2014-11-22 12:14:25 +01:00
shypike
0d14ac7763 In debug logging mode, use Google to determine our own IP address (IPv4 and IPv6).
Helpful for diagnosing troublesome setups.
2014-11-22 12:14:01 +01:00
shypike
7b8e09613d Log more info about failure to remove item from History. 2014-11-22 12:05:10 +01:00
oopoa
5d13e8fa7d Add api for server usage statistics. 2014-11-22 12:04:23 +01:00
sanderjo
15e7cd9d6c Logs in which rar file unwanted extension 2014-11-22 11:41:13 +01:00
shypike
218bf66391 Don't pass seemingly "joinable" files to par2. No longer needed since we use a wildcard.
Can lead to problems on Windows due to a potentially huge amount of parameters.
No longer needed since we're using the wild-card parameter nowadays.
2014-11-22 11:40:31 +01:00
shypike
6d5b095b2b Fix bug that prevented multiple sets in one NZB from joining. 2014-11-22 11:39:47 +01:00
sanderjo
2c18d49efa Handle server side 5xx problems 2014-11-22 11:38:10 +01:00
Der-Jan
1488758155 Set pwd to none when empty 2014-11-22 11:37:30 +01:00
JessThrysoee
d8ca1ed4f4 Use default-path in Plush cookies.
Cookies are not port specific. When sending cookies with path=/ to
http://localhost:8090/sabnzbd/ these cookies also turn up when
requesting e.g. http://localhost/apache2 or http://localhost:8080/tomcat and so on.

To avoid polluting the global cookie space, simply do not specify a path
when setting the cookie. This will result in a cookie with the default-path,
i.e. path=/sabnzbd for http://localhost:8090/sabnzbd.
2014-11-22 11:36:51 +01:00
shypike
60b8fbfc7b Prevent false encryption messages.
Probably not encrypted when multiple files are in a RAR.
2014-11-22 11:36:00 +01:00
shypike
9a37449aa5 Prevent false positives for encryption detection.
Weird posts with double rar-ed subtitles.
2014-11-22 11:35:08 +01:00
shypike
bb992b441f Implement support for X-Failure call-back URL.
Optionally to be called when par2 verification fails, the RAR files have CRC errors or the right password isn't known.
2014-11-22 11:19:17 +01:00
shypike
99521ca931 Prevent crash in unpacking due to unset variable. 2014-11-22 10:53:18 +01:00
Joe Nyland
5481b6663e Update favicon, apple-touch-icon and mstiles 2014-07-31 22:39:31 +01:00
shypike
ced3301c07 Fix problem with series name extraction in TVSort.
The latin1() call is now obsolete, but would convert None to u"None".
2014-06-19 22:07:38 +02:00
shypike
e6094141a0 Update main POT file. 2014-05-24 11:59:30 +02:00
shypike
7475c02a0f When testing notifications for Growl and Prowl, use new parameters without needing to save first. 2014-05-10 20:43:45 +02:00
shypike
d58ef71a9d Prevent URLs in the queue from getting ”sanitized”. 2014-05-10 19:47:08 +02:00
shypike
134a9b1507 Fix merge error.
Merge conflict of commit ef02b5eb (X-DNZB-Failure) wasn't resolved.
2014-05-10 18:19:13 +02:00
shypike
82d1d78d9c Using priority "Force" will override the duplicate NZB check.
Will work when manually entering an NZB.
2014-05-10 18:08:43 +02:00
shypike
6b1f9c0a50 Implement support to detect unwanted extensions inside RAR archives.
unwanted_extensions = .exe, .bla
action_on_unwanted_extensions = 1
2014-05-10 18:04:15 +02:00
shypike
7cbb6d5705 Prevent pseudo error message when testing "Notification Center". 2014-05-10 17:55:44 +02:00
Jostein Kjønigsen
4026904e9c Support testing email based on values in UI instead of stored config. 2014-05-10 17:47:42 +02:00
shypike
0286f0736c Don't trim file names when renaming them (so revert to old behavior). 2014-05-10 17:32:21 +02:00
shypike
2e77dd594e Add "pause_pp" to the API. 2014-05-10 17:31:44 +02:00
shypike
c2adc2b846 Pause/abort on encryption failed when pre-check was active. 2014-05-10 17:28:55 +02:00
shypike
2c90cf9f13 Also remove colons ":" with option sanitize_safe 2014-05-10 17:28:35 +02:00
shypike
6f6d57f76d Update OSX DMG template for Mavericks. 2014-05-10 17:28:04 +02:00
shypike
262b1304f4 Support "retry-after" attribute (for NZBFinder), used for rate limiting of NZB grabs. 2014-05-10 17:24:44 +02:00
shypike
fbc2deb054 Sanitize names when renaming files and folders. 2014-05-10 16:16:10 +02:00
shypike
a37c1e9a87 Make RAR/RAR5 detection more robust. 2014-05-10 16:15:27 +02:00
shypike
630c03f99f Support double quotes in password entry boxes on job detail page. 2014-05-10 16:12:52 +02:00
shypike
6ae257d463 Prevent embedded password from getting damaged by sanitizing. 2014-05-10 16:06:38 +02:00
shypike
b19f082d5a Extend password boxes on file details page. 2014-05-10 15:57:17 +02:00
shypike
2e24d073b9 Provisional RAR5 support.
Recognize magic rar5 marker and the "incorrect password" message.
2014-05-10 15:41:55 +02:00
shypike
37f3c482f0 Add password fields to File Detail pages of "smpl" and "Classic" skins.
Also remove previous band-aid to preserve passwords after repeated scans.
Scan is now only needed when old-style API call is done (so without password field).
2014-05-10 15:41:12 +02:00
shypike
70262b516c When checking unrar, prevent creating a zombie process on some systems. 2014-05-10 15:37:00 +02:00
shypike
38a7263aee Prevent unwanted change of queue order after editing job details.
When an explicit priority is set, the category evaluation should not temporarily change the priority,
which will cause a re-sort within the priority group.
2014-05-10 15:33:13 +02:00
Ed Courtenay
5145dfd4a3 Fix trailing slash 2014-05-10 15:24:45 +02:00
shypike
b072a3be57 Add Special option ”warn_dupl_jobs” to suppress/enable warning about duplicate jobs. 2014-05-09 22:00:29 +02:00
shypike
ebda0bf267 Support UNC paths in Sort expressions (Windows). 2014-05-09 21:50:42 +02:00
shypike
861ce9b4b2 Remove race-condition in PP-queue exit that prevented shutdown. 2014-05-09 21:50:20 +02:00
shypike
ba069ae8a3 Allow "Force" priority to be set in the NZO page.
Otherwise "Force" jobs will lose their priority when other fields are changed on the NZO page.
2014-05-09 21:49:36 +02:00
shypike
d3852666a7 Make sure a manually entered decryption password has no leading spaces. 2014-05-09 21:43:25 +02:00
shypike
805e97b581 Prevent PP queue timeout construction from keeping the CPU awake.
Due to a bug in the Python libraries queue.get(timeout=3) will awake the CPU every 50 msec.
The new code will only use the timeout to detect an empty queue.
If empty, but no end-of-queue check was needed, then launch an indefinite get().
2014-05-09 21:40:35 +02:00
shypike
56d9a74a39 Add more logging about password file results. 2014-05-09 21:37:48 +02:00
shypike
b20991ac4b Add Special option "flat_unpack" to remove embedded folders in archives. 2014-05-09 21:30:22 +02:00
shypike
d685280f30 Upgrade unrar to 5.01 2014-05-09 21:27:22 +02:00
shypike
725f2cf489 Fix problem with space added to password coming from a file name.
Regression caused by redesign of password scanner.
2014-05-09 21:26:35 +02:00
shypike
676c235fe6 Don’t send 8th parameter to user script when empty.
Modify sample script to show 8th parameter.
2014-05-09 21:26:07 +02:00
shypike
ef02b5eb62 Add some basic support for X-DNZB-Failure and X-DNZB-Details headers coming from indexers.
”Failure” will send an extra URL parameter to the post processing script.
”Details” will override ”More-info” in the History.
2014-05-09 21:19:30 +02:00
shypike
204df02d5d Add provisional support for unrar 5.
The report text for encrypted files has changed in unrar 5.
Thank you, Sander!
2014-05-09 21:14:55 +02:00
shypike
dbe51f0a82 Improve scanning of passwords in file names.
Replace regexes by plain logic to allow "/" and "{{" and "}}” in passwords.
2014-05-09 21:11:29 +02:00
shypike
c535fecf7f Add the command line parameter —pidfile to set an explicit PID-file name. 2014-05-09 21:06:41 +02:00
shypike
7c174e09e7 Fix another issue with commit 3b3759e81e (NZB-meta data). 2014-05-09 21:04:11 +02:00
shypike
bfb103534e Always rename files in Sorting, regardless of casing. 2014-05-09 21:03:56 +02:00
shypike
4700477bb9 Fix issue with commit 3b3759e81e (NZB-meta data). 2014-05-09 21:03:22 +02:00
shypike
cf0019e72f Add Solaris manifest to tar.gz distribution file. 2014-05-09 21:03:04 +02:00
shypike
67a6fba3bf Add usage of NZB-meta data and X-headers for Sorting.
Meta records: "episodename", "propername" and "year".
X-headers: "x-dnzb-episodename", "x-dnzb-propername" and "x-dnzb-year".
Controlled by an option.
2014-05-09 21:02:28 +02:00
shypike
2efdf186b0 Pass extra parameter to OSX Notification Center tool to enable Mavericks support. 2014-05-09 20:58:08 +02:00
shypike
34c6795519 Show job's ETA when its priority is forced, but queue is paused. 2014-05-09 20:57:22 +02:00
shypike
81c79ce470 Another fix for false encryption reports. 2014-05-09 20:56:21 +02:00
shypike
248f4eaab0 Fix crash in API-call "queue-rename" when "value3" is empty or undefined. 2014-05-09 20:55:51 +02:00
Jim80net
606105c5a9 Adds solaris manifest 2014-05-09 20:51:18 +02:00
shypike
50c9fb3a2f For Unix systems, expand wildcards for the par2 tool to prevent problems with some builds of par2cmdline. 2014-05-09 20:50:27 +02:00
shypike
5b5a4ad87b Remove "news" section in Config skin's main page.
Was never used and caused mixed mode https/http issues.
2014-05-09 20:30:29 +02:00
shypike
7d272c6695 Add password entry box to "File Details" page (Plush only).
Also extend api call "queue_rename" with a password parameter (value3).
2014-05-09 20:02:36 +02:00
shypike
e4f8e83e23 Prevent "special" sub-folders on file servers from being scanned during unpacking. 2014-05-09 19:51:20 +02:00
shypike
6100c8109c Add special option 'sanitize_safe' to remove bad Windows chars on other platforms. 2014-05-09 19:50:47 +02:00
shypike
0c7a2f38ae Fix false positive encryption alarm for some posts, 2014-05-09 19:47:18 +02:00
manandre
9608481fa5 Add of GUID field in Queue RSS feed
The NZO id is used as unique id for the queue RSS feed to help some RSS
readers (like Thunderbird) to identify articles when the link field is
the same for all articles
2014-05-09 19:45:37 +02:00
manandre
a0040f5dd5 Add of GUID field in History RSS feed
The NZO id is used as unique id for the history RSS feed to help some RSS readers (like Thunderbird) to identify articles when the link field is the same for all articles.
2014-05-09 19:38:16 +02:00
shypike
d4c1d32acc Fix special case of unjustified encryption warning. 2014-05-09 19:37:09 +02:00
shypike
52889d77cf Missing mini-par2 sometimes prevents the other par2 files from being downloaded. 2014-05-09 19:36:38 +02:00
shypike
f0a3f373a8 Make sure even invalid RAR files are fed to unrar and handle its reporting. 2014-05-09 19:26:32 +02:00
shypike
68c1fadb7d Merge pull request #139 from startswithaj/patch-1
Change for the smpl interface.
2014-02-20 22:22:04 +01:00
Jake Mc
13a2f15c75 Change for the smpl interface.
This resets the submit button text from 'Saved' to 'Save Changes' on form changes that happen after an initial save to notify user has edited since last save and has unsaved changes.

http://www.quirksmode.org/dom/events/change.html -> form.onchange is compatible with all modern browsers.

If your interested in pulling this request I will go through and make some other tweaks to the smpl interface.
2014-02-10 23:03:58 +10:00
shypike
644971a7a2 Merge pull request #137 from sanderjo/develop
Report IPv4 and IPv6 address via logging.info, patched against develop
2014-01-31 09:57:32 -08:00
SanderJ
6ec65239a4 Report IPv4 and IPv6 address via logging.info, patched against develop 2014-01-31 17:36:52 +01:00
shypike
a21a48ae98 Merge pull request #131 from breunigs/feature/example-systemd-unit-file
add (example) systemd unit file
2014-01-11 06:13:09 -08:00
Stefan Breunig
e9a436ceda add (example) systemd unit file 2014-01-08 00:15:11 +01:00
shypike
c8f4b46656 Do safe conversion of season/episode in RSS. 2013-12-03 22:29:03 +01:00
shypike
d2e4a6a5cc Revert "Prevent Unicode warning from CherryPy."
This reverts commit 2d3c263ab8.
2013-09-09 17:23:07 +02:00
shypike
efaa675183 Update README.mkd with 0.8.0 info. 2013-09-07 14:48:19 +02:00
shypike
2d3c263ab8 Prevent Unicode warning from CherryPy. 2013-09-07 14:45:28 +02:00
shypike
293d43470a Remove the "Classic" skin. 2013-06-15 11:45:46 +02:00
shypike
28aac3c70f Accept "nzbname" parameter in api-call "add url" even when a ZIP file is retrieved. 2013-06-14 20:58:49 +02:00
shypike
0aa0d8ad12 Accept partial par2 file when no others are available. 2013-06-14 20:50:53 +02:00
shypike
b305bf96a9 Option to make a job fail when user script exit code is non-zero.
'script_can_fail' in Config->Special.
2013-06-02 16:13:25 +02:00
shypike
aa783ae3b5 Add "From SxxEyy" filter to RSS.
A "require" filter that sets a minimum season/episode.
2013-06-02 14:41:05 +02:00
shypike
c55a517002 When post has just one par2-set, use full wildcard so that all files are repair and par candidates. 2013-06-01 11:33:07 +02:00
shypike
39f34a8355 Fix encryption detection again. 2013-05-28 20:47:32 +02:00
shypike
91c65817e7 Restore automatic recovery from 404 errors. 2013-05-26 12:27:18 +02:00
shypike
0b75447477 Better detection of incorrect par2 parameters. 2013-05-23 22:57:09 +02:00
shypike
4a64235cb6 Prevent needless error message when creating OSX top menu. 2013-05-19 14:46:06 +02:00
shypike
13c72b41a2 Fix crash on SFV-only downloads. 2013-05-12 13:08:56 +02:00
shypike
8328a36b5d Linux: compensate for par2 renaming files to CP1252 on UTF-8 file systems.
Par2-classic on Unix systems will rename files to CP1252 when running on UTF-8 file systems.
On such systems, this renaming must be undone before further processing.
2013-05-10 11:08:41 +02:00
shypike
d03568141d Fix omitted mkdir in package.py 2013-05-09 10:11:33 +02:00
shypike
821937e64f Fix problem in encryption detection. 2013-05-07 20:54:41 +02:00
shypike
d4717aac31 Make misc.globber() function behave when it gets a non-existing path. 2013-05-07 19:30:28 +02:00
shypike
7e5ce06e12 Update language templates. 2013-05-05 12:13:44 +02:00
shypike
d8ba7577f8 List NZB age in the download report. 2013-05-04 22:27:53 +02:00
shypike
ef1db24880 Show downloaded amount per server in download report. 2013-05-04 15:23:18 +02:00
shypike
c83300eb16 Make parameter errors visible in some Config Pages.
Messages about bad parameters were not shown in the Config skin's pages.
Fix this for: General, Folders and Notifications.
2013-05-04 13:51:33 +02:00
shypike
9516d80746 Remove support for upgrading very old data on OSX.
Also clean up some missed cache_dir references.
2013-05-04 10:45:26 +02:00
shypike
486d1113df Remove all support for pre-0.6.x style queues. 2013-05-03 21:03:38 +02:00
shypike
b3d24d7f3d Avoid problems with long paths in the temporary download folder,
The par2 tool does not support paths above 259 in any way.
Take extra measures to prevent long paths from being used,
by trimming the download path.
When over sized paths are still generated, abort post-processing with a clear error message.
2013-05-03 20:41:32 +02:00
shypike
edd1002313 Add long path support for Windows.
Use "\\?\" prefix for all path-related handling in post-processing.
Use shortened paths for unpacking and user scripting.
Also support oversized paths in "incomplete" as long as the base folder is below 260.
2013-04-30 18:32:53 +02:00
shypike
99939db461 Prevent needless reevaluation of show titles for pending post-processing jobs. 2013-04-29 20:53:18 +02:00
shypike
da8d9d0a89 In paths, "~" should always be the user's home folder and not the base folder. 2013-04-28 16:21:20 +02:00
shypike
6904b2de1f Show free disk space using flexible factors instead of fixed "GB". 2013-04-28 15:55:32 +02:00
shypike
0f18771534 Allow "Default" category to be selected in Multi-ops. 2013-04-26 21:53:48 +02:00
shypike
82f76e04fe Fix issue with multiple selection in Config->Sorting.
'platform_encode' crashed on lists.
2013-04-20 16:41:51 +02:00
shypike
28c45ed4aa Allow for weird encoding of names of uploaded NZB files.
Sometimes CherryPy returns correct Unicode, sometimes UTF-8 disguised as Unicode.
2013-04-18 21:53:10 +02:00
shypike
4e69fa3f4a CherryPy Patch: allow UTF-8 encoding of headers.
Modern browsers can use non standardized UTF-8 encoding on headers,  most notably the Content-Disposition.
In Content-Disposition it is used to carry file names.
Patch CherryPy so that UTF-8 is tried before ISO-8859-1.
2013-04-18 21:51:08 +02:00
shypike
dacfe8b29e Resolve "derefer.me" links to NZB files. 2013-04-15 19:19:42 +02:00
shypike
27b65cd136 Add "Enable/Disable quota management" to Scheduler.
This allows user to set a period in which quota are ignored.
2013-04-14 12:53:35 +02:00
shypike
39ac8d9bee Touch up Bonjour support. 2013-04-14 12:37:32 +02:00
shypike
330cd0623c Ignore a number of special folders used by NAS systems when looking for orphaned jobs.
"@eaDir" and ".AppleDouble"
2013-04-13 20:26:02 +02:00
shypike
795b9f8e60 When creating a new database, set the right user_version. 2013-04-13 20:08:16 +02:00
shypike
3c7d8ab241 When repairing queue, don't try to recover non-job items in the admin/future folder. 2013-04-12 20:50:42 +02:00
shypike
46dd2137ea Add Bonjour/ZeroConfig Support.
Basic ZeroConfig support, only when SABnzbd listens to external addresses.
On all platforms, ZeroConfig/Bonjour libraries fail to implement "localhost" support properly.
Default on when support libraries are installed, special option to disable in case of trouble.
HTTP-->HTTPS redirection only enabled when ZeroConfig is active.
2013-04-12 20:05:09 +02:00
shypike
7d9afc3bbd CherryPy patch: allow setting of a HTTPS-->HTTP forwarding using "301 Moved Permanently".
Needed for later Bonjour support.
2013-04-11 22:31:05 +02:00
shypike
33e8ffd571 Fix incorrect code after removal of newzbin support. 2013-04-11 20:57:19 +02:00
shypike
1e518ef6a6 Config: fix error in Cfg->General->"External Internet access". 2013-04-11 19:53:58 +02:00
shypike
8fd993d14e Remove spaces from RSS URL-s. 2013-04-09 22:04:13 +02:00
shypike
7580b3f5ed Config and Wizard skins: fix problem with Unicode when using Chrome.
The Config skin and the Wizard were missing a proper Content-Type in <head>.
2013-04-09 21:47:54 +02:00
shypike
81bbd07c21 Make encryption detection more careful. 2013-04-09 19:35:54 +02:00
shypike
2cf2492871 Config-skin: Move clean-up list from Config->General to Config->Switches->Post-processing. 2013-04-06 19:35:51 +02:00
shypike
500b25e46f Update some UI texts. 2013-04-06 19:25:23 +02:00
shypike
56a2e492b4 Solve some fringe issues for OSX. 2013-04-06 19:10:55 +02:00
shypike
2cf0b65c83 Add refresh button to Status-Connections page. 2013-04-06 14:11:50 +02:00
shypike
3b81d84333 Errors/warnings for Notifications were not extended with parameters. 2013-04-03 23:12:58 +02:00
shypike
5128e6eef1 Fix issue where a recovered lost job would get the name "None" in the queue.
Problem happens when the orphaned job doesn't have an attribute file.
Not a normal use case, but possible as a result of a crash.
2013-04-01 19:23:09 +02:00
shypike
ba080f346b Let API calls for the queue provide more feedback.
Return True and list of affected ids
Return False (and empty list) when no ids matched.
2013-04-01 16:12:45 +02:00
shypike
bdfa862796 Only warn user about missing max line speed when an actual speed limit percentage is set. 2013-04-01 14:31:26 +02:00
shypike
d7a59acc7f Fix another merge error. 2013-03-31 23:20:51 +02:00
shypike
a24598deee Fix merge error. 2013-03-31 22:28:33 +02:00
shypike
3f4e375165 Merge pull request #85 from sabnzbd/rss_filtering
RSS filtering
2013-03-31 13:21:20 -07:00
shypike
b73c25a722 Swap title and size columns. 2013-03-31 22:18:23 +02:00
shypike
f2b3fdbd13 Add "at least" and "at most" filters to RSS.
Will only work for feeds that have a recognizable job size in their <description> field.
2013-03-31 22:18:23 +02:00
shypike
0f754c67a3 Prevent race condition when upgrading history1.db
Set "user_version" to 1 before inserting new columns.
2013-03-31 22:13:20 +02:00
shypike
9fe8f5e446 Improved duplicate detection.
The generic version is based on the md5sum of the combined article-identifiers and no longer on the NZB backup.
Episode detection is based on the  name/season/episode extraction from the job title.
There's now an option for generic detection and an option for episode detection.
All info is stored in the History database, under a new user_version number "1" because two new columns are needed.
2013-03-31 22:13:20 +02:00
shypike
c281bb4d30 Enhance security settings for external access.
User can set the range of the local network ("local_range").
Access for anything outside the local network is controlled by "inet_exposure".
The access levels are: none, nzb-only, api, full-api, full-ui
2013-03-31 18:38:10 +02:00
shypike
ecc2d6b1fd Restructure Config->Notifications and add Prowl support.
- Prowl support
- Separate settings for each notification service
2013-03-31 18:11:12 +02:00
shypike
bd27532eba Fix crash of bpsmeter when both old and new file are missing. 2013-03-31 16:53:20 +02:00
shypike
13d544cb81 Fix error introduced by scheduled priority classes.
After scheduler evaluation, only set/reset job-pause after considering the current status.
When adding new job to queue:  pause job when schedule evaluates pause, but don't undo an already set pause.
When changing priority: only pause when job is checking/downloading/queued, only resume when it was paused.
2013-03-30 11:31:20 +01:00
shypike
944568d6dc Remove all legacy code for newzbin and nzbmatrix. 2013-03-28 21:18:48 +01:00
shypike
420ff033e7 Add option to enable/disable recursive unpacking. 2013-03-26 19:38:42 +01:00
shypike
36406531ab Add Finnish translation. 2013-03-25 20:04:54 +01:00
shypike
73754e537e Improve the url grabber code.
Use the newer urllib2 that supports HTTP 1.1 properly.
Remove some obsolete code.
2013-03-22 22:26:41 +01:00
shypike
4f41db3953 When changing other job attributes than priority and category, prevent a re-sort of the queue. 2013-03-22 20:43:47 +01:00
shypike
e13b6b76ba Update the warning in README.md 2013-03-21 23:04:07 +01:00
shypike
322f9037fe Remove the attribute remapping for job admin files.
Not needed anymore because compatibility with 0.7.x is broken anyway.
2013-03-21 22:59:34 +01:00
shypike
a0f5c728da Fix issue with RSS feed names having non-plain ASCII characters. 2013-03-21 21:10:24 +01:00
shypike
c91c226071 Optimize and correct testing for string property.
Don't test for "isinstance(x, str) or isinstance(x, unicode)" but for "isinstance(x, basestring)".
Correct some places that forgot to test for unicode (so test for basestring).
2013-03-20 23:09:48 +01:00
shypike
7eff48cd00 Fix bad handling by API of Sort menu in Plush (improved).
Fix the root cause (in nzbqueue.py) instead of the symptom (in api.py).
2013-03-20 22:58:11 +01:00
shypike
40a27ae2d7 Make name sorting of the queue case-insensitive. 2013-03-20 22:46:04 +01:00
shypike
193d1125ba Fix bad handling by API of Sort menu in Plush.
Leads to (invisible) crashes in the UI, but also to warnings and failure of the sort.
2013-03-20 22:45:17 +01:00
shypike
a417b27e18 All regex expressions in newsunpack should use "raw" literal strings. 2013-03-20 21:58:14 +01:00
shypike
635ba9d98b Don't try to join a set of just one file (e.g. IMAGE.000) and reduce memory usage when joining large segments.
When there a single file called something like IMAGE.000, don't try to join it.
The joining procedure tries to read an entire segment file into memory, this may lead to a string overflow.
Use shutil.copyfileobj() with a 24 MB buffer instead.
2013-03-20 21:54:47 +01:00
shypike
7fe04d229b Use server-id instead of host:port in all downloader messages.
When user has multiple accounts on one server, the distinction wasn't visible in the messages.
2013-03-19 21:20:08 +01:00
shypike
63f2237ee1 Implement robots.txt to keep web crawlers out.
Should not really be needed, because users should password-protect any
SABnzbd instance exposed to internet.
2013-03-19 20:48:34 +01:00
shypike
d3088b504d Option to download all par2 files when repair is needed.
With QuickCheck on, all par2 files will be downloaded after QuickCheck verification fails.
With QuickCheck off, all par2 files will always be downloaded.
2013-03-19 20:22:14 +01:00
shypike
e24e374c15 CherryPy patch: no strict adherence to CRLF requirement for MIME messages. (Correction)
This would break some poorly written third-party utilities (like nzbdStatus for FireFox).
2013-03-19 19:47:01 +01:00
shypike
24fef5a838 CherryPy patch: no strict adherence to CRLF requirement for MIME messages.
This would break some poorly written third-party utilities (like nzbdStatus for FireFox).
2013-03-18 20:03:34 +01:00
shypike
ec9dd97fee Improve INI file backup handling.
When backup is not possible, do not write changes to disk.
When writing the new INI file fails, rename the backup to INI.
2013-03-17 11:45:42 +01:00
shypike
c946ab04f6 Only warn about missing unzip and 7za when their usage has been enabled. 2013-03-16 13:34:01 +01:00
shypike
f039a8f9f4 Improve scheduled pause/resume for priority groups.
Set pause/resume state based on scheduled events when adding job or changing its priority.
No change to jobs when changing schedules.
2013-03-15 21:57:56 +01:00
shypike
75cf199455 Add scheduled events for pausing and resuming low, medium and high priority job groups. 2013-03-15 21:05:48 +01:00
shypike
9c153f4b38 Improve recovery from corrupt or unwritable History database.
Create new database when the existing one is corrupt.
Report error when database is read-only, but ignore otherwise.
2013-03-15 19:58:46 +01:00
shypike
5c5bf56e7c Show URLs for primary and secondary skin in Config->General. 2013-03-14 22:08:37 +01:00
shypike
cdae0a741c Don't use an old queue when a new queue isn't present yet. 2013-03-14 22:06:59 +01:00
shypike
d00b47a32e Add warning about INI file upgrade issues. 2013-03-14 21:02:29 +01:00
shypike
2574c12a9e Allow password in AddNZB dialog box to be entered in the "nzb name" field.
NzbObject sanitized the 'nzbname' parameter before the password was extracted, which disabled password extraction.
Improve scan_password() so that a password-only "nzbname" parameter is handled correctly.
2013-03-13 19:37:18 +01:00
shypike
1fd9bf7032 Save job admin to disk when setting password or changing other attributes. 2013-03-12 20:56:51 +01:00
shypike
32ba0ad001 Plush: add "resume pp" entry to pulldown menu, when pause_pp event is scheduled.
The option allows manual resume of a scheduled paused post-processing.
2013-03-12 20:56:44 +01:00
shypike
e05b67e2ca Improve RAR detection. 2013-03-12 20:56:19 +01:00
shypike
bbfcb22796 Enable "abort if hopeless" for pre-check as well. 2013-03-12 20:56:10 +01:00
shypike
3cbc507409 More Unicode fixes.
- Show intermediate panic screen when old queue is detected
- Convert byte-counters file to Unicode
2013-03-12 19:30:01 +01:00
shypike
adcfc8dd2d Add Russian, Serbian and Simplified Chinese translations. 2013-03-11 19:36:36 +01:00
shypike
620e10a69f Convert to Unicode application.
This breaks compatibility with existing 0.7.x queues.
Polish and Romanian no longer need conversion to latin1.
2013-03-10 23:15:30 +01:00
shypike
ede33fad32 CherryPy patch: fix HTTPS file upload bug.
As described in: https://bitbucket.org/cherrypy/cherrypy/issue/1068
2013-03-10 23:00:04 +01:00
shypike
c811e090ec CherryPy patch: fix missing import of 'socket_errors_to_ignore'. 2013-03-10 14:17:39 +01:00
shypike
05c7c60c1f Add CherryPy 3.2.2 official release.
Folders 'tutorial', 'test' and 'scaffold' have been removed.
The root folder of the tar.gz has been omitted.
File VERSION.txt has been added.
2013-03-10 14:06:20 +01:00
shypike
347eaf46c3 Remove old CherryPy version. 2013-03-10 13:58:53 +01:00
shypike
e98c6e8cae Restore missing checkboxes for Multi-ops.
Introduction of searchbox (merge request #77) messed up checkboxes,
due to a debug version of $.plush.RefreshQueue() overriding the normal version.
2013-03-09 15:24:12 +01:00
shypike
9c43d20363 Replace "out of retention" text by the more likely "no longer on server". 2013-03-09 12:29:40 +01:00
shypike
357dc0437e Merge pull request #80 from jm3/tune-header-layout
Tune header layout to accomodate new search box
2013-02-13 10:21:16 -08:00
John Manoogian III
eb80a613f4 tune queue header layout to accomodate search box 2013-02-12 23:01:01 -08:00
John Manoogian III
597e03a692 make indenting style consistent 2013-02-12 21:55:49 -08:00
shypike
d351aa6ad6 Merge branch 'master' into develop 2013-02-12 18:06:37 +01:00
shypike
4989e8c156 Merge pull request #77 from jm3/searchable-queue-UI
Plush: front-end engineering for searchable NZB queue.
2013-02-11 13:32:07 -08:00
John Manoogian III
200c3d8b6a HTML+CSS+JS UI work for making queue searchable
Matches UI + behavior of history search (search updates on Enter press
and Queue Purge respects current queue search value)
2013-02-11 13:15:12 -08:00
shypike
90895116a2 Support "Search" box in queue display and Purge-queue command.
Python part.
2013-02-10 15:51:55 +01:00
shypike
a50412a327 Merge branch '0.7.x' 2013-02-07 20:30:09 +01:00
shypike
07be241112 Update text files for 0.7.11 2013-02-07 20:21:59 +01:00
shypike
fbdd264653 Update translations 2013-02-07 19:38:48 +01:00
shypike
a8bc793132 Fix regression error that could result in slow verification of NZBs with multiple rar/par sets.
The detection of obfuscated files failed, causing each par2 run to parse all files.
2013-02-07 19:27:03 +01:00
shypike
6bce423f23 Fix "Sorting" file renaming for RAR files that contain an extra folder level.
collapse_folder() should convert filename list accoerding to the renaming it does.
It's also its task to remove the _unpack_ markers from those filenames.
Rename this function to rename_and_collapse().
2013-02-04 22:47:53 +01:00
shypike
51cabf85a1 Plush: default refresh-rate now 4 sec and multi-ops bar visible. 2013-02-04 21:43:12 +01:00
shypike
38fdc2c7c8 Show warning when decoder encounters I/O-errors. 2013-02-03 12:25:33 +01:00
shypike
b91a2af9df Some badly encoded articles can be accepted as valid data.
Regression error since 0.7.9 (commit ccfbb07).
decoder.decode() no long ran into an excepton when no valid data was found.
Solved now by using the "found" flag.
2013-02-02 14:57:28 +01:00
shypike
f12317f499 Merge branch 'master' into develop 2013-01-31 23:06:29 +01:00
shypike
c300159537 Merge pull request #71 from jm3/fix-seperate-typo
Fix typo in config+comments: s/seperate/separate/g
2013-01-31 14:01:42 -08:00
shypike
e4de2d626d Merge branch '0.7.x' 2013-01-30 22:39:25 +01:00
shypike
71ee5969a8 Update text files for 0.7.10 2013-01-30 21:04:24 +01:00
shypike
1b1c772b55 Update text files for 0.7.10 2013-01-30 20:34:13 +01:00
shypike
458eb3a417 Update translations 2013-01-29 19:31:57 +01:00
shypike
98b753f015 Change access to build share, even more improved. 2013-01-28 23:31:25 +01:00
shypike
a3284e12d2 Change access to build share, improved. 2013-01-28 22:51:35 +01:00
shypike
c85120bb4b Change access to build share. 2013-01-28 21:36:12 +01:00
John Manoogian III
7f7f3dddb5 Fix typo in config+comments: s/seperate/separate/g 2013-01-26 18:48:06 -08:00
shypike
903925e06b Update main POT file. 2013-01-24 10:35:27 +01:00
shypike
03196f25e4 Accept NNTP error 400 without "too many connection" clues as a transient error.
Previously it would file a warning and lock out the server for a few minutes.
Reduce the lockout to 6 seconds.
400 should report "too many connections" but some servers use it for temporary connectivity issues.
2013-01-24 10:34:18 +01:00
ShyPike
e3f3f962b6 Handle unrar error messages better (like "path too long").
In the case of fully encrypted RAR files, SABnzbd cannot check the resulting files.
Handling unrar errors explicitly is better anyway.
2013-01-23 22:23:11 +01:00
shypike
153f92e697 "Failed" message should also appear in email notifications. 2013-01-22 23:27:21 +01:00
shypike
c1dcafe953 Display next RSS scan moment in Config->RSS 2013-01-19 21:34:31 +01:00
ShyPike
b53d97732b Reset the "today" byte counters at midnight even when idle.
Set a scheduled event at midnight for resetting the "today" byte counters.
Otherwise, when idle, the bpsmeter isn't called at all.
2013-01-17 22:24:12 +01:00
ShyPike
8f47cce9c8 Try to process obfuscated rar/par sets as good as possible.
When detecting a main par2 file without extra pars, use full wildcard for par2-run.
During par2-run, register which par2 files contain matching blocks.
Remove matching par2 files after the repair.
Skip sets of which the main par2 file has been deleted (due to having been used
in another set).
2013-01-15 22:42:59 +01:00
ShyPike
3cf42a7f94 Accept %fn (next to %fn.%ext) as end parameter in sorting strings. 2013-01-14 20:52:37 +01:00
ShyPike
ae74370cfb Add IP address of unauthenticated API-call to warning. 2013-01-14 19:23:47 +01:00
ShyPike
2aaa283991 Plush: repair and unpack icons in History were swapped.
Fixing does mean that order changes too, but that was the only way to
make the icons correspond with the hover popups.
2013-01-11 21:55:14 +01:00
ShyPike
dca7a8ccdb Plush: show speed when forced job is running in Paused mode. 2013-01-11 19:48:17 +01:00
ShyPike
2b3b5b765a Plush: show speed when forced job is running in Paused mode. 2013-01-11 19:45:31 +01:00
shypike
3172d6e987 Disable scheduled task for newzbin bookmarks. 2013-01-10 21:36:43 +01:00
ShyPike
c237ddfef4 Update text files for 0.7.9 2013-01-06 20:11:33 +01:00
ShyPike
b543dcb5ac Fix text in dropdowns being hard to see in chrome. 2013-01-06 19:33:31 +01:00
ShyPike
ccfbb07333 Take servers that only support ARTICLE into account.
When only full articles are available, the decoder needs to scan more lines
to find the start of the payload.
2013-01-06 19:33:19 +01:00
ShyPike
256ccbd6a1 Prevent crash in decoder.py 2013-01-06 19:06:15 +01:00
ShyPike
3a306ab8e0 Fix text in dropdowns being hard to see in chrome. 2013-01-06 14:34:28 +01:00
ShyPike
b9b87040dd Take servers that only support ARTICLE into account.
When only full articles are available, the decoder needs to scan more lines
to find the start of the payload.
2013-01-06 14:20:32 +01:00
ShyPike
2b8f3b0ac9 Merge branch 'master' (0.7.8) into develop 2013-01-04 19:58:10 +01:00
ShyPike
d8d507f110 Update text files for 0.7.8 2013-01-03 19:24:27 +01:00
ShyPike
7b3309649f Cancel encryption detection if meta-data if NZB contains password. 2013-01-03 18:44:31 +01:00
shypike
9a7a6652e8 Update text files for 0.7.8 2013-01-03 18:41:50 +01:00
shypike
db4891748f Update copyright year. 2013-01-03 18:40:49 +01:00
ShyPike
3dce2e8908 Support NZB 1.1 meta data; currently "category" and "password" are used.
"category" will trigger category conversion.
"password" value(s) will be used when an encrypted download is encountered.
The latter will also suppress the on-the-fly encryption detection.
2013-01-02 23:05:15 +01:00
ShyPike
c91291c315 Don't retry an empty but correct NZB retrieved from an indexer.
Also add special option "warn_empty_nzb" to control warning about empty NZBs.
2013-01-02 19:37:49 +01:00
shypike
a2a5a1f8e4 Make sure "Abort" error message ends up in download report. 2013-01-01 22:04:24 +01:00
shypike
7651f709ad API functions "addfile" and "addlocalfile" now support "nzbname" parameter for ZIP files with single NZB.
Also, prevent crash on calling "cat_convert" in those api functions (undefined).
2012-12-31 14:11:09 +01:00
shypike
a565077348 Update translations 2012-12-31 12:27:13 +01:00
shypike
6cf99e7d3a Add handling of an extra par2 error message. 2012-12-30 15:06:35 +01:00
shypike
f730a82005 Check for IPv6 connectivity should not use specific exceptions. 2012-12-30 13:16:46 +01:00
ShyPike
5449607c1d Update POT file. 2012-12-28 14:12:36 +01:00
ShyPike
c62415abfd Add "Abort" option to encryption detection.
The option pause_on_pwrar gets an extra value (2) which will
abort an encrypted job. Retrying the job will disable the check.
2012-12-28 14:10:53 +01:00
ShyPike
dcbea3057c Register removed articles and list in download report. 2012-12-28 13:23:34 +01:00
ShyPike
91642d16c8 Update POT file. 2012-12-27 22:26:33 +01:00
ShyPike
2f2773149d Fix missing Retry link for "Out of retention" jobs. 2012-12-27 22:26:05 +01:00
ShyPike
adaba03f50 Option to terminate download if too much data is missing.
Option 'fail_hopeless' Config->Switches.
On-the-fly check for possible completion after each file is processed.
Abort if it's no longer possible to download at least 99% of total data (payload + par2).
Don't do the check when retrying from History.
2012-12-27 21:56:00 +01:00
shypike
58a5e09540 Prevent web-watchdog from crashing when using Python 2.5 2012-12-27 11:28:06 +01:00
shypike
20dc906095 Support servers that don't support STAT and BODY commands.
When server sends error 500, use alternative "HEADER" and "ARTICLE" instead,
which are less efficient.
2012-12-24 20:12:01 +01:00
shypike
e2f41d3761 Add special "wait_for_dfolder", will wait for "temp download folder" at startup.
At startup, wait for the temporary download folder to come on line.
Supports situations where external drives are used, which do not mount before
SABnzbd starts up.
2012-12-23 12:11:18 +01:00
ShyPike
ab1372c7fc Prevent crash in DateSorter. 2012-12-20 18:46:45 +01:00
ShyPike
177fab2e54 Merge branch 'master' into develop 2012-12-19 22:28:26 +01:00
ShyPike
e305678cf4 In Sorting the %fn substitution sometimes fails to rename the file properly.
The Sorting code assumed that the file name used as the source for %fn
is always a base name. Sometimes it can be a full name. Deal with it.
2012-12-19 21:51:00 +01:00
ShyPike
a82df9bf2e Update text files for 0.7.7 2012-12-14 20:47:59 +01:00
ShyPike
986604f27c Update feedparser.py to from 5.1 to 5.1.3 2012-12-14 19:04:50 +01:00
shypike
59324c7453 Remove "Indexers" support page from the Wizard. 2012-12-09 13:03:03 +01:00
shypike
91613a5b37 Remove "Indexers" page from all skins. 2012-12-09 12:28:02 +01:00
shypike
5ca05fd2c0 OSX: remove association with "rar" and "zip" files. 2012-12-08 14:44:54 +01:00
shypike
4d4045cff4 Update translations 2012-12-05 21:27:04 +01:00
ShyPike
1f209a42d8 Filter unusable folders from lists generated by pathbrowser. 2012-12-03 21:13:13 +01:00
shypike
bffbb362db Update POT file. 2012-12-01 10:53:05 +01:00
shypike
435eed8818 Add 'B' to download totals on main page. 2012-12-01 10:48:27 +01:00
shypike
f86656543a Add schedule: when schedule has no days, assume daily instead of ignoring. 2012-12-01 10:06:11 +01:00
ShyPike
9c510c6dd1 When re-evaluating RSS feed, the original sort order was not preserved.
When re-using RSS entries from memory, use the original feed order and not
Python's random dictionary order.
2012-11-30 20:48:03 +01:00
ShyPike
f81ab3d1c0 Prevent crash when nzbxxx.com's category cannot be determined. 2012-11-30 18:47:38 +01:00
ShyPike
d1585c28a9 Remove all visible features for newzbin. 2012-11-29 22:07:45 +01:00
ShyPike
9c314532c0 Add optional web-watchdog to the watchdog. 2012-11-29 21:47:14 +01:00
ShyPike
853bda5d86 Add 'B' to server amounts. 2012-11-28 21:18:02 +01:00
ShyPike
d05e31f7f0 Remove placeholder for Cleanup filter in Config->General. Confusing for users. 2012-11-28 20:49:16 +01:00
ShyPike
383354871d Generic sort didn't always rename media files in multi-part jobs properly.
`rename_similar()` should be called outside of the media rename loop.
`rar_extract_core` should always return full paths.
2012-11-26 22:10:48 +01:00
ShyPike
bd54bbcf19 Generic sort didn't always rename media files in multi-part jobs properly.
`rename_similar()` should be called outside of the media rename loop.
`rar_extract_core` should always return full paths.
2012-11-26 21:39:32 +01:00
ShyPike
2086a217e0 Don't use SFV check when more par2 files can still be downloaded. 2012-11-24 22:38:04 +01:00
ShyPike
34f3574746 Update unrar for Windows to 4.20 2012-11-24 21:50:14 +01:00
ShyPike
1dfe0b957e Catch a renaming bug in par2-tbb and retry with par2-classic. 2012-11-24 21:33:58 +01:00
shypike
17d14bc3b4 OSX: update unrar to version 4.20 2012-11-24 21:33:36 +01:00
ShyPike
885032e436 Improve handling of orphaned jobs.
Re-queue of a set without a nzb.gz file would not recognize par2 sets.
Convert "None" strings in the attrib file to None.
2012-11-24 21:21:06 +01:00
ShyPike
ceee95aaf7 Orphan re-queue and delete failed when path contains non-plain ASCII characters.
Convert UTF-8 name coming from CherryPy to a platform compatible name.
2012-11-24 20:38:29 +01:00
ShyPike
aab4d933be Catch a renaming bug in par2-tbb and retry with par2-classic. 2012-11-20 23:37:28 +01:00
shypike
9b3220786e OSX: update unrar to version 4.20 2012-11-20 21:23:01 +01:00
ShyPike
490c72d934 Merge master (0.7.6) into develop. 2012-11-19 21:12:37 +01:00
ShyPike
bc6b3091eb Update text files for 0.7.6Final. 2012-11-17 14:01:38 +01:00
ShyPike
4be1a13316 Add the "User-Agent" header of each API call to logging and warnings. 2012-11-17 10:56:36 +01:00
ShyPike
a77327ee7f Support NZB re-queuing also for NZB files in sub-folders. 2012-11-15 22:01:41 +01:00
ShyPike
aa706012af Update text files for 0.7.6Beta2 2012-11-14 21:01:01 +01:00
ShyPike
f5b6203194 Make check for running SABnzbd instance more robust.
Cancel bad side-effect of removing the version check.
Under some circumstances SABnzbd can draw the unjustified conclusion
that another instance is running. Now check for a proper version pattern
in the received output.
2012-11-14 20:57:02 +01:00
ShyPike
1ced9a54e4 Fix evaluation of schedules at startup.
With the introduction of multiple-day schedules, the schedule evaluator failed.
Fixed the evaluation.
A side-effect is that Config->Scheduler will no longer show the schedules in
the order they will occur from now. Instead they will be shown in order of
occurrence from Monday to Sunday.
2012-11-14 20:23:40 +01:00
ShyPike
06c7089a77 Correct indentation in interface.py 2012-11-13 20:59:50 +01:00
ShyPike
ee1d864eea Update text files for 0.7.6Beta2 2012-11-12 21:47:19 +01:00
ShyPike
d703338935 Repair failed when mini-par2 file was in NZB but did not result in a file.
An incomplete mini-par2 file is now skipped in favor of the next available vol-par2 file.
A missing or damaged par2 file must make the next par2 file the primary par2-file
in the next repair run.
2012-11-12 21:10:27 +01:00
ShyPike
e87b24c460 Update text files for 0.7.6Beta1 2012-11-09 19:30:29 +01:00
shypike
3404ef6516 Update translations 2012-11-09 19:23:04 +01:00
shypike
181897e92b Prevent the Decoder from choking the Assembler.
Because the Decoder is CPU-bound, it has no reason to relinquish control.
This will choke the Assembler which cannot write finished and cached articles
to the designated file. The result is an increasing cache, which either grows
indefinitely or until the Decoder must flush articles.
By simply adding a sleep(0.001), the Decoder will trigger the task-scheduler
after each article, giving the Assembler a chance to do its work.
2012-11-08 23:12:15 +01:00
ShyPike
26a504e3e2 Prepare code for intro of zoned access to UI and API. 2012-11-07 21:41:04 +01:00
ShyPike
b72ed09011 Prevent IPv6 Usenet servers from being tried when they're not reachable.
Detect whether external IPv6 addresses are reachable.
If so, allow IPv6 IPs to be picked.
Add a special option 'ipv6_servers' to allow the user to forbid (0), allow (1) or force (2)
the use of IPv6. Value 2 can be used in case the detection by SABnzbd doesn't work reliable.
2012-11-07 20:07:25 +01:00
ShyPike
bb99c0d58e Fix problem with late detection of win32api absence. 2012-11-06 23:40:45 +01:00
ShyPike
4516027fdb Repair side-effect of SFV improvements.
A download without par2 files and without SFV files should not be failed.
2012-11-05 22:51:51 +01:00
ShyPike
e35f2ea3cd Prevent crash on Unix-Pythons that don't have the os.getloadavg() function.
Some Unix Pythons are defective in not providing os.getloadavg().
Add simple exception handler to cover this case.
2012-11-05 20:40:21 +01:00
ShyPike
6b79fad626 Remove version check when looking for a running instance of SABnzbd.
This will lower the chance of inadvertently launching multiple instances.
User will need to use --new to force a new instance.
2012-11-05 19:19:32 +01:00
ShyPike
ac311be430 Successfully pre-checked job lost its attributes when those were changed during check.
For successful jobs, the attributes were not saved to disk (they were for failed ones).
Solution is to save attributes independent of result.
2012-11-05 19:09:10 +01:00
shypike
4fb32bff5f Fix crash when a job is sent to postprocessing immediaterly after startup.
The Assembler wasn't running when job was sent to post processing at
the startup of the queue. The Assembler is used as a relay to send
a job to post-processing.
Solution is to start Assembler before initializing the queue.
2012-11-05 18:54:15 +01:00
ShyPike
5fda342a55 Don't try to repair/verify par sets that have "sample" in their names.
Only when sample deletion is enabled.
2012-11-03 20:34:17 +01:00
ShyPike
e23aab4710 Improve SFV handling, preventing odd side-effects in multi-set NZBs.
SFV verification per PAR-set using only the matching SFV file.
When no par2 files are found, use all available SFV files.
Remember the verification status of each set in the "verified" marker file.
Improve par-set matcher, so that there's no mix-up when one set name
is a substring of another set name.
2012-11-03 16:57:32 +01:00
ShyPike
3837d5dace Handle par-sets that have been renamed after generation of the par2 files.
Requires a wildcard to be added as a par2 parameter to make it scan all applicable files.
The rename actions need to be stored in a persistent file to prevent re-downloading in a Retry.
The status of correct sets must be remembered while fetching extra par file for failed sets.
2012-11-03 16:56:53 +01:00
shypike
f61e7cb1ed Update text files for 0.7.5 Final. 2012-11-03 16:15:22 +01:00
ShyPike
3de0c0e4ac Add missing "%dn" (original folder name) formula to Generic Sorting. 2012-11-01 21:24:51 +01:00
ShyPike
63796d3feb Improve logging for RSS readouts. 2012-11-01 19:47:48 +01:00
ShyPike
6b07529300 Update text files for 0.7.5RC1 2012-10-30 20:35:57 +01:00
ShyPike
e10676710c Support for news in Config. 2012-10-30 20:17:51 +01:00
shypike
77f67c6666 Merge pull request #59 from akuiraz/newzxxx2_fix
Fixed regex for newzbin rss filtering
2012-10-30 11:58:20 -07:00
ShyPike
bdbcdd61e1 Mask password in "Add Server" dialog. 2012-10-30 19:51:12 +01:00
ShyPike
4ab7ec754d Add periodic detection of completed but hanging jobs in the queue.
The 30 second watchdog now detects jobs without pending files.
Those jobs will be sent to the post-processor.
2012-10-30 18:47:18 +01:00
akuiraz
20f98f48bc Fixed regex for newzbin filtering by adding xxx2, now rss feeds from newzxxx2.ch will successfully download 2012-10-30 01:27:36 -04:00
shypike
84e0502e50 Prevent crash when trying to open non-existing "complete" folder from Windows System-tray icon. 2012-10-28 12:39:34 +01:00
shypike
2aa1b00dbb Prevent CherryPy crash when reading a cookie from another app which has a non-standard name. 2012-10-27 13:33:33 +02:00
ShyPike
972078a514 Fix problem with "Read" button when RSS feed name contains "&".
The feed's name wasn't properly encoded in the URL.
2012-10-24 19:34:45 +02:00
shypike
be8382d25b Add special option 'empty_postproc'.
Setting this option will run the user script on an empty download.
Normally this isn't done.
The status sent to the user script is -1, meaning "no files were downloaded".
2012-10-21 18:23:25 +02:00
shypike
8d46e88cd8 Update translations 2012-10-21 12:56:00 +02:00
shypike
6b6b1b79ad Add 'prio_sort_list' special.
This is a list of file name extensions.
Matching files will be the first to be downloaded within an NZB.

Also, if the user sets a simple space-seperated list, this will be converted to a standardized list.
2012-10-21 12:16:13 +02:00
shypike
e1fd40b34d OSX: Retina compatible menu-bar icons. 2012-10-20 19:57:11 +02:00
ShyPike
bc1f8f97a8 Prefix categories of nzbxxx.com with "XXX:". 2012-10-20 16:03:09 +02:00
shypike
b51705f458 Fix issues with accented and special characters in names of downloaded files.
name_extractor() returned Unicode instead of platform-compatible encoding.
QuickCheck assumed incorrectly that file names are not yet platform-compatible.
2012-10-20 15:11:08 +02:00
ShyPike
aaed5f4797 Adjust nzbmatrix category table. 2012-10-17 21:30:22 +02:00
ShyPike
a8eedef1d2 Prevent stuck jobs at end of pre-check. 2012-10-17 21:22:36 +02:00
shypike
9407e21e1e Prevent unusual SFV files from crashing post-processing. 2012-10-13 10:56:05 +02:00
ShyPike
ba6dcfd467 Don't show speed and ETA when download is paused during post-processing. 2012-10-08 21:21:15 +02:00
shypike
e2c1de5008 Prevent soft-crash when api-function "addfile" is called without parameters. 2012-10-06 21:57:48 +02:00
ShyPike
6737704b15 Update version to 0.8.x 2012-10-03 21:12:47 +02:00
ShyPike
481308e365 Merge branch 'master' into 'develop' at 0.7.4 release. 2012-10-03 21:04:31 +02:00
ShyPike
10b7403748 Update text files for 0.7.4 Final. 2012-10-02 20:04:03 +02:00
ShyPike
1ba924cc12 Pre-queue script didn't get the show/season/episode information any longer.
Due to earlier changes in tvsort, the pre-queue script handler now needs to
call an extra method of the SeriesSorter.
2012-10-02 19:56:47 +02:00
ShyPike
11eb034bd3 Prevent crash on startup when a fully downloaded job is still in download queue.
When at startup, there's a job in the download which has no more file to download,
it will be passed to post-processing. However the queue logic will try do disconnect
all servers first. This call will fail because the downloader hasn't start yet.
2012-10-02 19:34:21 +02:00
ShyPike
c3250e15cb Update text files for 0.7.4 Final. 2012-10-01 21:25:19 +02:00
shypike
8ff8a59b4c Update translations 2012-10-01 21:02:44 +02:00
ShyPike
0c646d88b2 New RSS feed should no longer be considered new after first, but empty readout.
Search and bookmark feeds can be empty when entered into SABnzbd.
The "don't download first batch" rule should be discarded when the
first readout is empty (as expected).
Currently the feed remains "new" until the first content is found.
2012-10-01 19:53:22 +02:00
ShyPike
05670ea599 Make "auth" call backward-compatible with 0.6.x releases.
Return "apikey" when no key is sent, instead of "badkey".
2012-09-30 23:17:05 +02:00
ShyPike
e25eb32885 Config->Notifications: email and growl server addresses should not be marked as "url" type. 2012-09-30 16:08:19 +02:00
shypike
250f75f084 OSX: fix top menu queue info.
The OSX queue menu entry shows a maximum of 10 jobs.
However, the counter should show the total amount of active jobs along with
the total size of these jobs.
Instead only the contribution of the 10 visible jobs was shown.
2012-09-29 15:21:30 +02:00
ShyPike
cdd39e6777 Plush: Purge history will now use the active filter.
When selecting one of the buttons in the Purge History dialog will
now use the current filter to select jobs to be deleted.
This is more what people expect to happen.
2012-09-29 12:19:56 +02:00
ShyPike
281ed6766c Update text files for 0.7.4 RC2 2012-09-27 20:38:16 +02:00
shypike
cd78c89de1 Update translations 2012-09-27 20:37:14 +02:00
ShyPike
c6c983e8f2 Linux: add memory usage to status display and add special option to control display.
Show "Total Program size" (V) and "Resident set size" (R).
Option "show_sysload" has three values:
0 = Off
1 = Show CPU load
2 = Show CPU load + memory usage (default)
2012-09-26 20:44:18 +02:00
ShyPike
ef4d1ce54f Fix problems with timing of quota reset at end of period. 2012-09-25 22:51:36 +02:00
ShyPike
b1177f4265 Fix issue with queue and history not updating in Safari-IOS6.
Add "Cache-Control":"no-cache" header to each Ajax POST action.
2012-09-24 21:38:30 +02:00
shypike
02d373e4a6 Remove warning about Growl when user has disabled message class in Growl itself.
Growl 2.0 uses different text than 1.4.
Test is now compatible with both versions.
2012-09-24 17:31:41 +02:00
shypike
58c8608667 Pre-check didn't check all available par2 files.
Result was that any post with a missing article was rejected.
Regression error introduced when fixing stalling fetches of extra par2 files.
2012-09-23 14:35:27 +02:00
ShyPike
848110ac3e Fix transmission of Growl icon when running on Linux/Unix.
There's a bug in the GNTP library that prevents sending the icon as
a binary attribute along with the registration.
Work-around is to send an URL to an external location.
2012-09-22 14:37:21 +02:00
ShyPike
74d677cf09 Update text files for 0.7.4 RC1. 2012-09-22 10:19:15 +02:00
ShyPike
f029c4eb4f Add a default limit to API-call "history" in order to prevent over-sized output.
Some external tools query the full history repeatedly.
Add a default value for the API call "history" as a work-around.
Calls that have a start and limit parameter will work unchanged.
The default limit can be set in the Special "history_limit".
2012-09-22 10:16:31 +02:00
ShyPike
838811f085 Update text files for 0.7.4 RC1 2012-09-21 20:50:40 +02:00
ShyPike
e212ec7ca3 Remove potential stalling when fetching extra par fetches.
Assembler didn't set the "completed" flag on a finished file,
leading to potential deadlock when fetching extra par2 files.
2012-09-21 20:47:25 +02:00
ShyPike
6314a536af Windows: be less eager to run par2-classic instead of the multi-core version. 2012-09-21 20:46:43 +02:00
shypike
df33765ce0 Update translations 2012-09-21 20:39:20 +02:00
ShyPike
2524333e79 Update text files for 0.7.4 RC1 2012-09-20 21:04:42 +02:00
ShyPike
cf81e815ee Classic and Plush skins must specify their character encoding.
Some browsers get it wrong when they need to guess the encoding.
Added:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
2012-09-19 20:29:26 +02:00
ShyPike
7bd7f1826c Win32: fix display of system name in startup notification message. 2012-09-19 20:00:19 +02:00
shypike
9ce7b528e3 Improved OSX DMG template. 2012-09-19 19:49:41 +02:00
shypike
db7b3cf0b5 Update translations 2012-09-18 21:52:26 +02:00
ShyPike
201b30b6c4 Fix packaging for Windows and add tar.gz for OSX again. 2012-09-18 20:47:57 +02:00
ShyPike
36a6bcd1f5 Update text files for 0.7.4 Beta3 2012-09-17 21:00:37 +02:00
shypike
20cfca5bb6 Combine OSX builds into one DMG image.
Split DMG creation off into a separate file.
`package.py` will now just build one OSX platform.
`make_dmg.py` will combine the three builds into one DMG.
2012-09-17 20:55:46 +02:00
ShyPike
63fe4b15c8 Show correct base folder for HTTPS certificate files (corrected). 2012-09-17 19:43:31 +02:00
ShyPike
aabc57a1f8 Show correct base folder for HTTPS certificate files. 2012-09-17 19:34:19 +02:00
ShyPike
9a8eca0993 Some code cleanup. 2012-09-17 19:16:22 +02:00
ShyPike
f8723d7e52 Prevent potential crash due to missing attribute in NzfObject. 2012-09-14 22:10:03 +02:00
ShyPike
59bb5528ed Ignore new schedule without days instead of crashing on it. 2012-09-14 21:46:36 +02:00
ShyPike
247c10692a Put "missing ssl support" warning in Wizard page One too. 2012-09-10 22:09:40 +02:00
shypike
63bed3c127 Update text files for 0.7.4Beta2 2012-09-10 19:27:58 +02:00
shypike
ca96743bad Update translations 2012-09-10 19:07:53 +02:00
shypike
525fb4de61 Update POT file, for new texts. 2012-09-10 19:06:54 +02:00
shypike
f294084dbc Build three separate DMG files for OSX.
Lion and MountainLion use the most recent native Python.
Leopard/SnowLeopard uses ActiveState Python 2.6.3.
Use a separate template for each OS version.
2012-09-09 22:19:10 +02:00
shypike
ca1327a9ae Improve handling of badly formatted subject lines in NZBs.
The NZB parser overwrote the subject even when there was no proper filename extracted.
This compromized recognition of par2 files after a first failed verification run.
Improve ability to extract a proper file name so that sorting and displaying is better.
The extraction still won't work for every malformed subject, but at least the par2 recognition should always work now.
2012-09-09 14:49:30 +02:00
shypike
dcb1b0b3dc Improve startup notification.
- Move hostname() from misc.py to growler.py.
- Improve startup notification
2012-09-08 21:00:28 +02:00
shypike
ec4b613498 Modify growler.py for new GNTP module. Fix transmission of icon.
- use 'localhost' for local Growl instead of "None".
- Sending binary icon now works properly, so external icon not needed any more.
- No need to send icon with every message, only when registering.
- Log more info about gntp-error exceptions
2012-09-08 20:23:59 +02:00
shypike
c3f4eccfbc Re-apply custom patches to new GNTP.
Reduce logging verbosity for sent messages.
Convert "info" logging to "debug" logging.
Suppress 404 + "user has disabled" error (which isn't really an error).
Time-out patch no longer needed, built-in now.
2012-09-08 20:18:13 +02:00
shypike
1aafe25a83 Update GNTP module.
Version 0.8 from github.
2012-09-08 19:52:46 +02:00
shypike
cc25ef0af0 Prevent filing an error message when the user has disabled message classes in Growl-preferences.
Problem is in the GNTP library, while at the same time it is very odd that Growl sends an error response for this.
2012-09-08 15:45:17 +02:00
shypike
5c221f4a14 After successful pre-check, preserve a job's spot in the queue.
Previously a pre-checked job re-entered at the end of the queue.
Moved the quality check from the post-processor to nzbqueue, so a successful job doesn't need to go through the post-processor.
Improvement: all other ways for a job to be sent to post-processing need to go through the end_job() method.
2012-09-08 15:01:14 +02:00
shypike
03221fc645 Config skin: "check new releases" setting did not work properly. 2012-09-08 09:46:40 +02:00
ShyPike
febf81e597 Update text files for 0.7.4Beta1 2012-09-06 21:53:52 +02:00
shypike
447ec55822 OSX: create one build for ML and one for others.
Also remove 64bit code from binaries.
2012-09-06 21:51:12 +02:00
ShyPike
eee1f49c4a Fix translation of notification classes in Config->Notification. 2012-09-05 20:00:33 +02:00
shypike
140b903783 Update translations 2012-09-05 19:43:57 +02:00
ShyPike
8361bc9f3a A job with priority "forced" should keep that when fetching more par2 files.
It was now set to "repair" priority, which doesn't ignore paused state.
Other priorities will still be promoted to "repair".
2012-09-04 19:10:13 +02:00
ShyPike
744290c228 Log failed attempts to login to the Web UI. 2012-09-03 23:18:08 +02:00
ShyPike
20768df430 Remove leak of SQLite database handles.
The non-UI threads need to close the db handles after using.
Move the unnecessary proxy for the database sizes for the bpsmeter.
2012-09-03 22:35:41 +02:00
ShyPike
7ec7e8d432 Allow compression of more MIME type in API-calls. 2012-09-03 19:30:29 +02:00
shypike
7b657a85ba Update POT files. 2012-09-01 13:23:04 +02:00
ShyPike
258699f1db Add scheduled task "Remove failed jobs". 2012-09-01 12:43:33 +02:00
Lucas Parry
0412f45323 Make scheduler more flexible
Schedules can now be set for any arbitrary group of days of the week.
2012-09-01 12:09:11 +02:00
shypike
997eb93cd9 Don't do an SFV-based verification when the job is already marked as verified.
When retrying a job that has passed succesful verification, the par2 files will already have been removed. When doing an SFV-based verification, this may lead to errors because the PAR2 files are gone. So when the "__verified__" flag-file is present and there's no par2-set, skip the SFV check.
2012-09-01 11:42:41 +02:00
shypike
b94192486b Add special option "overwrite_files".
This will overwrite existing files in the "complete" folders.
Useful for effective in "Single folder" mode and "Season Sorting".
2012-08-29 22:08:07 +02:00
shypike
73bdd2c5bf Prevent error and pause when last file of a removed job happens to be written to (and fails). 2012-08-25 12:49:07 +02:00
shypike
56e9b54cd9 Add parameter "pp_active" to the "qstatus" api call, showing state of Postprocessing. 2012-08-25 11:13:51 +02:00
shypike
3308074f81 Don't also show job in post-processing queue while fetching more par2 files. 2012-08-23 22:24:46 +02:00
shypike
c2305034a1 Handle unexpected missing par2 file in par2-check properly. 2012-08-23 20:06:44 +02:00
shypike
d77b22be37 Add support for HTTPS chain certificate file.
Just enabling the user to specify the file and then passing it on to CherryPy.
2012-08-18 14:51:21 +02:00
ShyPike
9717912ff7 When a numeric option has no value, it should be set to the default and not to the minimum value. 2012-08-16 13:47:29 +02:00
ShyPike
cfa79c08b2 When retrying post processing don't attempt to download existing par2 files again. 2012-08-16 12:10:48 +02:00
ShyPike
4f65f87ad6 Fix syntax of command used in server test. 2012-08-16 11:37:57 +02:00
shypike
aea0d21fd2 Log the output of the PAR2 command.
This will help diagnosing repair trouble that users might have.
2012-08-15 19:17:43 +02:00
shypike
ba77b43364 Default for Growl is off when on MountainLion. 2012-08-15 18:36:41 +02:00
shypike
7727eb58a8 Sort 'Done' list on RSS page in reverse time order (latest first). 2012-08-15 18:31:16 +02:00
shypike
dbd2e3f54b Make list of RSS sites that use weird titles a "Special" option, "misc, rss_odd_titles". 2012-08-15 18:13:41 +02:00
shypike
7ac6e07576 Update translatable texts 2012-08-14 20:42:34 +02:00
shypike
24ffd90fb4 sleepless.keep_awake() call needs a Unicode argument 2012-08-14 20:37:57 +02:00
shypike
011b680337 Fix path for OSX-Leopard build 2012-08-11 17:18:22 +02:00
shypike
5328e07a93 OSX: add notifier package and licencse files to DMG. 2012-08-11 14:37:37 +02:00
shypike
2d07af7cc8 Add parameter to sleepless.keep_awake() call. 2012-08-11 13:23:32 +02:00
shypike
a40d2da2ab Improve OSX Mountain Lion "stay awake" support with native OS calls.
Also add Special option "keep_awake" for Windows and OSX.
2012-08-10 22:49:41 +02:00
shypike
25ac101751 Add OSX Notification Center support.
Add option when support available on OSX ML.
Add class selection for notifications. Useful for N-Center and NtfOSD.
2012-08-09 22:55:07 +02:00
ShyPike
66f50f9f9d Merge in 0.7.3 from master branch. 2012-08-04 13:49:44 +02:00
ShyPike
a666165a5e Plush: remove mailto link in Help pop-up. 2012-08-04 13:44:22 +02:00
ShyPike
e24aedc6ac Update text files for 0.7.3 Final. 2012-08-04 11:05:58 +02:00
ShyPike
d10d69e44b Correct typo in dirscanner.py. 2012-08-04 10:54:40 +02:00
ShyPike
af5c01ee3a Update Windows install size. 2012-08-03 22:58:34 +02:00
ShyPike
bf350cddc8 Update text files for 0.7.3
Fix package.py for README.mkd handling.
2012-08-03 22:28:44 +02:00
ShyPike
9bc4d909b5 Use 'pandoc' to generate README.rtf from README.mkd. 2012-08-03 21:05:08 +02:00
ShyPike
42236de5bd Update text files for 0.7.3 Final 2012-08-03 19:41:17 +02:00
ShyPike
02e2fe2cc8 Correct errors in Danish email templates. 2012-08-03 19:27:30 +02:00
ShyPike
b75bcb90f4 Server 502 message about expired subscription will now block server. 2012-08-03 19:08:21 +02:00
shypike
56b88eb406 Update translations 2012-08-03 16:01:20 +02:00
ShyPike
93741ea9ab SFV-check: don't issue a warning for each problem file, but just list in History entry. 2012-08-03 11:57:13 +02:00
ShyPike
f7509132fc Rename 'random_server_ip' to 'randomize_server_ip' and make the default 'False'.
This is to avoid confusing speed loss with some providers. Now the user has to enable this consciously.
2012-08-02 21:18:03 +02:00
ShyPike
2d0d62ec00 The "watched folder" scan should ignore all files starting with a period (hidden on Linux and OSX). 2012-08-01 00:27:23 +02:00
ShyPike
4e07a84102 Update POT files. 2012-08-01 00:25:35 +02:00
ShyPike
32a048a879 Update text files for 0.7.3 Beta2 2012-07-30 16:08:48 +02:00
shypike
caac28fcbc Try to keep OSX Mountain Lion awake as long as downloading/postprocessing runs.
Quick & Dirty solution by launching 'caffeinate' every 5 minutes.
2012-07-30 15:40:13 +02:00
ShyPike
5b0bbf57c6 Prevent queue deadlock in case of fatally damaged par2 files.
Basic problem: prevent par2 files that have already been downloaded from re-entering the queue.
The issue was introduced after removing the habit of 0.6.x to download a part of each par2 file.
The advantage of the 0.6.x method was that more was known about the par2 files,
the disadvantage that quite some data was downloaded that was never used.

Add job flag that prevents further QuickCheck attempts on known to be damaged jobs.
The method of inspecting large par2 files is time-consuming if nothing is ever found.
2012-07-30 12:41:55 +02:00
ShyPike
072af938c2 Prevent deadlock in fetching more par2 files when first par2 file is so corrupt that no info can be retrieved. 2012-07-29 14:48:27 +02:00
ShyPike
9e8202371e Add filter-enable check-boxes to Classic, Plush and smpl skinms.
Necessary because new filters created with those skins were not enabled.
2012-07-29 00:09:53 +02:00
ShyPike
27dd253c5d Fix UI crash on saving parameters of an already enabled server in Config->Servers. 2012-07-28 23:35:12 +02:00
ShyPike
1aef323e7b Set bandwidth as a percentage of the maximum bandwidth.
Both to be set by the user.
Add max bandwidth to the Wizard.
Give warning when user sets bandwidth percentage without setting a max bandwidth.
2012-07-28 17:04:32 +02:00
ShyPike
8d651af2f8 Add some forward compatibility with changes coming to Scheduler in 0.8.x 2012-07-28 14:05:27 +02:00
ShyPike
0cc7812e1f Treat 7zip archives as an NZB source (like ZIPs and RARs). 2012-07-28 12:57:39 +02:00
shypike
9957660e43 Make osx/7zip/7za executable. 2012-07-27 20:24:20 +02:00
ShyPike
f27c16ea9f Support for unpacking 7Zip files.
Same password support as for unrar.
Can be enabled and disabled.
Includes 7za 9.20 for Windows and OSX.
2012-07-27 20:15:53 +02:00
ShyPike
22a1051a19 Merge branch '0.7.x' into develop 2012-07-26 16:27:41 +02:00
ShyPike
6358312272 Replaced a few missed strings to be substituted with constants.Status values. 2012-07-24 21:35:03 +02:00
ShyPike
42c8367e13 Extend "check for release" with option "Also test releases". 2012-07-24 20:38:44 +02:00
ShyPike
08139bc808 Merge branch '0.7.x' into develop 2012-07-24 16:03:13 +02:00
ShyPike
c1e38b5e81 Update text files for 0.7.3 Beta1 2012-07-24 15:06:49 +02:00
ShyPike
1b4ce24037 Config->Servers: optimize layout 2012-07-24 11:17:40 +02:00
ShyPike
cf440750b6 Config->Servers: hide server details initially and have a simple checkbox to enable/disable servers quickly. 2012-07-24 09:14:02 +02:00
ShyPike
30c480df36 Add unofficial support for nzbmatrix's adult cousin. 2012-07-23 14:29:02 +02:00
ShyPike
9c3dbd39ef Make detection of samples less aggressive. 2012-07-22 16:35:34 +02:00
ShyPike
1af2f92828 Improve the Sort functions.
- Renaming secondary files after the main file didn't always work
- Collapsing folder structures could fail
- Joined (instead of unpacked) media files were not seen as candidates for renaming
- Presence of DVD/BRD special folders will now block collapsing
- Non-functional improvements: coding std, removed unused code and needless access functions, added doc-strings.
2012-07-22 15:43:26 +02:00
ShyPike
77f3eb8b1c Merge branch '0.7.x' into develop 2012-07-20 19:43:35 +02:00
ShyPike
45277bb00f Add another keyword for canceled article detection. 2012-07-20 19:31:31 +02:00
ShyPike
10e21a3af9 Remove extra '\r' in Windows logging. 2012-07-20 19:16:35 +02:00
ShyPike
19cbadd420 Plush: fix odd formatting of multi-line warnings on Status page. 2012-07-20 19:11:52 +02:00
ShyPike
a28cbe52b9 Recovery from corrupt totals9.sab doesn't always remove bad entry. 2012-07-19 21:00:01 +02:00
ShyPike
c78250fae8 Merge branch 'master' into develop 2012-07-18 18:35:10 +02:00
shypike
37f1d64e46 Fix incorrect explanation of --no_ipv6 flag. 2012-07-18 18:00:39 +03:00
ShyPike
cadb4afe7c Merge from master (release 0.7.1 Final). 2012-07-06 21:29:33 +02:00
shypike
e52326f4ac Update translations 2012-06-23 12:15:54 +02:00
ShyPike
11b705fb78 Windows: the installer did not set an icon for NZB files (association). 2012-06-13 19:40:38 +02:00
Lucas Parry
172813bcd5 Make scheduler more flexible
Schedules can now be set for any aribtary group of days of the week.
2012-06-12 23:31:32 +02:00
ShyPike
dcba1877a8 Config-skin: "server" field in Config->Servers should have html5 tag "text" instead of "url". 2012-06-12 19:24:05 +02:00
ShyPike
a69120d25a Modify server test to please very critical Usenet server. 2012-06-10 20:01:00 +02:00
511 changed files with 113240 additions and 62882 deletions

13
.gitignore vendored
View File

@@ -1,5 +1,5 @@
#Compiled python
*.py[co]
*.py[cod]
# Working folders for Win build
build/
@@ -10,19 +10,20 @@ srcdist/
# Generated email templates
email/*.tmpl
# Romanian ro.po is generated from ro.px, due to mapping to latin-1
ro.po
# Build results
SABnzbd*.zip
SABnzbd*.exe
SABnzbd*.gz
SABnzbd*.dmg
# WingIDE project file
*.wpr
# WingIDE project files
*.wp[ru]
# General junk
*.keep
*.bak
*.log
# Some people use Emacs as an editor
\#*
.\#*

View File

@@ -1,5 +1,5 @@
*******************************************
*** This is SABnzbd 0.7.2 ***
*** This is SABnzbd 1.2.x ***
*******************************************
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
@@ -10,47 +10,22 @@ SABnzbd also has a fully customizable user interface,
and offers a complete API for third-party applications to hook into.
There is an extensive Wiki on the use of SABnzbd.
http://wiki.sabnzbd.org/
IMPORTANT INFORMATION about release 0.7.0:
http://wiki.sabnzbd.org/introducing-0-7-0
https://sabnzbd.org/wiki/
IMPORTANT INFORMATION about release 1.x.x:
https://sabnzbd.org/wiki/new-features-and-changes
Please also read the file "ISSUES.txt"
*******************************************
*** Upgrading from 0.6.x ***
*******************************************
Stop SABnzbd.
Install new version
Start SABnzbd.
*******************************************
*** Upgrading from 0.5.x ***
*******************************************
Stop SABnzbd.
Uninstall current version, keeping the data.
Install new version
Start SABnzbd.
The organization of the download queue is different from 0.5.x.
0.6.x will finish downloading an existing queue, but you
cannot go back to an older version without losing your queue.
The organization of the download queue is different from 0.7.x (and older).
1.0.0 will not finish downloading an existing queue.
Also, your sabnzbd.ini file will be upgraded, making it
incompatible with release 0.5.x
incompatible with older releases.
*******************************************
*** Upgrading from 0.4.x ***
*** Upgrading from 0.7.x and below ***
*******************************************
>>>>> PLEASE DOWNLOAD YOUR CURRENT QUEUE BEFORE UPGRADING <<<<<<
When upgrading from a 0.4.x release such as 0.4.12 your old settings will be kept.
You will however be given a fresh queue and history. If you have items in your queue
from the older version of SABnzbd, you can either re-import the nzb files if you kept
an nzb backup folder, or temporarily go back to 0.4.x until your queue is complete.
The history is now stored in a better format meaning future upgrades should be backwards
compatible.
Empty your current queue
Stop SABnzbd.
Install new version
Start SABnzbd.

View File

@@ -1,205 +0,0 @@
-------------------------------------------------------------------------------
0.7.2Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix for NZB-icon issue when 0.7.0 was previously installed
- Check validity of totals9.sab file
- Fix startup problem when localhost has unexpected order of IP addresses
-------------------------------------------------------------------------------
0.7.2RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Improve support for nzbsrus.com
- Don't try to show NZB age when not known yet
- Prevent systems with unresolvable hostnames from always using 0.0.0.0
-------------------------------------------------------------------------------
0.7.2RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix fatal error in nzbsrus.com support
- Initial "quota left" was not set correctly when enabling quota
- Report incorrect RSS filter expressions (instead of aborting analysis)
- Improve detection of invalid articles (so that backup server will be tried)
- Windows installer: improve NZB association so that a reboot isn't needed
- Windows installer: don't remove settimngs by default when uninstalling
- Fix sorting of rar files in job so that .rar preceeds .r00
-------------------------------------------------------------------------------
0.7.1Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Disable VC90 check in Windows Installer as long as we're still on Python 2.5
- Windows: make sure \\server\share notation is never seen as a relative path
-------------------------------------------------------------------------------
0.7.1RC5 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix signing of OSX DMG
- Fix endless par2-fetch loop after retrying failed job
- Don't send "bad fetch" email when emailing is off
- Add some support for nzbrus.com's non-VIP limiting
-------------------------------------------------------------------------------
0.7.1RC4 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix failure to grab NZBs from indexers that send compressed files.
-------------------------------------------------------------------------------
0.7.1RC3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed stalling par2 fetches (after first verification run)
- Fixed retry behaviour of NZB fetching from URL
and add handling of nzbsrus.com error codes
- Make sure that all malformed articles are retried on another server
- Add no_ipv6 option that suppresses listing on ::1
(to be used if your system cannot handle that)
- Prevent crash in QuickCheck when expected par2 file wasn't downloaded
- Verification/repair would not be executed properly when one more RAR files
missed their first article.
- API calls "addurl" and "addid" (newzbin) can be used interchangeably
(Fixes a problem in Qouch)
-------------------------------------------------------------------------------
0.7.1RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Improved backup of sabnzbd.ini file
Will use backup when original is gone or become corrupt
- Windows: Using ::1 as single webhost address would start IE instead of default browser
-------------------------------------------------------------------------------
0.7.1RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Plush skin: fix problems with pull-down menus in Mobile Safari
- On some Linux and OSX systems using localhost would still make SABnzbd
give access to other computers
- Windows: the installer did not set an icon when associating NZB files with SABnzbd
- Fix problem that the Opera browser had with Config->Servers
- Retry a few times when accessing a mounted drive to create the
final destination folder
- Reduce load caused by WinTray and OSX topmenu
-------------------------------------------------------------------------------
0.7.0Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Updated translations
-------------------------------------------------------------------------------
0.7.0RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Suppress permission errors on paths containing ".AppleDouble" or ".DS_Store"
(Required for NAS systems that support Apple AFP shares)
- OSX/Windows: Set article cache to 200M when not already set.
- Pre-check: lower default minimum completion rate to 100.2%
-------------------------------------------------------------------------------
0.7.0RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix for rare crash in par2 fetching
- Another /nomedia fix
- Quota reset wasn't done when quota-reset-time was passed while SABnzbd wasn't running.
- Pre-check: required ratio for NZB without par2 files should be 100%
and not the "safe" ratio
-------------------------------------------------------------------------------
0.7.0Beta8 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Disabled the .nomedia marker file feature.
Those who want to try it, use the "nomedia_marker" setting in Config->Special
It remains an experimental feature without guarantees
- Add missing info in email about failed pre-check
- Updated translations
-------------------------------------------------------------------------------
0.7.0Beta7 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix for .nomedia files not being deleted
- Fix NZB re-queueing (due to .nomedia remaining)
- Polish was missing in Windows installer and Dutch was incorrect
- When Sort renames auxillirary files, it should disregard case
- Fix crash in Wizard on some Linux systems
-------------------------------------------------------------------------------
0.7.0Beta6 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Upgrade unzip for Windows to 6.00 (supports ZIPs above 2G)
- Lower threshold for pre-check to 100.5%
- Fix removal of .nomedia file when using Sorting
- Add Polish translation (using reduced character set)
- Extension-based cleanup list now also removes extension-only files like ".sfv".
- Several small issues
-------------------------------------------------------------------------------
0.7.0Beta5 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Solved serious connection problem with some providers
- Windows Tray has the "restart" entries no under a Troubleshoot menu
- Fix newzbin entries in History's "Source" field
- During unpacking the destination folder will contain a ".nomedia" file
which will keep mediaplayers temporarily from indexing
- Pre-check jobs now require 101% completion rate (with a "special" parameter)
- Unified OSX DMG
-------------------------------------------------------------------------------
0.7.0Beta4 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Add Portuguese (Brazil) language
- Updated translations
- Some odd NZB files led to blank initial filenames in file overview
- Jobs that have 99.91%-99.99% completion rate should not be rounded to 100.0%
- Windows Tray icon now has entry to show "complete" folder
- Some minor fixes in code and Config skin
- Individual RSS filter toggle
-------------------------------------------------------------------------------
0.7.0Beta3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- OSX/Linux: permissions are now also applied to the "temporary download folder"
- Fix some issues in the Config skin.
- The default for "apply max retries only on optional servers" is now 0,
thus enabling the new anti-deadlock behaviour for all servers
- Fix incompatibility with nzbsa.co.za indexer
- Log all API calls (in debug mode)
- Restore Python2.5 compatibility in growler.py
- After a language change, register again with Growl
- Clean up the api-call auth. It will now give preference to 'apikey'.
- Fix detection of retry-able history entries for case-insensitive file systems.
- API-calls "addfile" and "addlocalfile" returned an incorrect status value.
- Add support for the peculiar Usenet provider "free.xsusenet.com".
- OSX menu now uses the same formatting for speed as the skins.
- Accept multiple items in API-calls "addurl" and "addid".
The "name" and "nzbname" keywords can be repeated.
-------------------------------------------------------------------------------
0.7.0Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix behavior when using host address 0.0.0.0 on a system
that doesn't resolve localhost properly
- Add Spanish translation
-------------------------------------------------------------------------------
0.7.0Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Updated nzbmatrix categories
- "Special" option allows incomplete/partial NZB files
- Forbid "complete" being a subfolder of "incomplete"
-------------------------------------------------------------------------------
0.7.0Alpha3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix failing join-by-par2
- Prevent API crash when deleting non-existing history item
- Prevent UI crash message when looking at NZB details page of finished job
- Config skin: fix path complettion in Config->Folders
- Config skin: fixes to support "hide behind proxy"
- Keep using unrar 4.10 for OSX Leopard and older, due to PPC support
-------------------------------------------------------------------------------
0.7.0Alpha2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix disabled options in Config skin
- Remove flags from the Wizard and Config skin
- Replace real spaces in RSS-urls with %20
- Prevent double entries in History's "Source" section
- Prevent crash when OSDNotify doesn't work properly
- Small improvents in Windows installer
-------------------------------------------------------------------------------
0.7.0Alpha1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Not tracked
-------------------------------------------------------------------------------
0.6.15Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Flag post-processing as failed when files cannot be moved/copied to destination
- Fixed another newzbin link
-------------------------------------------------------------------------------
0.6.15RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Change newzbin URL
- Prevent setting watched-folder speed to 0 (while having no watched-folder)
from triggering an inifinite loop.
- Move "locale" construction from Plush skin to Python code.
Some embedded Linux platforms show unstable behavior with the original construction.
- Extend OSX menu with troubleshooting options
- Add trailing slashes to internal Plush paths to support reverse proxies better.
- Ignore whitespace around regular expressions in RSS filters.
- Prevent crash on restoring URL-fetches when using --repair-all option
- Fix "Repair" button on smpl Connection page. Current path fails when using a reverse proxy
- Suppress "incompatible feed" error when doing a scheduled/automatic RSS read-out.
- Add special setting to use "pickle" library instead of cPickle.

View File

@@ -1,5 +1,5 @@
(c) Copyright 2007-2012 by "The SABnzbd-team" <team@sabnzbd.org>
(c) Copyright 2007-2017 by "The SABnzbd-team" <team@sabnzbd.org>
The SABnzbd-team is:
@@ -7,6 +7,7 @@ Active team:
ShyPike <shypike@sabnzbd.org>
inpheaux <inpheaux@sabnzbd.org>
zoggy <zoggy@sabnzbd.org>
Safihre <safihre@sabnzbd.org>
Sleeping members
sw1tch <switch@sabnzbd.org>
pairofdimes <pairofdimes@sabnzbd.org>
@@ -15,18 +16,20 @@ Honorary member (and original author)
Gregor Kaufmann <tdian@users.sourceforge.net>
The main contributors and moderators of the translations
Danish: Rene (nordjyden6)
Dutch: ShyPike
French : rAf and Fox Ace
German: Severin Heiniger
Norwegian: Protx
Danish: Rene (nordjyden6), Scott
Dutch: ShyPike, Safihre
French : rAf, Fox Ace, Fred, Morback, Jih
German: Severin Heiniger, Tim Hartmann, DonPizza, Alex
Norwegian: Protx, mjelva, TomP, John
Romanian: nicusor
Serbian: Ozzii
Swedish: Malmis
Spanish: Syquus
Portuguese (Brazil): lrrosa
Serbian: Ozzii, Krišan Darko
Swedish: Malmis, Kim Joahnsson, Patrik-liind, Chris M
Spanish: Syquus, Adolfo Jayme
Portuguese (Brazil): lrrosa, diegosps
Russian: Pavel Maryanov
Polish: Tomasz 'Zen' Napierala
Chinese: XsLiDian
Finnish: Matti Ylönen
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License

View File

@@ -1,10 +1,10 @@
SABnzbd 0.7.2
SABnzbd 1.2.1
-------------------------------------------------------------------------------
0) LICENSE
-------------------------------------------------------------------------------
(c) Copyright 2007-2012 by "The SABnzbd-team" <team@sabnzbd.org>
(c) Copyright 2007-2017 by "The SABnzbd-team" <team@sabnzbd.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
@@ -25,7 +25,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-------------------------------------------------------------------------------
Just run the downloaded EXE file and the installer will start.
It's just a simple standard installer.
After installaton, find the SABnzbd program in the Start menu and start it.
After installation, find the SABnzbd program in the Start menu and start it.
Within 5-10 seconds your web browser will start and show the user interface.
Use the "Help" button in the web-interface to be directed to the Help Wiki.
@@ -39,6 +39,7 @@ Start the SABnzbd.exe program.
Within 5-10 seconds your web browser will start and show the user interface.
Use the "Help" button in the web-interface to be directed to the Help Wiki.
-------------------------------------------------------------------------------
3) INSTALL pre-built OSX binaries
-------------------------------------------------------------------------------
@@ -55,40 +56,38 @@ You need to have Python installed plus some non-standard Python modules
and a few tools.
Unix/Linux/OSX
Python-2.5, 2.6 or 2.7 http://www.python.org
Python-2.7.latest http://www.python.org (2.7.9+ recommended)
OSX Leopard/SnowLeopard
Python 2.6 http://www.activestate.com
OSX Lion Apple Python 2.7 (included in OSX)
OSX Mavericks or newer
Apple Python 2.7 Included in OSX (default)
Windows
Python-2.7.latest http://www.activestate.com
Python-2.7.latest http://www.python.org (2.7.9+ recommended)
PyWin32 use "pip install pypiwin32"
Essential modules
cheetah-2.0.1+ http://www.cheetahtemplate.org/ (or use "pypm install cheetah")
cheetah-2.0.1+ use "pip install cheetah"
par2cmdline >= 0.4 http://parchive.sourceforge.net/
http://chuchusoft.com/par2_tbb/index.html (multi-core)
Note: https://sabnzbd.org/wiki/configuration/1.2/switches#par2cmdline
unrar >= 5.00+ http://www.rarlab.com/rar_add.htm
Optional modules
unrar >= 3.90+ http://www.rarlab.com/rar_add.htm
unzip >= 5.52 http://www.info-zip.org/
yenc module >= 0.3 http://sabnzbd.sourceforge.net/yenc-0.3.tar.gz
http://sabnzbd.sourceforge.net/yenc-0.3-w32fixed.zip (Win32-only)
Optional modules Windows
pyopenssl >= 0.11 http://pypi.python.org/pypi/pyOpenSSL
(Binaries, including the OpenSSL libraries)
unzip >= 6.00 http://www.info-zip.org/
7zip >= 9.20 http://www.7zip.org/
yenc module >= 0.4 use "pip install yenc"
https://sabnzbd.org/wiki/installation/yenc-0.4_py2.7.rar (Win32-only)
openssl => 1.0.0 http://www.openssl.org/
v0.9.8 will work, but limits certificate validation
cryptography >= 1.0 use "pip install cryptography"
Enables certificate generation and detection of encrypted RAR-files
Optional modules Unix/Linux/OSX
pyopenssl >= 0.11 http://pypi.python.org/pypi/pyOpenSSL
openssl => v0.9.8g+ http://www.openssl.org/
Make sure the OpenSSL libraries match with PyOpenSSL
pynotify Should be part of GTK for Python support on Debian/Ubuntu
If not, you cannot use the NotifyOSD feature.
python-dbus Enable option to Shutdown/Restart/Standby PC on queue finish.
Embedded modules (only use the included version)
CherryPy-3.2 rev2138 with patches http://www.cherrypy.org
Embedded modules (preferably use the included version)
CherryPy-8.1.2 with patches http://www.cherrypy.org
Unpack the ZIP-file containing the SABnzbd sources to any folder of your liking.
@@ -98,6 +97,7 @@ Start this from a shell terminal (or command prompt):
Start this from a shell terminal (or command prompt):
python SABnzbd.py
Within 5-10 seconds your web browser will start and show the user interface.
Use the "Help" button in the web-interface to be directed to the Help Wiki.
@@ -125,7 +125,7 @@ may help you solve problems easier.
-------------------------------------------------------------------------------
Visit the WIKI site:
http://wiki.sabnzbd.org/
https://sabnzbd.org/wiki/
-------------------------------------------------------------------------------
@@ -134,4 +134,4 @@ Visit the WIKI site:
Several parts of SABnzbd were built by other people, illustrating the
wonderful world of Free Open Source Software.
See the licences folder of the main program and of the skin folders.
See the licenses folder of the main program and of the skin folders.

View File

@@ -12,34 +12,25 @@
Windows-only:
If you keep having trouble with par2 multicore you can disable it
in Config->Switches.
This will force the use of the old and tried, but slower par2-classic program.
This will force the use of the old and tried, but slower par2cmdline program.
- A bug in Windows 7 may cause severe memory leaks when you use SABnzbd in
combination with some virus scanners and firewals.
combination with some virus scanners and firewalls.
Install this hotfix:
Description: http://support.microsoft.com/kb/979223/en-us
Download location: http://support.microsoft.com/hotfix/KBHotfix.aspx?kbnum=979223&kbln=en-us
- Windows cannot handle pathnames longer than 254 characters.
Currently, SABnzbd doesn't handle this problem gracefully.
We have added the INI-only option "folder_length_max" in which you can set
a maximum folder name size.
For Windows the default is 128 and for others 256.
A quite safe value for Windows would be 64.
SABnzbd will take care of overlapping names.
See: http://wiki.sabnzbd.org/configure-special-0-7
- Some Usenet servers have intermittent login (or other) problems.
For these the server blocking method is not very favourable.
There is an INI-only option that will limit blocks to 1 minute.
no_penalties = 1
See: http://wiki.sabnzbd.org/configure-special-0-7
See: https://sabnzbd.org/wiki/configuration/1.2/special
- Some third-party utilties try to probe SABnzbd API in such a way that you will
often see warnings about unauthenticated access.
If you are sure these probes are harmless, you can suppress the warnings by
setting the option "api_warnings" to 0.
See: http://wiki.sabnzbd.org/configure-special-0-7
See: https://sabnzbd.org/wiki/configuration/1.2/special
- On OSX you may encounter downloaded files with foreign characters.
The par2 repair may fail when the files were created on a Windows system.
@@ -48,9 +39,9 @@
- On Linux when you download files they may have the wrong character encoding.
You will see this only when downloaded files contain accented characters.
You need to fix it yourself by running the convmv utility (availaible for most Linux platforms).
You need to fix it yourself by running the convmv utility (available for most Linux platforms).
Possible the file system override setting 'fsys_type' might be solve things:
See: http://wiki.sabnzbd.org/configure-special-0-7
See: https://sabnzbd.org/wiki/configuration/1.2/special
- The "Watched Folder" sometimes fails to delete the NZB files it has
processed. This happens when other software still accesses these files.
@@ -59,22 +50,28 @@
prevents the removal.
- Memory usage can sometimes have high peaks. This makes using SABnzbd on very low
memory systems (eg a SAN device) a challenge.
memory systems (e.g. a NAS device or a router) a challenge.
- SABnzbd is not compatible with some software firewall versions.
The Mircosoft Windows Firewall works fine, but remember to tell this
The Microsoft Windows Firewall works fine, but remember to tell this
firewall that SABnzbd is allowed to talk to other computers.
- When SABnzbd cannot send nofication emails, check your virus scanner,
firewall or securiry suite. It may be blocking outgoing email.
- When SABnzbd cannot send notification emails, check your virus scanner,
firewall or security suite. It may be blocking outgoing email.
- When you are using external drives or network shares on OSX or Linux
make sure that the drives are mounted.
The operating system wil simply redirect your files to alternative locations.
The operating system will simply redirect your files to alternative locations.
You may have trouble finding the files when mounting the drive later.
On OSX, SABnzbd will not create new folders in /Volumes.
The result will be a failed job that can be retried once the volume has been mounted.
- If you use a mounted drive as "temporary download folder", it must be present when SABnzbd
starts up. If not, SABnzbd will use the default location.
You can make SABnzbd wait for a mount of the "temporary download folder" by setting
Config->Special->wait_for_dfolder to 1.
SABnzbd will appear to hang until the drive is mounted.
- On some operating systems it looks like there is a problem with one of the standard Python libraries.
It is possible that you get errors about saving admin files and even unexplained crashes.
If so, you can enable the option for the alternative library.
@@ -84,4 +81,4 @@
- Squeeze Linux
There is a "special" option that will allow you to select an alternative library.
use_pickle = 1
See: http://wiki.sabnzbd.org/configure-special-0-7
See: https://sabnzbd.org/wiki/configuration/1.2/special

View File

@@ -1,4 +1,4 @@
(c) Copyright 2007-2012 by "The SABnzbd-team" <team@sabnzbd.org>
(c) Copyright 2007-2017 by "The SABnzbd-team" <team@sabnzbd.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License

View File

@@ -1,462 +0,0 @@
; -*- coding: latin-1 -*-
;
; Copyright 2008-2012 The SABnzbd-Team <team@sabnzbd.org>
;
; This program is free software; you can redistribute it and/or
; modify it under the terms of the GNU General Public License
; as published by the Free Software Foundation; either version 2
; of the License, or (at your option) any later version.
;
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
; GNU General Public License for more details.
;
; You should have received a copy of the GNU General Public License
; along with this program; if not, write to the Free Software
; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
!addplugindir win\nsis\Plugins
!addincludedir win\nsis\Include
!include "MUI2.nsh"
!include "registerExtension.nsh"
!include "FileFunc.nsh"
!include "LogicLib.nsh"
!include "WinVer.nsh"
!include "WinSxSQuery.nsh"
;------------------------------------------------------------------
;
; Marco for removing existing and the current installation
; It share buy the installer and the uninstaller.
; Make sure it covers 0.5.x, 0.6.x and 0.7.x in one go.
;
!define RemovePrev "!insertmacro RemovePrev"
!macro RemovePrev idir
Delete "${idir}\email\email-de.tmpl"
Delete "${idir}\email\email-en.tmpl"
Delete "${idir}\email\email-nl.tmpl"
Delete "${idir}\email\email-fr.tmpl"
Delete "${idir}\email\email-sv.tmpl"
Delete "${idir}\email\email-da.tmpl"
Delete "${idir}\email\email-nb.tmpl"
Delete "${idir}\email\email-pl.tmpl"
Delete "${idir}\email\email-ro.tmpl"
Delete "${idir}\email\email-sr.tmpl"
Delete "${idir}\email\email-es.tmpl"
Delete "${idir}\email\email-pt_BR.tmpl"
Delete "${idir}\email\email-sr.tmpl"
Delete "${idir}\email\email-ru.tmpl"
Delete "${idir}\email\rss-de.tmpl"
Delete "${idir}\email\rss-en.tmpl"
Delete "${idir}\email\rss-nl.tmpl"
Delete "${idir}\email\rss-pl.tmpl"
Delete "${idir}\email\rss-fr.tmpl"
Delete "${idir}\email\rss-sv.tmpl"
Delete "${idir}\email\rss-da.tmpl"
Delete "${idir}\email\rss-nb.tmpl"
Delete "${idir}\email\rss-ro.tmpl"
Delete "${idir}\email\rss-sr.tmpl"
Delete "${idir}\email\rss-es.tmpl"
Delete "${idir}\email\rss-pt_BR.tmpl"
Delete "${idir}\email\rss-sr.tmpl"
Delete "${idir}\email\rss-ru.tmpl"
Delete "${idir}\email\badfetch-da.tmpl"
Delete "${idir}\email\badfetch-de.tmpl"
Delete "${idir}\email\badfetch-en.tmpl"
Delete "${idir}\email\badfetch-fr.tmpl"
Delete "${idir}\email\badfetch-nb.tmpl"
Delete "${idir}\email\badfetch-nl.tmpl"
Delete "${idir}\email\badfetch-pl.tmpl"
Delete "${idir}\email\badfetch-ro.tmpl"
Delete "${idir}\email\badfetch-sr.tmpl"
Delete "${idir}\email\badfetch-sv.tmpl"
Delete "${idir}\email\badfetch-sr.tmpl"
Delete "${idir}\email\badfetch-es.tmpl"
Delete "${idir}\email\badfetch-pt_BR.tmpl"
Delete "${idir}\email\badfetch-ru.tmpl"
RMDir "${idir}\email"
RMDir /r "${idir}\locale"
RMDir /r "${idir}\interfaces\Classic"
RMDir /r "${idir}\interfaces\Plush"
RMDir /r "${idir}\interfaces\smpl"
RMDir /r "${idir}\interfaces\Mobile"
RMDir /r "${idir}\interfaces\wizard"
RMDir /r "${idir}\interfaces\Config"
RMDir "${idir}\interfaces"
RMDir /r "${idir}\win\curl"
RMDir /r "${idir}\win\par2"
RMDir /r "${idir}\win\unrar"
RMDir /r "${idir}\win\unzip"
RMDir /r "${idir}\win"
RMDir /r "${idir}\licenses"
RMDir /r "${idir}\lib\"
RMDir /r "${idir}\po\email"
RMDir /r "${idir}\po\main"
RMDir /r "${idir}\po\nsis"
RMDir "${idir}\po"
RMDir /r "${idir}\icons"
Delete "${idir}\CHANGELOG.txt"
Delete "${idir}\COPYRIGHT.txt"
Delete "${idir}\email.tmpl"
Delete "${idir}\GPL2.txt"
Delete "${idir}\GPL3.txt"
Delete "${idir}\INSTALL.txt"
Delete "${idir}\ISSUES.txt"
Delete "${idir}\LICENSE.txt"
Delete "${idir}\nzbmatrix.txt"
Delete "${idir}\MSVCR71.dll"
Delete "${idir}\nzb.ico"
Delete "${idir}\sabnzbd.ico"
Delete "${idir}\PKG-INFO"
Delete "${idir}\python25.dll"
Delete "${idir}\python26.dll"
Delete "${idir}\python27.dll"
Delete "${idir}\README.txt"
Delete "${idir}\README.rtf"
Delete "${idir}\ABOUT.txt"
Delete "${idir}\IMPORTANT_MESSAGE.txt"
Delete "${idir}\SABnzbd-console.exe"
Delete "${idir}\SABnzbd.exe"
Delete "${idir}\SABnzbd.exe.log"
Delete "${idir}\SABnzbd-helper.exe"
Delete "${idir}\SABnzbd-service.exe"
Delete "${idir}\Sample-PostProc.cmd"
Delete "${idir}\Uninstall.exe"
Delete "${idir}\w9xpopen.exe"
RMDir "${idir}"
!macroend
;------------------------------------------------------------------
; Define names of the product
Name "${SAB_PRODUCT}"
OutFile "${SAB_FILE}"
InstallDir "$PROGRAMFILES\SABnzbd"
InstallDirRegKey HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" ""
;DirText $(MsgSelectDir)
;------------------------------------------------------------------
; Some default compiler settings (uncomment and change at will):
SetCompress auto ; (can be off or force)
SetDatablockOptimize on ; (can be off)
CRCCheck on ; (can be off)
AutoCloseWindow false ; (can be true for the window go away automatically at end)
ShowInstDetails hide ; (can be show to have them shown, or nevershow to disable)
SetDateSave off ; (can be on to have files restored to their orginal date)
WindowIcon on
;------------------------------------------------------------------
; Vista/Win7 redirects $SMPROGRAMS to all users without this
RequestExecutionLevel admin
FileErrorText "If you have no admin rights, try to install into a user directory."
;------------------------------------------------------------------
;Variables
Var MUI_TEMP
Var STARTMENU_FOLDER
;------------------------------------------------------------------
;Interface Settings
!define MUI_ABORTWARNING
;Show all languages, despite user's codepage
!define MUI_LANGDLL_ALLLANGUAGES
!define MUI_ICON "interfaces/Classic/templates/static/images/favicon.ico"
;--------------------------------
;Pages
!insertmacro MUI_PAGE_LICENSE "LICENSE.txt"
!define MUI_COMPONENTSPAGE_NODESC
!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY
;Start Menu Folder Page Configuration
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU"
!define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\SABnzbd"
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "SABnzbd"
;Remember the installer language
!define MUI_LANGDLL_REGISTRY_ROOT "HKCU"
!define MUI_LANGDLL_REGISTRY_KEY "Software\SABnzbd"
!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language"
!insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER
!insertmacro MUI_PAGE_INSTFILES
!define MUI_FINISHPAGE_RUN
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
!define MUI_FINISHPAGE_RUN_TEXT $(MsgGoWiki)
!define MUI_FINISHPAGE_RUN_NOTCHECKED
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\README.txt"
!define MUI_FINISHPAGE_SHOWREADME_TEXT $(MsgShowRelNote)
!define MUI_FINISHPAGE_LINK $(MsgSupportUs)
!define MUI_FINISHPAGE_LINK_LOCATION "http://www.sabnzbd.org/contribute/"
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!define MUI_UNPAGE_COMPONENTSPAGE_NODESC
!insertmacro MUI_UNPAGE_COMPONENTS
!insertmacro MUI_UNPAGE_INSTFILES
;------------------------------------------------------------------
; Set supported languages
!insertmacro MUI_LANGUAGE "English" ;first language is the default language
!insertmacro MUI_LANGUAGE "French"
!insertmacro MUI_LANGUAGE "German"
!insertmacro MUI_LANGUAGE "Dutch"
!insertmacro MUI_LANGUAGE "Polish"
!insertmacro MUI_LANGUAGE "Swedish"
!insertmacro MUI_LANGUAGE "Danish"
!insertmacro MUI_LANGUAGE "NORWEGIAN"
!insertmacro MUI_LANGUAGE "Romanian"
!insertmacro MUI_LANGUAGE "Spanish"
!insertmacro MUI_LANGUAGE "PortugueseBR"
;------------------------------------------------------------------
;Reserve Files
;If you are using solid compression, files that are required before
;the actual installation should be stored first in the data block,
;because this will make your installer start faster.
!insertmacro MUI_RESERVEFILE_LANGDLL
;------------------------------------------------------------------
Function LaunchLink
ExecShell "" "http://wiki.sabnzbd.org/"
FunctionEnd
;------------------------------------------------------------------
Function .onInit
!insertmacro MUI_LANGDLL_DISPLAY
;--------------------------------
;make sure that the requires MS Runtimes are installed
;
goto nodownload ; Not needed while still using Python25
runtime_loop:
push 'msvcr90.dll'
push 'Microsoft.VC90.CRT,version="9.0.21022.8",type="win32",processorArchitecture="x86",publicKeyToken="1fc8b3b9a1e18e3b"'
call WinSxS_HasAssembly
pop $0
StrCmp $0 "1" nodownload
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgNoRuntime) /SD IDOK IDOK download IDCANCEL noinstall
download:
inetc::get /BANNER $(MsgDLRuntime) \
"http://download.microsoft.com/download/1/1/1/1116b75a-9ec3-481a-a3c8-1777b5381140/vcredist_x86.exe" \
"$TEMP\vcredist_x86.exe"
Pop $0
DetailPrint "Downloaded MS runtime library"
StrCmp $0 "OK" dlok
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgDLError) /SD IDCANCEL IDCANCEL exitinstall IDOK download
dlok:
ExecWait "$TEMP\vcredist_x86.exe" $1
DetailPrint "VCRESULT=$1"
DetailPrint "Tried to install MS runtime library"
delete "$TEMP\vcredist_x86.exe"
StrCmp $1 "0" nodownload
noinstall:
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgDLNeed) /SD IDOK IDOK runtime_loop IDCANCEL exitinstall
Abort
nodownload:
;------------------------------------------------------------------
;make sure user terminates sabnzbd.exe or else abort
;
loop:
StrCpy $0 "SABnzbd.exe"
KillProc::FindProcesses
StrCmp $0 "0" endcheck
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgCloseSab) /SD IDCANCEL IDOK loop IDCANCEL exitinstall
exitinstall:
Abort
endcheck:
FunctionEnd
;------------------------------------------------------------------
; SECTION main program
;
Section "SABnzbd" SecDummy
SetOutPath "$INSTDIR"
;------------------------------------------------------------------
; Make sure old versions are gone
IfFileExists $INSTDIR\sabnzbd.exe 0 endWarnExist
IfFileExists $INSTDIR\python27.dll 0 endWarnExist
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "$(MsgRemoveOld)$\n$\n$(MsgRemoveOld2)" IDOK uninst
Abort
uninst:
${RemovePrev} "$INSTDIR"
endWarnExist:
; add files / whatever that need to be installed here.
File /r "dist\*"
WriteRegStr HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" "" "$INSTDIR"
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayName" "SABnzbd ${SAB_VERSION}"
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayVersion" '${SAB_VERSION}'
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "Publisher" 'The SABnzbd Team'
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "HelpLink" 'http://forums.sabnzbd.org/'
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "URLInfoAbout" 'http://wiki.sabnzbd.org/'
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "URLUpdateInfo" 'http://sabnzbd.org/'
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "Comments" 'The automated Usenet download tool'
WriteRegStr HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "DisplayIcon" '$INSTDIR\interfaces\Classic\templates\static\images\favicon.ico'
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "EstimatedSize" 18400
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoRepair" -1
WriteRegDWORD HKEY_LOCAL_MACHINE "Software\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd" "NoModify" -1
; write out uninstaller
WriteUninstaller "$INSTDIR\Uninstall.exe"
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
;Create shortcuts
CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER"
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe"
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd - SafeMode.lnk" "$INSTDIR\SABnzbd.exe" "--server 127.0.0.1:8080 -b1 --no-login -t Plush"
WriteINIStr "$SMPROGRAMS\$STARTMENU_FOLDER\SABnzbd - Documentation.url" "InternetShortcut" "URL" "http://wiki.sabnzbd.org/"
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\Uninstall.lnk" "$INSTDIR\Uninstall.exe"
!insertmacro MUI_STARTMENU_WRITE_END
SectionEnd ; end of default section
Section /o $(MsgRunAtStart) startup
CreateShortCut "$SMPROGRAMS\Startup\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe" "-b0"
SectionEnd ;
Section $(MsgIcon) desktop
CreateShortCut "$DESKTOP\SABnzbd.lnk" "$INSTDIR\SABnzbd.exe"
SectionEnd ; end of desktop icon section
Section /o $(MsgAssoc) assoc
${registerExtension} "$INSTDIR\icons\nzb.ico" "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File"
${RefreshShellIcons}
SectionEnd ; end of file association section
; begin uninstall settings/section
UninstallText $(MsgUninstall)
Section "un.$(MsgDelProgram)" Uninstall
;make sure sabnzbd.exe isnt running..if so shut it down
StrCpy $0 "sabnzbd.exe"
DetailPrint "Searching for processes called '$0'"
KillProc::FindProcesses
StrCmp $1 "-1" wooops
DetailPrint "-> Found $0 processes"
StrCmp $0 "0" completed
Sleep 1500
StrCpy $0 "sabnzbd.exe"
DetailPrint "Killing all processes called '$0'"
KillProc::KillProcesses
StrCmp $1 "-1" wooops
DetailPrint "-> Killed $0 processes, failed to kill $1 processes"
Goto completed
wooops:
DetailPrint "-> Error: Something went wrong :-("
Abort
completed:
DetailPrint "Process Killed"
; add delete commands to delete whatever files/registry keys/etc you installed here.
Delete "$INSTDIR\uninstall.exe"
DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd"
DeleteRegKey HKEY_LOCAL_MACHINE "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\SABnzbd"
${RemovePrev} "$INSTDIR"
!insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd.lnk"
Delete "$SMPROGRAMS\$MUI_TEMP\Uninstall.lnk"
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - SafeMode.lnk"
Delete "$SMPROGRAMS\$MUI_TEMP\SABnzbd - Documentation.url"
RMDir "$SMPROGRAMS\$MUI_TEMP"
Delete "$SMPROGRAMS\Startup\SABnzbd.lnk"
Delete "$DESKTOP\SABnzbd.lnk"
DeleteRegKey HKEY_CURRENT_USER "Software\SABnzbd"
${unregisterExtension} ".nzb" "NZB File"
${RefreshShellIcons}
SectionEnd ; end of uninstall section
Section /o "un.$(MsgDelSettings)" DelSettings
DetailPrint "Uninstall settings $LOCALAPPDATA"
Delete "$LOCALAPPDATA\sabnzbd\sabnzbd.ini"
RMDir /r "$LOCALAPPDATA\sabnzbd"
SectionEnd
; eof
;--------------------------------
;Language strings
LangString MsgGoWiki ${LANG_ENGLISH} "Go to the SABnzbd Wiki"
LangString MsgShowRelNote ${LANG_ENGLISH} "Show Release Notes"
LangString MsgSupportUs ${LANG_ENGLISH} "Support the project, Donate!"
LangString MsgCloseSab ${LANG_ENGLISH} "Please close $\"SABnzbd.exe$\" first"
LangString MsgOldQueue ${LANG_ENGLISH} " >>>> WARNING <<<<$\r$\n$\r$\nPlease, first check the release notes or go to http://wiki.sabnzbd.org/introducing-0-7-0 !"
LangString MsgUninstall ${LANG_ENGLISH} "This will uninstall SABnzbd from your system"
LangString MsgRunAtStart ${LANG_ENGLISH} "Run at startup"
LangString MsgIcon ${LANG_ENGLISH} "Desktop Icon"
LangString MsgAssoc ${LANG_ENGLISH} "NZB File association"
LangString MsgDelProgram ${LANG_ENGLISH} "Delete Program"
LangString MsgDelSettings ${LANG_ENGLISH} "Delete Settings"
LangString MsgNoRuntime ${LANG_ENGLISH} "This system requires the Microsoft runtime library VC90 to be installed first. Do you want to do that now?"
LangString MsgDLRuntime ${LANG_ENGLISH} "Downloading Microsoft runtime installer..."
LangString MsgDLError ${LANG_ENGLISH} "Download error, retry?"
LangString MsgDLNeed ${LANG_ENGLISH} "Cannot install without runtime library, retry?"
LangString MsgRemoveOld ${LANG_ENGLISH} "You cannot overwrite an existing installation. $\n$\nClick `OK` to remove the previous version or `Cancel` to cancel this upgrade."
LangString MsgRemoveOld2 ${LANG_ENGLISH} "Your settings and data will be preserved."
Function un.onInit
!insertmacro MUI_UNGETLANGUAGE
FunctionEnd

View File

@@ -1,8 +1,8 @@
Metadata-Version: 1.0
Name: SABnzbd
Version: 0.7.2
Summary: SABnzbd-0.7.2
Home-page: http://sourceforge.net/projects/sabnzbdplus
Version: 1.2.2
Summary: SABnzbd-1.2.2
Home-page: http://sabnzbd.org
Author: The SABnzbd Team
Author-email: team@sabnzbd.org
License: GNU General Public License 2 (GPL2 or later)

View File

@@ -1,6 +1,15 @@
SABnzbd - The automated Usenet download tool
============================================
This Unicode release is not compatible with 0.7.x queues!
There is also an issue with upgrading of the "sabnzbd.ini" file.
Make sure that you have a backup!
Saved queues may not be compatible after updates.
----
SABnzbd is an Open Source Binary Newsreader written in Python.
It's totally free, incredibly easy to use, and works practically everywhere.
@@ -13,16 +22,18 @@ If you want to know more you can head over to our website: http://sabnzbd.org.
SABnzbd has a good deal of dependencies you'll need before you can get running. If you've previously run SABnzbd from one of the various Linux packages floating around (Ubuntu, Debian, Fedora, etc), then you likely already have all the needed dependencies. If not, here's what you're looking for:
- `python` (We support Python 2.5-2.7, preferably 2.6 or 2.7.)
- `python` (only 2.7.x and higher, but not 3.x.x)
- `python-cheetah`
- `python-configobj`
- `python-feedparser`
- `python-dbus`
- `python-openssl`
- `python-support`
- `python-yenc`
- `par2` (Multi-threaded par2 can be downloaded from [ChuChuSoft](http://chuchusoft.com/par2_tbb/download.html) )
- `par2` (Multi-threaded par2 installation guide can be found [here](https://forums.sabnzbd.org/viewtopic.php?f=16&t=18793#p99702))
- `unrar` (Make sure you get the "official" non-free version of unrar)
Optional:
- `python-cryptography` (enables certificate generation and detection of encrypted RAR-files during download)
- `python-yenc`
- `python-dbus` (enable option to Shutdown/Restart/Standby PC on queue finish)
- `7zip`
- `unzip`
Your package manager should supply these. If not, we've got links in our more in-depth [installation guide](https://github.com/sabnzbd/sabnzbd/blob/master/INSTALL.txt).
@@ -38,7 +49,7 @@ python SABnzbd.py
Or, if you want to run in the background:
```
python -d -f /path/to/sabnzbd.ini
python SABnzbd.py -d -f /path/to/sabnzbd.ini
```
If you want multi-language support, run:
@@ -47,8 +58,23 @@ If you want multi-language support, run:
python tools/make_mo.py
```
Our many other commandline options are explained in depth [here](http://wiki.sabnzbd.org/command-line-parameters).
Our many other command line options are explained in depth [here](https://sabnzbd.org/wiki/advanced/command-line-parameters).
## About Our Repo
We're going to be attempting to follow the [gitflow model](http://nvie.com/posts/a-successful-git-branching-model/), so you can consider "master" to be whatever our present stable release build is (presently 0.6.x) and "develop" to be whatever our next build will be (presently 0.7.x). Once we transition from unstable to stable dev builds we'll create release branches, and encourage you to follow along and help us test.
The workflow we use, is a simplified form of "GitFlow".
Basically:
- `master` contains only stable releases (which have been merged to `master`) and is intended for end-users.
- `develop` is the target for integration and is **not** intended for end-users.
- `1.1.x` is a release and maintenance branch for 1.1.x (1.1.0 -> 1.1.1 -> 1.1.2) and is **not** intended for end-users.
- `feature/my_feature` is a temporary feature branch based on `develop`.
- `hotfix/my_hotfix` is an optional temporary branch for bugfix(es) based on `develop`.
Conditions:
- Merging of a stable release into `master` will be simple: the release branch is always right.
- `master` is not merged back to `develop`.
- `develop` is not re-based on `master`.
- Release branches branch from `develop` only.
- Bugfixes created specifically for a release branch are done there (because they are specific, they're not cherry-picked to `develop`).
- Bugfixes done on `develop` may be cherry-picked to a release branch.
- We will not release a 1.0.2 if a 1.1.0 has already been released.

64
README.mkd Normal file
View File

@@ -0,0 +1,64 @@
Release Notes - SABnzbd 1.2.2
==============================================
## Bug fix in 1.2.2
- Windows: job-directory incorrectly passed to PostProcessing-script
## What's new in 1.2.1
- QuickCheck will perform fast rename of obfuscated posts
- RSS Downloaded page now shows icon to indicate source
- HTML tags are filtered from single-line script output
- New self-signed certificates now list local IP in SAN-list
- Handle jobs on Windows with forbidden names (Con.*, Aux.*,..)
## Bug fixes in 1.2.1
- Fix crashing Assembler
- 'Only Download Top of Queue' was broken for a long time
- Cloaked files (RAR within RAR) were not detected anymore
- Incorrectly labeled some downloads as Encrypted
- Passwords were not parsed correctly from filenames
- RSS reading could fail on missing attributes
- Multi-feed RSS will not stop if only 1 feed is not functioning
- Duplicate detection set to Fail would not work for RSS feeds
- Incorrectly marking jobs with folders inside as failed
- Categories were not matched properly if a list of tags was set
- PostProcessing-script was not called on Accept&Fail or Dupe detect
- Support for newer par2cmdline(-mt) versions that need -B parameter
- Some newsservers would timeout when connecting
- More robust detection of execute permissions for scripts
- CPU type reporting on Windows and macOS
- Failed to start with some localhost configs
- Removed some more stalling issues
- Retry rename 3x before falling back to copy during "Moving"
- Catch several SSL errors of the webserver
- Disk-space information is now only checked every 10 seconds
## Translations
- Many translations updated, thanks to our translators!
## About
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
thanks to its web-based user interface and advanced
built-in post-processing options that automatically verify, repair,
extract and clean up posts downloaded from Usenet.
(c) Copyright 2007-2017 by "The SABnzbd-team" \<team@sabnzbd.org\>
### IMPORTANT INFORMATION about release 1.x.x
<https://sabnzbd.org/wiki/new-features-and-changes>
### Known problems and solutions
- Read the file "ISSUES.txt"
### Upgrading from 0.7.x and older
- Finish queue
- Stop SABnzbd
- Install new version
- Start SABnzbd
The organization of the download queue is different from older versions.
1.x.x will not see the existing queue, but you can go to
Status->QueueRepair and "Repair" the old queue.
Also, your sabnzbd.ini file will be upgraded, making it
incompatible with releases older than 0.7.9

View File

@@ -1,133 +0,0 @@
{\rtf1\ansi\ansicpg1252\cocoartf1138\cocoasubrtf320
{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
\paperw11900\paperh16840\vieww16360\viewh15680\viewkind0
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720
\f0\b\fs48 \cf0 SABnzbd 0.7.2\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural
\b0\fs26 \cf0 \
\b Fixes in 0.7.2
\b0 \
- Improve support for nzbsrus.com\
- Don't try to show NZB age when not known yet\
- Prevent systems with unresolvable hostnames from always using 0.0.0.0\
- Initial "quota left" was not set correctly when enabling quota\
- Report incorrect RSS filter expressions (instead of aborting analysis)\
- Improve detection of invalid articles (so that backup server will be tried)\
- Windows installer: don't remove settings by default when uninstalling\
- Fix sorting of rar files in job so that .rar preceeds .r00\
- Fix for NZB-icon issue when 0.7.0 was previously installed\
- Fix startup problem on Windows when IPv4 has precedence over IPv6\
\
\b Fixes in 0.7.1
\b0 \
- Fixed problem were fetching par2 files after first verification could stall in the queue\
- Fixed retry behaviour of NZB fetching from URL (with handling of nzbsrus.com error codes)\
- Verification/repair would not be executed properly when one more RAR files missed their first article.\
- Improved backup of sabnzbd.ini file, now uses backup when original is gone or corrupt\
- Several translations extended/improved\
- Plush skin: fix problems with pull-down menus in Mobile Safari\
- On some Linux and OSX systems using localhost would still make SABnzbd give access to other computers\
- Windows: the installer did not set an icon when associating NZB files with SABnzbd\
- Fix problem that the Opera browser had with Config->Servers\
- Retry a few times when accessing a mounted drive to create the final destination folder\
- Minor fixes in Window Tray icon and OSX top menu\
- Add no_ipv6 special for systems that keep having issues with [::1]\
- Fix crash in QuickCheck when expected par2 file wasn't downloaded\
- API calls "addurl" and "addid" (newzbin) can now be used interchangeably\
- Fix endless par2-fetch loop after retrying failed job\
- Don't send "bad fetch" email when emailing is off\
- Add some support for nzbrus.com's non-VIP limiting\
- Fix signing of OSX DMG\
\
\b What's new in 0.7.0
\b0 \
- Download quota management\
- Windows: simple system tray menu\
- Multi-platform Growl support\
- NotifyOSD support for Linux distros that have it\
- Option to set maximum number of retries for servers (prevents deadlock)\
- Pre-download check to estimate completeness (reliability is limited)\
- Prevent partial downloading of par2 files that are not needed yet\
- Config->Special for settings previously only available in the sabnzbd.ini file\
- For Usenet servers with multiple IP addresses, pick a random one per connection\
- Add pseudo-priority "Stop" that will send the job immediately to the post-processing queue\
- Allow jobs still waiting for post-processing to be deleted too\
- More persistent retries for unreliable indexers\
- Single Configuration skin for all others skins (there is an option for the old style)\
- Config->Special for settings that were previously only changeable in the sabnzbd.ini file\
- Add Spanish, Portuguese (Brazil) and Polish translations\
- Individual RSS filter toggle\
- Unified OSX DMG\
\
\b About
\b0 \
SABnzbd is an open-source cross-platform binary newsreader.\
It simplifies the process of downloading from Usenet dramatically,\
thanks to its web-based user interface and advanced\
built-in post-processing options that automatically verify, repair,\
extract and clean up posts downloaded from Usenet.\
SABnzbd also has a fully customizable user interface,\
and offers a complete API for third-party applications to hook into.\
\
(c) Copyright 2007-2012 by "The SABnzbd-team" <team@sabnzbd.org>\
\
There is an extensive Wiki on the use of SABnzbd.\
{\field{\*\fldinst{HYPERLINK "http://wiki.sabnzbd.org/"}}{\fldrslt http://wiki.sabnzbd.org/}}\
\
\b IMPORTANT INFORMATION
\b0 about release 0.7.1:\
{\field{\*\fldinst{HYPERLINK "http://wiki.sabnzbd.org/introducing-0-7-0"}}{\fldrslt http://wiki.sabnzbd.org/introducing-0-7-0}}\
\
\b Known problems and solutions\
\b0 Read the file "ISSUES.txt"
\b \
\b0 \
\
\b\fs40 Upgrading from 0.6.x
\b0\fs26 \
Stop SABnzbd.\
Install new version\
Start SABnzbd.\
\
\b\fs40 Upgrading from 0.5.x
\b0\fs26 \
Stop SABnzbd.\
Uninstall current version, keeping the data.\
Install new version\
Start SABnzbd.\
\
The organization of the download queue is different from 0.5.x.\
0.6.x will finish downloading an existing queue, but you\
cannot go back to an older version without losing your queue.\
Also, your sabnzbd.ini file will be upgraded, making it\
incompatible with release 0.5.x\
\
\b\fs40 \
Upgrading from 0.4.x
\b0\fs26 \
\
\b PLEASE DOWNLOAD YOUR CURRENT QUEUE BEFORE UPGRADING
\b0 \
\
When upgrading from a 0.4.x release such as 0.4.12 your old settings will be kept.\
You will however be given a fresh queue and history. If you have items in your queue\
from the older version of SABnzbd, you can either re-import the nzb files if you kept\
an nzb backup folder, or temporarily go back to 0.4.x until your queue is complete.\
The history is now stored in a better format meaning future upgrades should be backwards\
compatible.\
}

View File

@@ -1,67 +0,0 @@
Release Notes - SABnzbd 0.7.2
===============================
## Fixes in 0.7.2
- Improve support for nzbsrus.com
- Don't try to show NZB age when not known yet
- Prevent systems with unresolvable hostnames from always using 0.0.0.0
- Initial "quota left" was not set correctly when enabling quota
- Report incorrect RSS filter expressions (instead of aborting analysis)
- Improve detection of invalid articles (so that backup server will be tried)
- Windows installer: don't remove settings by default when uninstalling
- Fix sorting of rar files in job so that .rar preceeds .r00
- Fix for NZB-icon issue when 0.7.0 was previously installed
- Fix startup problem on Windows when IPv4 has precedence over IPv6
## Fixes in 0.7.1
- Fixed problem were fetching par2 files after first verification could stall in the queue
- Fixed retry behaviour of NZB fetching from URL (with handling of nzbsrus.com error codes)
- Verification/repair would not be executed properly when one more RAR files
missed their first article.
- Improved backup of sabnzbd.ini file, now uses backup when original is gone or corrupt
- Several translations extended/improved
- Plush skin: fix problems with pull-down menus in Mobile Safari
- On some Linux and OSX systems using localhost would still make SABnzbd
give access to other computers
- Windows: the installer did not set an icon when associating NZB files with SABnzbd
- Fix problem that the Opera browser had with Config->Servers
- Retry a few times when accessing a mounted drive to create the
final destination folder
- Minor fixes in Window Tray icon and OSX top menu
- Add no_ipv6 special for systems that keep having issues with [::1]
- Fix crash in QuickCheck when expected par2 file wasn't downloaded
- API calls "addurl" and "addid" (newzbin) can now be used interchangeably
- Fix endless par2-fetch loop after retrying failed job
- Don't send "bad fetch" email when emailing is off
- Add some support for nzbrus.com's non-VIP limiting
- Fix signing of OSX DMG
## What's new in 0.7.0
- Download quota management
- Windows: simple system tray menu
- Multi-platform Growl support
- NotifyOSD support for Linux distros that have it
- Option to set maximum number of retries for servers (prevents deadlock)
- Pre-download check to estimate completeness (reliability is limited)
- Prevent partial downloading of par2 files that are not needed yet
- Config->Special for settings previously only available in the sabnzbd.ini file
- For Usenet servers with multiple IP addresses, pick a random one per connection
- Add pseudo-priority "Stop" that will send the job immediately to the post-processing queue
- Allow jobs still waiting for post-processing to be deleted too
- More persistent retries for unreliable indexers
- Single Configuration skin for all others skins (there is an option for the old style)
- Config->Special for settings that were previously only changeable in the sabnzbd.ini file
- Add Spanish, Portuguese (Brazil) and Polish translations
- Individual RSS filter toggle
- Unified OSX DMG
## About
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
thanks to its web-based user interface and advanced
built-in post-processing options that automatically verify, repair,
extract and clean up posts downloaded from Usenet.
(c) Copyright 2007-2012 by "The SABnzbd-team" <team@sabnzbd.org>

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python -OO
# Copyright 2008-2012 The SABnzbd-Team <team@sabnzbd.org>
# Copyright 2008-2017 The SABnzbd-Team <team@sabnzbd.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
@@ -16,18 +16,23 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import sys
if sys.version_info < (2,5):
print "Sorry, requires Python 2.5 or higher."
if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
print "Sorry, requires Python 2.6 or 2.7."
sys.exit(1)
import os
import time
import subprocess
#------------------------------------------------------------------------------
try:
import win32api, win32file
import win32serviceutil, win32evtlogutil, win32event, win32service, pywintypes
import win32api
import win32file
import win32serviceutil
import win32evtlogutil
import win32event
import win32service
import pywintypes
except ImportError:
print "Sorry, requires Python module PyWin32."
sys.exit(1)
@@ -35,11 +40,10 @@ except ImportError:
from util.mailslot import MailSlot
from util.apireg import del_connection_info, set_connection_info
#------------------------------------------------------------------------------
WIN_SERVICE = None
#------------------------------------------------------------------------------
def HandleCommandLine(allow_service=True):
""" Handle command line for a Windows Service
Prescribed name that will be called by Py2Exe.
@@ -52,7 +56,6 @@ def start_sab():
return subprocess.Popen('net start SABnzbd', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).stdout.read()
#------------------------------------------------------------------------------
def main():
mail = MailSlot()
@@ -78,13 +81,13 @@ def main():
elif msg.startswith('api '):
active = True
counter = 0
cmd, url = msg.split()
_cmd, url = msg.split()
if url:
set_connection_info(url.strip(), user=False)
if active:
counter += 1
if counter > 120: # 120 seconds
if counter > 120: # 120 seconds
counter = 0
start_sab()
@@ -96,11 +99,12 @@ def main():
return ''
#####################################################################
#
##############################################################################
# Windows Service Support
#
##############################################################################
import servicemanager
class SABHelper(win32serviceutil.ServiceFramework):
""" Win32 Service Handler """
@@ -115,7 +119,7 @@ class SABHelper(win32serviceutil.ServiceFramework):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
self.overlapped = pywintypes.OVERLAPPED()
self.overlapped = pywintypes.OVERLAPPED() # @UndefinedVariable
self.overlapped.hEvent = win32event.CreateEvent(None, 0, 0, None)
WIN_SERVICE = self
@@ -143,11 +147,9 @@ class SABHelper(win32serviceutil.ServiceFramework):
unicode(text))
#####################################################################
#
##############################################################################
# Platform specific startup code
#
##############################################################################
if __name__ == '__main__':
win32serviceutil.HandleCommandLine(SABHelper, argv=sys.argv)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
Copyright (c) 2004-2007, 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.

View File

@@ -1 +1,5 @@
This CherryPy Rev 2138 patched with Rev 2272.
CherryPy 8.1.2
Official distribution: https://github.com/cherrypy/cherrypy/releases
The folders 'tutorial', 'test' and 'scaffold' have been removed.
This file has been added.

View File

@@ -53,123 +53,34 @@ 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"
try:
import pkg_resources
except ImportError:
pass
from urlparse import urljoin as _urljoin
from threading import local as _local
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect # noqa
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError # noqa
class _AttributeDocstrings(type):
"""Metaclass for declaring docstrings for class attributes."""
# The full docstring for this type is down in the __init__ method so
# that it doesn't show up in help() for every consumer class.
def __init__(cls, name, bases, dct):
'''Metaclass for declaring docstrings for class attributes.
Base Python doesn't provide any syntax for setting docstrings on
'data attributes' (non-callables). This metaclass allows class
definitions to follow the declaration of a data attribute with
a docstring for that attribute; the attribute docstring will be
popped from the class dict and folded into the class docstring.
The naming convention for attribute docstrings is:
<attrname> + "__doc".
For example:
class Thing(object):
"""A thing and its properties."""
__metaclass__ = cherrypy._AttributeDocstrings
height = 50
height__doc = """The height of the Thing in inches."""
In which case, help(Thing) starts like this:
>>> help(mod.Thing)
Help on class Thing in module pkg.mod:
class Thing(__builtin__.object)
| A thing and its properties.
|
| height [= 50]:
| The height of the Thing in inches.
|
The benefits of this approach over hand-edited class docstrings:
1. Places the docstring nearer to the attribute declaration.
2. Makes attribute docs more uniform ("name (default): doc").
3. Reduces mismatches of attribute _names_ between
the declaration and the documentation.
4. Reduces mismatches of attribute default _values_ between
the declaration and the documentation.
The benefits of a metaclass approach over other approaches:
1. Simpler ("less magic") than interface-based solutions.
2. __metaclass__ can be specified at the module global level
for classic classes.
For various formatting reasons, you should write multiline docs
with a leading newline and not a trailing one:
response__doc = """
The response object for the current thread. In the main thread,
and any threads which are not HTTP requests, this is None."""
The type of the attribute is intentionally not included, because
that's not How Python Works. Quack.
'''
newdoc = [cls.__doc__ or ""]
dctnames = dct.keys()
dctnames.sort()
for name in dctnames:
if name.endswith("__doc"):
# Remove the magic doc attribute.
if hasattr(cls, name):
delattr(cls, name)
# Make a uniformly-indented docstring from it.
val = '\n'.join([' ' + line.strip()
for line in dct[name].split('\n')])
# Get the default value.
attrname = name[:-5]
try:
attrval = getattr(cls, attrname)
except AttributeError:
attrval = "missing"
# Add the complete attribute docstring to our list.
newdoc.append("%s [= %r]:\n%s" % (attrname, attrval, val))
# Add our list of new docstrings to the class docstring.
cls.__doc__ = "\n\n".join(newdoc)
from cherrypy import _cplogging
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
from cherrypy import _cpdispatch as dispatch
from cherrypy import _cpdispatch as dispatch # noqa
from cherrypy import _cptools
tools = _cptools.default_toolbox
Tool = _cptools.Tool
from cherrypy._cptools import default_toolbox as tools, Tool
from cherrypy import _cprequest
from cherrypy.lib import http as _http
from cherrypy.lib import httputil as _httputil
from cherrypy import _cptree
tree = _cptree.Tree()
from cherrypy._cptree import Application
from cherrypy import _cpwsgi as wsgi
from cherrypy._cptree import Application # noqa
from cherrypy import _cpwsgi as wsgi # noqa
from cherrypy import _cpserver
from cherrypy import process
try:
from cherrypy.process import win32
@@ -180,22 +91,33 @@ except ImportError:
engine = process.bus
# Timeout monitor
tree = _cptree.Tree()
__version__ = '8.1.2'
# 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:
@@ -212,14 +134,31 @@ engine.thread_manager.subscribe()
engine.signal_handler = process.plugins.SignalHandler(engine)
from cherrypy import _cpserver
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)
server = _cpserver.Server()
server.subscribe()
def quickstart(root=None, script_name="", config=None):
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.
@@ -227,7 +166,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
@@ -236,27 +175,18 @@ def quickstart(root=None, script_name="", config=None):
"""
if config:
_global_conf_alias.update(config)
if root is not None:
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()
tree.mount(root, script_name, config)
engine.signals.subscribe()
engine.start()
engine.block()
try:
from threading import local as _local
except ImportError:
from cherrypy._cpthreadinglocal import local 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
@@ -264,24 +194,22 @@ class _Serving(_local):
conversation, yet still refer to them as module-level globals in a
thread-safe way.
"""
__metaclass__ = _AttributeDocstrings
request = _cprequest.Request(_http.Host("127.0.0.1", 80),
_http.Host("127.0.0.1", 1111))
request__doc = """
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()
response__doc = """
"""
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()
@@ -290,58 +218,59 @@ 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__", ):
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)
# Python 3
__bool__ = __nonzero__
# Create request and response object (the same objects will be used
# throughout the entire life of the webserver, but will redirect
@@ -350,7 +279,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()
@@ -373,18 +305,31 @@ except ImportError:
pass
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):
try:
"""Log the given message to the app.log or global log as appropriate.
"""
# Do NOT use try/except here. See
# https://github.com/cherrypy/cherrypy/issues/945
if hasattr(request, 'app') and hasattr(request.app, 'log'):
log = request.app.log
except AttributeError:
else:
log = self
return log.error(*args, **kwargs)
def access(self):
"""Log an access message to the app.log or global log as appropriate.
"""
try:
return request.app.log.access()
except AttributeError:
@@ -398,164 +343,29 @@ 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)
# Helper functions for CP apps #
def expose(func=None, alias=None):
"""Expose the function, optionally providing an alias or set of aliases."""
def expose_(func):
func.exposed = True
if alias is not None:
if isinstance(alias, basestring):
parents[alias.replace(".", "_")] = func
else:
for a in alias:
parents[a.replace(".", "_")] = func
return func
import sys, types
if isinstance(func, (types.FunctionType, types.MethodType)):
if alias is None:
# @expose
func.exposed = True
return func
else:
# func = expose(func, alias)
parents = sys._getframe(1).f_locals
return expose_(func)
elif func is None:
if alias is None:
# @expose()
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose(alias="alias") or
# @expose(alias=["alias1", "alias2"])
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose("alias") or
# @expose(["alias1", "alias2"])
parents = sys._getframe(1).f_locals
alias = func
return expose_
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).
If True, the output will instead be a URL that is relative to the
current request path, perhaps including '..' atoms. If relative is
the string 'server', the output will instead be a URL that is
relative to the server root; i.e., it will start with a slash.
"""
if qs:
qs = '?' + qs
if request.app:
if not path.startswith("/"):
# Append/remove trailing slash from path_info as needed
# (this is to support mistyped URL's without redirecting;
# if you want to redirect, use tools.trailing_slash).
pi = request.path_info
if request.is_index is True:
if not pi.endswith('/'):
pi = pi + '/'
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).
# We'll have to guess the base from server.* attributes.
# This will produce very different results from the above
# 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 = []
for atom in newurl.split('/'):
if atom == '.':
pass
elif atom == '..':
atoms.pop()
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
# termed an absolute-path reference, as defined by <abs_path>..."
# This is also sometimes called "server-relative".
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
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]
new = newurl.split('/')
while old and new:
a, b = old[0], new[0]
if a != b:
break
old.pop(0)
new.pop(0)
new = (['..'] * len(old)) + new
newurl = '/'.join(new)
return newurl
from cherrypy._helper import expose, popargs, url # noqa
# import _cpconfig last so it can reference other top-level objects
from cherrypy import _cpconfig
from cherrypy import _cpconfig # noqa
# Use _global_conf_alias so quickstart can use 'config' as an arg
# without shadowing cherrypy.config.
config = _global_conf_alias = _cpconfig.Config()
config.defaults = {
'tools.log_tracebacks.on': True,
'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.
config.reset()
from cherrypy import _cpchecker
from cherrypy import _cpchecker # noqa
checker = _cpchecker.Checker()
engine.subscribe('start', checker)

4
cherrypy/__main__.py Executable file
View File

@@ -0,0 +1,4 @@
import cherrypy.daemon
if __name__ == '__main__':
cherrypy.daemon.run()

View File

@@ -1,79 +0,0 @@
import cgi
import cherrypy
class FieldStorage(cgi.FieldStorage):
def __init__(self, *args, **kwds):
try:
cgi.FieldStorage.__init__(self, *args, **kwds)
except ValueError, ex:
if str(ex) == 'Maximum content length exceeded':
raise cherrypy.HTTPError(status=413)
else:
raise ex
def read_lines_to_eof(self):
"""Internal: read lines until EOF."""
while 1:
line = self.fp.readline(1<<16)
if not line:
self.done = -1
break
self.__write(line)
def read_lines_to_outerboundary(self):
"""Internal: read lines until outerboundary."""
next = "--" + self.outerboundary
last = next + "--"
delim = ""
last_line_lfend = True
while 1:
line = self.fp.readline(1<<16)
if not line:
self.done = -1
break
if line[:2] == "--" and last_line_lfend:
strippedline = line.strip()
if strippedline == next:
break
if strippedline == last:
self.done = 1
break
odelim = delim
if line[-2:] == "\r\n":
delim = "\r\n"
line = line[:-2]
last_line_lfend = True
elif line[-1] == "\n":
delim = "\n"
line = line[:-1]
last_line_lfend = True
else:
delim = ""
last_line_lfend = False
self.__write(odelim + line)
def skip_lines(self):
"""Internal: skip lines until outer boundary if defined."""
if not self.outerboundary or self.done:
return
next = "--" + self.outerboundary
last = next + "--"
last_line_lfend = True
while 1:
line = self.fp.readline(1<<16)
if not line:
self.done = -1
break
if line[:2] == "--" and last_line_lfend:
strippedline = line.strip()
if strippedline == next:
break
if strippedline == last:
self.done = 1
break
if line.endswith('\n'):
last_line_lfend = True
else:
last_line_lfend = False

View File

@@ -2,29 +2,30 @@ import os
import warnings
import cherrypy
from cherrypy._cpcompat import iteritems, copykeys, builtins
class Checker(object):
"""A checker for CherryPy sites and their mounted applications.
on: set this to False to turn off the checker completely.
When this object is called at engine startup, it executes each
of its own methods whose names start with "check_". If you wish
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.
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:
@@ -32,102 +33,149 @@ class Checker(object):
warnings.formatwarning = self.formatwarning
try:
for name in dir(self):
if name.startswith("check_"):
if name.startswith('check_'):
method = getattr(self, name)
if method and callable(method):
if method and hasattr(method, '__call__'):
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
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):
for sn, app in cherrypy.tree.apps.iteritems():
"""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
if not app.config:
continue
if sn == '':
continue
sn_atoms = sn.strip("/").split("/")
sn_atoms = sn.strip('/').split('/')
for key in app.config.keys():
key_atoms = key.strip("/").split("/")
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))
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)
warnings.warn(os.linesep.join(msg))
def check_skipped_app_config(self):
for sn, app in cherrypy.tree.apps.iteritems():
"""Check for mounted Applications that have no config."""
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
if not app.config:
msg = "The Application mounted at %r has an empty config." % sn
msg = 'The Application mounted at %r has an empty config.' % sn
if self.global_config_contained_paths:
msg += (" It looks like the config you passed to "
"cherrypy.config.update() contains application-"
"specific sections. You must explicitly pass "
"application config via "
"cherrypy.tree.mount(..., config=app_config)")
msg += (' It looks like the config you passed to '
'cherrypy.config.update() contains application-'
'specific sections. You must explicitly pass '
'application config via '
'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.
"""
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
if not app.config:
continue
for key in app.config.keys():
if key.startswith('[') or key.endswith(']'):
warnings.warn(
'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.
request = cherrypy.request
for sn, app in cherrypy.tree.apps.iteritems():
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
request.app = app
for section in app.config:
# get_resource will populate request.config
request.get_resource(section + "/dummy.html")
request.get_resource(section + '/dummy.html')
conf = request.config.get
if conf("tools.staticdir.on", False):
msg = ""
root = conf("tools.staticdir.root")
dir = conf("tools.staticdir.dir")
if conf('tools.staticdir.on', False):
msg = ''
root = conf('tools.staticdir.root')
dir = conf('tools.staticdir.dir')
if dir is None:
msg = "tools.staticdir.dir is not set."
msg = 'tools.staticdir.dir is not set.'
else:
fulldir = ""
fulldir = ''
if os.path.isabs(dir):
fulldir = dir
if root:
msg = ("dir is an absolute path, even "
"though a root is provided.")
msg = ('dir is an absolute path, even '
'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)
msg += '\n'
msg += ('%r (root + dir) is not an existing '
'filesystem path.' % fulldir)
if msg:
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
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',
@@ -140,115 +188,115 @@ 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.iteritems():
for section, conf in config.items():
if isinstance(conf, dict):
for k, v in conf.iteritems():
for k, v in conf.items():
if k in self.obsolete:
warnings.warn("%r is obsolete. Use %r instead.\n"
"section: [%s]" %
warnings.warn('%r is obsolete. Use %r instead.\n'
'section: [%s]' %
(k, self.obsolete[k], section))
elif k in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead.\n"
"section: [%s]" %
warnings.warn('%r is deprecated. Use %r instead.\n'
'section: [%s]' %
(k, self.deprecated[k], section))
else:
if section in self.obsolete:
warnings.warn("%r is obsolete. Use %r instead."
warnings.warn('%r is obsolete. Use %r instead.'
% (section, self.obsolete[section]))
elif section in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead."
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)
for sn, app in cherrypy.tree.apps.iteritems():
for sn, app in cherrypy.tree.apps.items():
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(app.toolboxes.keys())
ns.extend(app.namespaces.keys())
ns.extend(app.request_class.namespaces.keys())
ns.extend(cherrypy.config.namespaces.keys())
ns = ['wsgi']
ns.extend(copykeys(app.toolboxes))
ns.extend(copykeys(app.namespaces))
ns.extend(copykeys(app.request_class.namespaces))
ns.extend(copykeys(cherrypy.config.namespaces))
ns += self.extra_config_namespaces
for section, conf in app.config.iteritems():
is_path_section = section.startswith("/")
for section, conf in app.config.items():
is_path_section = section.startswith('/')
if is_path_section and isinstance(conf, dict):
for k, v in conf.iteritems():
atoms = k.split(".")
for k, v in conf.items():
atoms = k.split('.')
if len(atoms) > 1:
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":
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.iteritems():
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
self._known_ns(app)
# -------------------------- Config Types -------------------------- #
known_config_types = {}
def _populate_known_types(self):
import __builtin__
builtins = [x for x in vars(__builtin__).values()
if type(x) is type(str)]
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
if name == 'body_params':
continue
vtype = type(getattr(obj, name, None))
if vtype in builtins:
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")
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.iteritems():
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.iteritems():
for k, v in conf.items():
if v is not None:
expected_type = self.known_config_types.get(k, None)
vtype = type(v)
@@ -263,23 +311,22 @@ 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)
for sn, app in cherrypy.tree.apps.iteritems():
for sn, app in cherrypy.tree.apps.items():
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.iteritems():
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.")

334
cherrypy/_cpcompat.py Normal file
View File

@@ -0,0 +1,334 @@
"""Compatibility code for using CherryPy with various versions of Python.
CherryPy 3.2 is compatible with Python versions 2.6+. This module provides a
useful abstraction over the differences between Python versions, sometimes by
preferring a newer idiom, sometimes an older one, and sometimes a custom one.
In particular, Python 2 uses str and '' for byte strings, while Python 3
uses str and '' for unicode strings. We will call each of these the 'native
string' type for each version. Because of this major difference, this module
provides
two functions: 'ntob', which translates native strings (of type 'str') into
byte strings regardless of Python version, and 'ntou', which translates native
strings to unicode strings. This also provides a 'BytesIO' name for dealing
specifically with bytes, and a 'StringIO' name for dealing with native strings.
It also provides a 'base64_decode' function with native strings as input and
output.
"""
import binascii
import os
import re
import sys
import threading
import six
if six.PY3:
def ntob(n, encoding='ISO-8859-1'):
"""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.
"""
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
else:
# Python 2
def ntob(n, encoding='ISO-8859-1'):
"""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.
"""
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
def assert_native(n):
if not isinstance(n, str):
raise TypeError('n must be a native str (got %s)' % type(n).__name__)
try:
# Python 3.1+
from base64 import decodebytes as _base64_decodebytes
except ImportError:
# Python 3.0-
# since CherryPy claims compability with Python 2.3, we must use
# 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, six.text_type):
b = n.encode(encoding)
else:
b = n
b = _base64_decodebytes(b)
if str is six.text_type:
return b.decode(encoding)
else:
return b
try:
sorted = sorted
except NameError:
def sorted(i):
i = i[:]
i.sort()
return i
try:
reversed = reversed
except NameError:
def reversed(x):
i = len(x)
while i > 0:
i -= 1
yield x[i]
try:
# Python 3
from urllib.parse import urljoin, urlencode
from urllib.parse import quote, quote_plus
from urllib.request import unquote, urlopen
from urllib.request import parse_http_list, parse_keqv_list
except ImportError:
# Python 2
from urlparse import urljoin # noqa
from urllib import urlencode, urlopen # noqa
from urllib import quote, quote_plus # noqa
from urllib import unquote # noqa
from urllib2 import parse_http_list, parse_keqv_list # noqa
try:
dict.iteritems
# Python 2
iteritems = lambda d: d.iteritems()
copyitems = lambda d: d.items()
except AttributeError:
# Python 3
iteritems = lambda d: d.items()
copyitems = lambda d: list(d.items())
try:
dict.iterkeys
# Python 2
iterkeys = lambda d: d.iterkeys()
copykeys = lambda d: d.keys()
except AttributeError:
# Python 3
iterkeys = lambda d: d.keys()
copykeys = lambda d: list(d.keys())
try:
dict.itervalues
# Python 2
itervalues = lambda d: d.itervalues()
copyvalues = lambda d: d.values()
except AttributeError:
# Python 3
itervalues = lambda d: d.values()
copyvalues = lambda d: list(d.values())
try:
# Python 3
import builtins
except ImportError:
# Python 2
import __builtin__ as builtins # noqa
try:
# 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, IncompleteRead
from httplib import NotConnected
from BaseHTTPServer import BaseHTTPRequestHandler
except ImportError:
# Python 3
from http.cookies import SimpleCookie, CookieError # noqa
from http.client import BadStatusLine, HTTPConnection, IncompleteRead # noqa
from http.client import NotConnected # noqa
from http.server import BaseHTTPRequestHandler # noqa
# Some platforms don't expose HTTPSConnection, so handle it separately
if six.PY3:
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
except NameError:
# Python 3
xrange = range
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)
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.
import simplejson as json
json_decode = json.JSONDecoder().decode
_json_encode = json.JSONEncoder().iterencode
except ImportError:
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 six.PY3:
# 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')
else:
json_encode = _json_encode
text_or_bytes = six.text_type, six.binary_type
try:
import cPickle as pickle
except ImportError:
# In Python 2, pickle is a Python version.
# In Python 3, pickle is the sped-up C version.
import pickle # noqa
def random20():
return binascii.hexlify(os.urandom(20)).decode('ascii')
try:
from _thread import get_ident as get_thread_ident
except ImportError:
from thread import get_ident as get_thread_ident # noqa
try:
# Python 3
next = next
except NameError:
# Python 2
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
try:
# Python 2.7+
from subprocess import _args_from_interpreter_flags
except ImportError:
def _args_from_interpreter_flags():
"""Tries to reconstruct original interpreter args from sys.flags for Python 2.6
Backported from Python 3.5. Aims to return a list of
command-line arguments reproducing the current
settings in sys.flags and sys.warnoptions.
"""
flag_opt_map = {
'debug': 'd',
# 'inspect': 'i',
# 'interactive': 'i',
'optimize': 'O',
'dont_write_bytecode': 'B',
'no_user_site': 's',
'no_site': 'S',
'ignore_environment': 'E',
'verbose': 'v',
'bytes_warning': 'b',
'quiet': 'q',
'hash_randomization': 'R',
'py3k_warning': '3',
}
args = []
for flag, opt in flag_opt_map.items():
v = getattr(sys.flags, flag)
if v > 0:
if flag == 'hash_randomization':
v = 1 # Handle specification of an exact seed
args.append('-' + opt * v)
for opt in sys.warnoptions:
args.append('-W' + opt)
return args

View File

@@ -1,4 +1,5 @@
"""Configuration system for CherryPy.
"""
Configuration system for CherryPy.
Configuration in CherryPy is implemented via dictionaries. Keys are strings
which name the mapped value, which may be of any type.
@@ -10,17 +11,20 @@ Architecture
CherryPy Requests are part of an Application, which runs in a global context,
and configuration data may apply to any of those three scopes:
Global: configuration entries which apply everywhere are stored in
Global
Configuration entries which apply everywhere are stored in
cherrypy.config.
Application: entries which apply to each mounted application are stored
Application
Entries which apply to each mounted application are stored
on the Application object itself, as 'app.config'. This is a two-level
dict where each key is a path, or "relative URL" (for example, "/" or
"/path/to/my/page"), and each value is a config dict. Usually, this
data is provided in the call to tree.mount(root(), config=conf),
although you may also use app.merge(conf).
Request: each Request object possesses a single 'Request.config' dict.
Request
Each Request object possesses a single 'Request.config' dict.
Early in the request process, this dict is populated by merging global
config entries, Application entries (whose path equals or is a parent
of Request.path_info), and any config acquired while looking up the
@@ -33,7 +37,7 @@ Declaration
Configuration data may be supplied as a Python dictionary, as a filename,
or as an open file object. When you supply a filename or file, CherryPy
uses Python's builtin ConfigParser; you declare Application config by
writing each path as a section header:
writing each path as a section header::
[/path/to/my/page]
request.stream = True
@@ -41,20 +45,22 @@ writing each path as a section header:
To declare global configuration entries, place them in a [global] section.
You may also declare config entries directly on the classes and methods
(page handlers) that make up your CherryPy application via the '_cp_config'
attribute. For example:
(page handlers) that make up your CherryPy application via the ``_cp_config``
attribute, set with the ``cherrypy.config`` decorator. For example::
@cherrypy.config(**{'tools.gzip.on': True})
class Demo:
_cp_config = {'tools.gzip.on': True}
@cherrypy.expose
@cherrypy.config(**{'request.show_tracebacks': False})
def index(self):
return "Hello world"
index.exposed = True
index._cp_config = {'request.show_tracebacks': False}
Note, however, that this behavior is only guaranteed for the default
dispatcher. Other dispatchers may have different restrictions on where
you can attach _cp_config attributes.
.. note::
This behavior is only guaranteed for the default dispatcher.
Other dispatchers may have different restrictions on where
you can attach config attributes.
Namespaces
@@ -63,23 +69,42 @@ Namespaces
Configuration keys are separated into namespaces by the first "." in the key.
Current namespaces:
engine: Controls the 'application engine', including autoreload.
These can only be declared in the global config.
tree: Grafts cherrypy.Application objects onto cherrypy.tree.
These can only be declared in the global config.
hooks: Declares additional request-processing functions.
log: Configures the logging for each application.
These can only be declared in the global or / config.
request: Adds attributes to each Request.
response: Adds attributes to each Response.
server: Controls the default HTTP server via cherrypy.server.
These can only be declared in the global config.
tools: Runs and configures additional request-processing packages.
wsgi: Adds WSGI middleware to an Application's "pipeline".
These can only be declared in the app's root config ("/").
checker: Controls the 'checker', which looks for common errors in
app state (including config) when the engine starts.
Global config only.
engine
Controls the 'application engine', including autoreload.
These can only be declared in the global config.
tree
Grafts cherrypy.Application objects onto cherrypy.tree.
These can only be declared in the global config.
hooks
Declares additional request-processing functions.
log
Configures the logging for each application.
These can only be declared in the global or / config.
request
Adds attributes to each Request.
response
Adds attributes to each Response.
server
Controls the default HTTP server via cherrypy.server.
These can only be declared in the global config.
tools
Runs and configures additional request-processing packages.
wsgi
Adds WSGI middleware to an Application's "pipeline".
These can only be declared in the app's root config ("/").
checker
Controls the 'checker', which looks for common errors in
app state (including config) when the engine starts.
Global config only.
The only key that does not exist in a namespace is the "environment" entry.
This special entry 'imports' other config entries from a template stored in
@@ -93,35 +118,103 @@ be any string, and the handler must be either a callable or a (Python 2.5
style) context manager.
"""
import ConfigParser
try:
set
except NameError:
from sets import Set as set
import sys
import cherrypy
from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import reprconf
# Deprecated in CherryPy 3.2--remove in 3.3
NamespaceSet = reprconf.NamespaceSet
environments = {
"staging": {
'engine.autoreload_on': False,
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, text_or_bytes):
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):
raise ValueError(
'Application config must include section headers, but the '
"config you tried to merge doesn't have any sections. "
'Wrap your config in another dict with paths as section '
"headers, for example: {'/': config}.")
base.setdefault(section, {}).update(value_map)
class Config(reprconf.Config):
"""The 'global' configuration data for the entire CherryPy process."""
def update(self, config):
"""Update self from a dict, file or filename."""
if isinstance(config, text_or_bytes):
# Filename
cherrypy.engine.autoreload.files.add(config)
reprconf.Config.update(self, config)
def _apply(self, config):
"""Update self from a 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)
@staticmethod
def __call__(*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):
_Vars(f).setdefault('_cp_config', {}).update(kwargs)
return f
return tool_decorator
class _Vars(object):
"""
Adapter that allows setting a default attribute on a function
or class.
"""
def __init__(self, target):
self.target = target
def setdefault(self, key, default):
if not hasattr(self.target, key):
setattr(self.target, key, default)
return getattr(self.target, key)
# Sphinx begin config.environments
Config.environments = environments = {
'staging': {
'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,
},
'production': {
'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": {
},
'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,
@@ -129,186 +222,35 @@ environments = {
'log.screen': False,
'engine.SIGHUP': None,
'engine.SIGTERM': None,
},
"test_suite": {
'engine.autoreload_on': False,
},
'test_suite': {
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': True,
'request.show_mismatched_params': True,
'log.screen': False,
},
}
def as_dict(config):
"""Return a dict from 'config' whether it is a dict, file, or filename."""
if isinstance(config, basestring):
config = _Parser().dict_from_file(config)
elif hasattr(config, 'read'):
config = _Parser().dict_from_file(config)
return config
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 as_dict(other).iteritems():
base.setdefault(section, {}).update(value_map)
class NamespaceSet(dict):
"""A dict of config namespace names and handlers.
Each config entry should begin with a namespace name; the corresponding
namespace handler will be called once for each config entry in that
namespace, and will be passed two arguments: the config key (with the
namespace removed) and the config value.
Namespace handlers may be any Python callable; they may also be
Python 2.5-style 'context managers', in which case their __enter__
method should return a callable to be used as the handler.
See cherrypy.tools (the Toolbox class) for an example.
"""
def __call__(self, config):
"""Iterate through config and pass it to each namespace handler.
'config' should be a flat dict, where keys use dots to separate
namespaces, and values are arbitrary.
The first name in each config key is used to look up the corresponding
namespace handler. For example, a config entry of {'tools.gzip.on': v}
will call the 'tools' namespace handler with the args: ('gzip.on', v)
"""
# Separate the given config into namespaces
ns_confs = {}
for k in config:
if "." in k:
ns, name = k.split(".", 1)
bucket = ns_confs.setdefault(ns, {})
bucket[name] = config[k]
# I chose __enter__ and __exit__ so someday this could be
# rewritten using Python 2.5's 'with' statement:
# for ns, handler in self.iteritems():
# with handler as callable:
# for k, v in ns_confs.get(ns, {}).iteritems():
# callable(k, v)
for ns, handler in self.iteritems():
exit = getattr(handler, "__exit__", None)
if exit:
callable = handler.__enter__()
no_exc = True
try:
try:
for k, v in ns_confs.get(ns, {}).iteritems():
callable(k, v)
except:
# The exceptional case is handled here
no_exc = False
if exit is None:
raise
if not exit(*sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if no_exc and exit:
exit(None, None, None)
else:
for k, v in ns_confs.get(ns, {}).iteritems():
handler(k, v)
def __repr__(self):
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
dict.__repr__(self))
def __copy__(self):
newobj = self.__class__()
newobj.update(self)
return newobj
copy = __copy__
class Config(dict):
"""The 'global' configuration data for the entire CherryPy process."""
defaults = {
'tools.log_tracebacks.on': True,
'tools.log_headers.on': True,
'tools.trailing_slash.on': True,
}
namespaces = NamespaceSet(
**{"log": lambda k, v: setattr(cherrypy.log, k, v),
"checker": lambda k, v: setattr(cherrypy.checker, k, v),
})
def __init__(self):
self.reset()
def reset(self):
"""Reset self to default values."""
self.clear()
dict.update(self, self.defaults)
def update(self, config):
"""Update self from a dict, file or filename."""
if isinstance(config, basestring):
# Filename
cherrypy.engine.autoreload.files.add(config)
config = _Parser().dict_from_file(config)
elif hasattr(config, 'read'):
# Open file object
config = _Parser().dict_from_file(config)
else:
config = config.copy()
if isinstance(config.get("global", None), dict):
if len(config) > 1:
cherrypy.checker.global_config_contained_paths = True
config = config["global"]
which_env = config.get('environment')
if which_env:
env = environments[which_env]
for k in env:
if k not in config:
config[k] = env[k]
if 'tools.staticdir.dir' in config:
config['tools.staticdir.section'] = "global"
dict.update(self, config)
self.namespaces(config)
def __setitem__(self, k, v):
dict.__setitem__(self, k, v)
self.namespaces({k: v})
},
}
# Sphinx end config.environments
def _server_namespace_handler(k, v):
"""Config handler for the "server" namespace."""
atoms = k.split(".", 1)
atoms = k.split('.', 1)
if len(atoms) > 1:
# Special-case config keys of the form 'server.servername.socket_port'
# to configure additional HTTP servers.
if not hasattr(cherrypy, "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()
@@ -318,98 +260,44 @@ def _server_namespace_handler(k, v):
setattr(cherrypy.servers[servername], k, v)
else:
setattr(cherrypy.server, k, v)
Config.namespaces["server"] = _server_namespace_handler
Config.namespaces['server'] = _server_namespace_handler
def _engine_namespace_handler(k, v):
"""Backward compatibility handler for the "engine" namespace."""
"""Config handler for the "engine" namespace."""
engine = cherrypy.engine
if k == 'autoreload_on':
if v:
engine.autoreload.subscribe()
else:
engine.autoreload.unsubscribe()
elif k == 'autoreload_frequency':
engine.autoreload.frequency = v
elif k == 'autoreload_match':
engine.autoreload.match = v
elif k == 'reload_files':
engine.autoreload.files = set(v)
elif k == 'deadlock_poll_freq':
engine.timeout_monitor.frequency = v
elif k == 'SIGHUP':
engine.listeners['SIGHUP'] = set([v])
if k == 'SIGHUP':
engine.subscribe('SIGHUP', v)
elif k == 'SIGTERM':
engine.listeners['SIGTERM'] = set([v])
elif "." in k:
plugin, attrname = k.split(".", 1)
engine.subscribe('SIGTERM', v)
elif '.' in k:
plugin, attrname = k.split('.', 1)
plugin = getattr(engine, plugin)
if attrname == 'on':
if v and callable(getattr(plugin, 'subscribe', None)):
if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
plugin.subscribe()
return
elif (not v) and callable(getattr(plugin, 'unsubscribe', None)):
elif (
(not v) and
hasattr(getattr(plugin, 'unsubscribe', None), '__call__')
):
plugin.unsubscribe()
return
setattr(plugin, attrname, v)
else:
setattr(engine, k, v)
Config.namespaces["engine"] = _engine_namespace_handler
Config.namespaces['engine'] = _engine_namespace_handler
def _tree_namespace_handler(k, v):
"""Namespace handler for the 'tree' config namespace."""
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
class _Parser(ConfigParser.ConfigParser):
"""Sub-class of ConfigParser that keeps the case of options and that raises
an exception if the file cannot be read.
"""
def optionxform(self, optionstr):
return optionstr
def read(self, filenames):
if isinstance(filenames, basestring):
filenames = [filenames]
for filename in filenames:
# try:
# fp = open(filename)
# except IOError:
# continue
fp = open(filename)
try:
self._read(fp, filename)
finally:
fp.close()
def as_dict(self, raw=False, vars=None):
"""Convert an INI file to a dictionary"""
# Load INI file into a dict
from cherrypy.lib import unrepr
result = {}
for section in self.sections():
if section not in result:
result[section] = {}
for option in self.options(section):
value = self.get(section, option, raw, vars)
try:
value = unrepr(value)
except Exception, x:
msg = ("Config error in section: %r, option: %r, "
"value: %r. Config values must be valid Python." %
(section, option, value))
raise ValueError(msg, x.__class__.__name__, x.args)
result[section][option] = value
return result
def dict_from_file(self, file):
if hasattr(file, 'read'):
self.readfp(file)
else:
self.read(file)
return self.as_dict()
del ConfigParser
if isinstance(v, dict):
for script_name, app in v.items():
cherrypy.tree.graft(app, script_name)
msg = 'Mounted: %s on %s' % (app, script_name or '/')
cherrypy.engine.log(msg)
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

View File

@@ -9,25 +9,61 @@ The default dispatcher discovers the page handler by matching path_info
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
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)
except TypeError, x:
except TypeError:
x = sys.exc_info()[1]
try:
test_callable_spec(self.callable, self.args, self.kwargs)
except cherrypy.HTTPError, error:
raise error
except cherrypy.HTTPError:
raise sys.exc_info()[1]
except:
raise x
raise
@@ -44,7 +80,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.
@@ -52,14 +89,16 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
incorrect, then a 404 Not Found should be raised. Conversely the body
parameters are part of the request; if they are invalid a 400 Bad Request.
"""
show_mismatched_params = getattr(cherrypy.request, 'show_mismatched_params', False)
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
@@ -84,14 +123,16 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
varkw_usage += 1
extra_kwargs.add(key)
# figure out which args have defaults.
args_with_defaults = args[-len(defaults or []):]
for i, val in enumerate(defaults or []):
# Defaults take effect only when the arg hasn't been used yet.
if arg_usage[args[i]] == 0:
arg_usage[args[i]] += 1
if arg_usage[args_with_defaults[i]] == 0:
arg_usage[args_with_defaults[i]] += 1
missing_args = []
multiple_args = []
for key, usage in arg_usage.iteritems():
for key, usage in arg_usage.items():
if usage == 0:
missing_args.append(key)
elif usage > 1:
@@ -104,27 +145,26 @@ 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
if not varargs and vararg_usage > 0:
raise cherrypy.HTTPError(404)
body_params = cherrypy.request.body_params or {}
body_params = cherrypy.serving.request.body.params or {}
body_params = set(body_params.keys())
qs_params = set(callable_kwargs.keys()) - body_params
if multiple_args:
if qs_params.intersection(set(multiple_args)):
# If any of the multiple parameters came from the query string then
# it's a 404 Not Found
@@ -135,8 +175,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:
@@ -146,8 +186,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
@@ -155,8 +195,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)
@@ -164,10 +204,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
@@ -175,24 +221,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.request.params.copy()
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)')
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'
@@ -200,130 +265,171 @@ 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.
"""
__metaclass__ = cherrypy._AttributeDocstrings
dispatch_method_name = '_cp_dispatch'
dispatch_method_name__doc = """
"""
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):
def __init__(self, dispatch_method_name=None,
translate=punctuation_to_underscores):
validate_translator(translate)
self.translate = translate
if dispatch_method_name:
self.dispatch_method_name = dispatch_method_name
def __call__(self, path_info):
"""Set handler and config for the current request."""
request = cherrypy.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]
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.
These virtual path components are passed to the handler as
positional arguments.
"""
request = cherrypy.request
request = cherrypy.serving.request
app = request.app
root = app.root
dispatch_name = self.dispatch_method_name
# Get config for the root object/path.
curpath = ""
fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
fullpath_len = len(fullpath)
segleft = fullpath_len
nodeconf = {}
if hasattr(root, "_cp_config"):
if hasattr(root, '_cp_config'):
nodeconf.update(root._cp_config)
if "/" in app.config:
nodeconf.update(app.config["/"])
object_trail = [['root', root, nodeconf, curpath]]
if '/' in app.config:
nodeconf.update(app.config['/'])
object_trail = [['root', root, nodeconf, segleft]]
node = root
names = [x for x in path.strip('/').split('/') if x] + ['index']
iternames = names[:]
iternames = fullpath[:]
while iternames:
name = iternames[0]
# map to legal Python identifiers (replace '.' with '_')
objname = name.replace('.', '_')
# map to legal Python identifiers (e.g. replace '.' with '_')
objname = name.translate(self.translate)
nodeconf = {}
subnode = getattr(node, objname, None)
pre_len = len(iternames)
if subnode is None:
dispatch = getattr(node, dispatch_name, None)
if dispatch and callable(dispatch) and not \
getattr(dispatch, 'exposed', False):
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.
index_name = iternames.pop()
subnode = dispatch(vpath=iternames)
name = iternames.pop(0)
iternames.append(index_name)
else:
# 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
iternames.pop(0)
segleft = len(iternames)
if segleft > pre_len:
# 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)
iternames.pop(0)
segleft -= 1
node = subnode
if node is not None:
# Get _cp_config attached to this node.
if hasattr(node, "_cp_config"):
if hasattr(node, '_cp_config'):
nodeconf.update(node._cp_config)
# Mix in values from app.config for this path.
curpath = "/".join((curpath, name))
if curpath in app.config:
nodeconf.update(app.config[curpath])
object_trail.append([name, node, nodeconf, curpath])
existing_len = fullpath_len - pre_len
if existing_len != 0:
curpath = '/' + '/'.join(fullpath[0:existing_len])
else:
curpath = ''
new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
for seg in new_segs:
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, curpath in object_trail:
for name, obj, conf, segleft in object_trail:
base.update(conf)
if 'tools.staticdir.dir' in conf:
base['tools.staticdir.section'] = curpath
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 xrange(num_candidates, -1, -1):
name, candidate, nodeconf, curpath = object_trail[i]
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"):
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, curpath])
conf = getattr(defhandler, '_cp_config', {})
object_trail.insert(
i + 1, ['default', defhandler, conf, segleft])
request.config = set_conf()
# See http://www.cherrypy.org/ticket/613
request.is_index = path.endswith("/")
return defhandler, names[i:-1]
# Uncomment the next line to restrict positional params to "default".
# See https://github.com/cherrypy/cherrypy/issues/613
request.is_index = path.endswith('/')
return defhandler, fullpath[fullpath_len - segleft:-1]
# 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()
@@ -337,45 +443,50 @@ class Dispatcher(object):
# Note that this also includes handlers which take
# positional parameters (virtual paths).
request.is_index = False
return candidate, names[i:-1]
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.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()]
if "GET" in avail and "HEAD" not in avail:
avail.append("HEAD")
if 'GET' in avail and 'HEAD' not in avail:
avail.append('HEAD')
avail.sort()
cherrypy.response.headers['Allow'] = ", ".join(avail)
cherrypy.serving.response.headers['Allow'] = ', '.join(avail)
# Find the subhandler
meth = request.method.upper()
func = getattr(resource, meth, None)
if func is None and meth == "HEAD":
func = getattr(resource, "GET", None)
if func is None and meth == 'HEAD':
func = getattr(resource, 'GET', None)
if func:
# 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]
vpath = [x.replace('%2F', '/') for x in vpath]
request.handler = LateParamPageHandler(func, *vpath)
else:
request.handler = cherrypy.HTTPError(405)
@@ -384,9 +495,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
@@ -397,40 +509,40 @@ 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)
if func:
cherrypy.request.handler = LateParamPageHandler(func)
cherrypy.serving.request.handler = LateParamPageHandler(func)
else:
cherrypy.request.handler = cherrypy.NotFound()
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.request
request = cherrypy.serving.request
config = routes.request_config()
config.mapper = self.mapper
if hasattr(cherrypy.request, 'wsgi_environ'):
config.environ = cherrypy.request.wsgi_environ
if hasattr(request, 'wsgi_environ'):
config.environ = request.wsgi_environ
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:
@@ -439,96 +551,106 @@ 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 = ""
curpath = ''
def merge(nodeconf):
if 'tools.staticdir.dir' in nodeconf:
nodeconf['tools.staticdir.section'] = curpath or "/"
nodeconf['tools.staticdir.section'] = curpath or '/'
base.update(nodeconf)
app = request.app
root = app.root
if hasattr(root, "_cp_config"):
if hasattr(root, '_cp_config'):
merge(root._cp_config)
if "/" in app.config:
merge(app.config["/"])
if '/' in app.config:
merge(app.config['/'])
# Mix in values from app.config.
atoms = [x for x in path_info.split("/") if x]
atoms = [x for x in path_info.split('/') if x]
if atoms:
last = atoms.pop()
else:
last = None
for atom in atoms:
curpath = "/".join((curpath, atom))
curpath = '/'.join((curpath, atom))
if curpath in app.config:
merge(app.config[curpath])
handler = None
if result:
controller = result.get('controller', None)
controller = self.controllers.get(controller)
controller = result.get('controller')
controller = self.controllers.get(controller, controller)
if controller:
if isinstance(controller, classtype):
controller = controller()
# Get config from the controller.
if hasattr(controller, "_cp_config"):
if hasattr(controller, '_cp_config'):
merge(controller._cp_config)
action = result.get('action', None)
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))
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):
"""Select a different handler based on the Host header.
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:
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:
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.
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"
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.
``**domains``
A dict of {host header value: virtual prefix} pairs.
The incoming "Host" request header is looked up in this dict,
and, if a match is found, the corresponding "virtual prefix"
value will be prepended to the URL path before calling the
@@ -536,26 +658,28 @@ def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domai
for "example.com" and "www.example.com". In addition, "Host"
headers may contain the port number.
"""
from cherrypy.lib import http
from cherrypy.lib import httputil
def vhost_dispatch(path_info):
header = cherrypy.request.headers.get
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, "")
domain = header('X-Forwarded-Host', domain)
prefix = domains.get(domain, '')
if prefix:
path_info = http.urljoin(prefix, path_info)
path_info = httputil.urljoin(prefix, path_info)
result = next_dispatcher(path_info)
# Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
section = cherrypy.request.config.get('tools.staticdir.section')
# Touch up staticdir config. See
# https://github.com/cherrypy/cherrypy/issues/614.
section = request.config.get('tools.staticdir.section')
if section:
section = section[len(prefix):]
cherrypy.request.config['tools.staticdir.section'] = section
request.config['tools.staticdir.section'] = section
return result
return vhost_dispatch

View File

@@ -1,67 +1,223 @@
"""Error classes for CherryPy."""
"""Exception classes for CherryPy.
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>`
or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the
:attr:`request.handler<cherrypy._cprequest.Request.handler>`.
.. _redirectingpost:
Redirecting POST
================
When you GET a resource and are redirected by the server to another Location,
there's generally no problem since GET is both a "safe method" (there should
be no side-effects) and an "idempotent method" (multiple calls are no different
than a single call).
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:
===== ================================= ===========
300 Multiple Choices Confirm with the user
301 Moved Permanently Confirm with the user
302 Found (Object moved temporarily) Confirm with the user
303 See Other GET the new URI--no confirmation
304 Not modified (for conditional GET only--POST should not raise this error)
305 Use Proxy Confirm with the user
307 Temporary Redirect Confirm with the user
===== ================================= ===========
However, browsers have historically implemented these restrictions poorly;
in particular, many browsers do not force the user to confirm 301, 302
or 307 when redirecting POST. For this reason, CherryPy defaults to 303,
which most user-agents appear to have implemented correctly. Therefore, if
you raise HTTPRedirect for a POST request, the user-agent will most likely
attempt to GET the new URI (without asking for confirmation from the user).
We realize this is confusing for developers, but it's the safest thing we
could do. You are of course free to raise ``HTTPRedirect(uri, status=302)``
or any other 3xx status if you know what you're doing, but given the
environment, we couldn't let any of those be the default.
Custom Error Handling
=====================
.. image:: /refman/cperrors.gif
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://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_.
::
_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
an error_page entry. It will be passed the same status, message, traceback and
version arguments that are interpolated into templates::
def error_page_402(status, message, traceback, version):
return "Error %s - Well, I'm very sorry but you haven't paid!" % status
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.
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
:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide
some other behavior, you generally replace "request.error_response".
Here is some sample code that shows how to display a custom error message and
send an e-mail containing the error::
from cherrypy import _cperror
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.config(**{'request.error_response': handle_error})
class Root:
pass
Note that you have to explicitly set
:attr:`response.body <cherrypy._cprequest.Response.body>`
and not simply return an error message as a result.
"""
import contextlib
from cgi import escape as _escape
from sys import exc_info as _exc_info
from traceback import format_exception as _format_exception
from urlparse import urljoin as _urljoin
from cherrypy.lib import http as _http
from xml.sax import saxutils
import six
from cherrypy._cpcompat import text_or_bytes, 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.
Any request.params must be supplied in a query string.
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.
"""
def __init__(self, path):
def __init__(self, path, query_string=''):
import cherrypy
request = cherrypy.request
self.query_string = ""
if "?" in path:
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)
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(request.path_info, path)
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. If a URL is
absolute, it will be used as-is. If it is relative, it is assumed
to be relative to the current cherrypy.request.path_info.
e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
If a URL is absolute, it will be used as-is. If it is relative, it is
assumed to be relative to the current cherrypy.request.path_info.
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.
"""
def __init__(self, urls, status=None):
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 not native strings"""
def __init__(self, urls, status=None, encoding=None):
import cherrypy
request = cherrypy.request
if isinstance(urls, basestring):
request = cherrypy.serving.request
if isinstance(urls, text_or_bytes):
urls = [urls]
abs_urls = []
for url in urls:
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")
@@ -70,7 +226,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
@@ -82,37 +238,41 @@ class HTTPRedirect(CherryPyException):
else:
status = int(status)
if status < 300 or status > 399:
raise ValueError("status must be between 300 and 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.response
response = cherrypy.serving.response
response.status = status = self.status
if status in (300, 301, 302, 303, 307):
response.headers['Content-Type'] = "text/html"
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]
response.body = "<br />\n".join([msg % (u, u) for u in self.urls])
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>.'
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)
@@ -121,7 +281,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',
@@ -129,7 +289,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.
@@ -137,13 +297,13 @@ class HTTPRedirect(CherryPyException):
elif status == 305:
# Use Proxy.
# self.urls[0] should be the URI of the proxy.
response.headers['Location'] = self.urls[0]
response.headers['Location'] = ntob(self.urls[0], 'utf-8')
response.body = None
# Previous code may have set C-L, so we have to reset it.
response.headers.pop('Content-Length', None)
else:
raise ValueError("The %s status code is unknown." % status)
raise ValueError('The %s status code is unknown.' % status)
def __call__(self):
"""Use this exception as a request.handler (raise self)."""
raise self
@@ -152,18 +312,18 @@ class HTTPRedirect(CherryPyException):
def clean_headers(status):
"""Remove any headers which should not apply to an error response."""
import cherrypy
response = cherrypy.response
response = cherrypy.serving.response
# Remove headers which applied to the original content,
# but do not apply to the error page.
respheaders = response.headers
for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
"Vary", "Content-Encoding", "Content-Length", "Expires",
"Content-Location", "Content-MD5", "Last-Modified"]:
if respheaders.has_key(key):
for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After',
'Vary', 'Content-Encoding', 'Content-Length', 'Expires',
'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
@@ -171,80 +331,118 @@ def clean_headers(status):
# specifies the current length of the selected resource.
# A response with status code 206 (Partial Content) MUST NOT
# include a Content-Range field with a byte-range- resp-spec of "*".
if respheaders.has_key("Content-Range"):
del respheaders["Content-Range"]
if 'Content-Range' in respheaders:
del respheaders['Content-Range']
class HTTPError(CherryPyException):
""" Exception used to return an HTTP error code (4xx-5xx) to the client.
This exception will automatically set the response status and body.
A custom message (a long description to display in the browser)
can be provided in place of the default.
"""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
``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
`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.")
"""
status = None
"""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 = _http.valid_status(status)
except ValueError, x:
raise cherrypy.HTTPError(500, x.args[0])
self.code, self.reason, defaultmsg = _httputil.valid_status(status)
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.")
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.response
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.request.show_tracebacks:
if cherrypy.serving.request.show_tracebacks:
tb = format_exc()
response.headers['Content-Type'] = "text/html"
response.headers.pop('Content-Length', None)
content = self.get_error_page(self.status, traceback=tb,
message=self._message)
response.body = content
response.headers['Content-Length'] = len(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
@classmethod
@contextlib.contextmanager
def handle(cls, exception, status=500, message=''):
"""Translate exception into an HTTPError."""
try:
yield
except exception as exc:
raise cls(status, message or str(exc))
class NotFound(HTTPError):
"""Exception raised when a URL could not be mapped to any handler (404)."""
"""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
path = cherrypy.request.script_name + cherrypy.request.path_info
request = cherrypy.serving.request
path = request.script_name + request.path_info
self.args = (path,)
HTTPError.__init__(self, 404, "The path %r was not found." % path)
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>
@@ -267,73 +465,101 @@ _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 = _http.valid_status(status)
except ValueError, x:
raise cherrypy.HTTPError(500, x.args[0])
code, reason, message = _httputil.valid_status(status)
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:
kwargs['status'] = "%s %s" % (code, reason)
kwargs['status'] = '%s %s' % (code, reason)
if kwargs.get('message') is None:
kwargs['message'] = message
if kwargs.get('traceback') is None:
kwargs['traceback'] = ''
if kwargs.get('version') is None:
kwargs['version'] = cherrypy.__version__
for k, v in kwargs.iteritems():
for k, v in iteritems(kwargs):
if v is None:
kwargs[k] = ""
kwargs[k] = ''
else:
kwargs[k] = _escape(kwargs[k])
# Use a custom template or callable for the error page?
pages = cherrypy.request.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 callable(error_page):
return error_page(**kwargs)
if hasattr(error_page, '__call__'):
# 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, six.text_type):
return result.encode('utf-8')
else:
if not isinstance(result, bytes):
raise ValueError('error page function did not '
'return a bytestring, six.text_typeing or an '
'iterator - returned object of type %s.'
% (type(result).__name__))
return result
else:
return file(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']
if m:
m += "<br />"
m += "In addition, the custom error page failed:\n<br />%s" % e
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.response
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
@@ -349,44 +575,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'] = len(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:
body += "\n" + extrabody
return ("500 Internal Server Error",
[('Content-Type', 'text/plain'),
('Content-Length', str(len(body)))],
if not isinstance(extrabody, bytes):
extrabody = extrabody.encode('utf-8')
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])

View File

@@ -1,39 +1,197 @@
"""CherryPy logging."""
"""
Simple config
=============
Although CherryPy uses the :mod:`Python logging module <logging>`, it does so
behind the scenes so that simple logging is simple, but complicated logging
is still possible. "Simple" logging means that you can log to the screen
(i.e. console/stdout) or to a file, and that you can easily have separate
error and access log files.
Here are the simplified logging settings. You use these by adding lines to
your config file or dict. You should set these at either the global level or
per application (see next), but generally not both.
* ``log.screen``: Set this to True to have both "error" and "access" messages
printed to stdout.
* ``log.access_file``: Set this to an absolute filename where you want
"access" messages written.
* ``log.error_file``: Set this to an absolute filename where you want "error"
messages written.
Many events are automatically logged; to log your own application events, call
:func:`cherrypy.log`.
Architecture
============
Separate scopes
---------------
CherryPy provides log managers at both the global and application layers.
This means you can have one set of logging rules for your entire site,
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
:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used
: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
"cherrypy.access.2378745" (the number is the id of the Application object).
This means that the application logs "bubble up" to the site logs, so if your
application has no log handlers, the site-level handlers will still log the
messages.
Errors vs. Access
-----------------
Each log manager handles both "access" messages (one per HTTP request) and
"error" messages (everything else). Note that the "error" log is not just for
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
===============
The simple settings above work by manipulating Python's standard :mod:`logging`
module. So when you need something more complex, the full power of the standard
module is yours to exploit. You can borrow or create custom handlers, formats,
filters, and much more. Here's an example that skips the standard FileHandler
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)
h.setLevel(DEBUG)
h.setFormatter(_cplogging.logfmt)
log.access_log.addHandler(h)
The ``rot_*`` attributes are pulled straight from the application log object.
Since "log.*" config entries simply set attributes on the log object, you can
add custom attributes to your heart's content. Note that these handlers are
used ''instead'' of the default, simple handlers outlined above (so don't set
the "log.error_file" config entry, for example).
"""
import datetime
import logging
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
logging.Logger.manager.emittedNoHandlerWarning = 1
logfmt = logging.Formatter("%(message)s")
import os
import rfc822
import sys
import six
import cherrypy
from cherrypy import _cperror
from cherrypy._cpcompat import ntob
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
logging.Logger.manager.emittedNoHandlerWarning = 1
logfmt = logging.Formatter('%(message)s')
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
access_log_format = \
"""The actual :class:`logging.Logger` instance for access messages."""
access_log_format = (
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
if six.PY3 else
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
def __init__(self, appid=None, logger_root="cherrypy"):
)
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
if appid is None:
self.error_log = logging.getLogger("%s.error" % logger_root)
self.access_log = logging.getLogger("%s.access" % logger_root)
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.setLevel(logging.DEBUG)
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):
@@ -43,28 +201,38 @@ 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):
"""Write to the error log.
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``.
"""
exc_info = None
if traceback:
msg += _cperror.format_exc()
self.error_log.log(severity, ' '.join((self.time(), context, msg)))
exc_info = _cperror._exc_info()
self.error_log.log(severity, ' '.join((self.time(), context, msg)), exc_info=exc_info)
def __call__(self, *args, **kwargs):
"""Write to the error log.
This is not just for errors! Applications may call this at any time
to log application-specific information.
"""
"""An alias for ``error``."""
return self.error(*args, **kwargs)
def access(self):
"""Write to the access log (in Apache/NCSA Combined Log format).
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
@@ -72,88 +240,122 @@ class LogManager(object):
escaped by prepending a backslash, and all whitespace characters,
which are written in their C-style notation (\\n, \\t, etc).
"""
request = cherrypy.request
request = cherrypy.serving.request
remote = request.remote
response = cherrypy.response
response = cherrypy.serving.response
outheaders = response.headers
inheaders = request.headers
if response.output_status is None:
status = '-'
else:
status = response.output_status.split(ntob(' '), 1)[0]
if six.PY3:
status = status.decode('ISO-8859-1')
atoms = {'h': remote.name or remote.ip,
'l': '-',
'u': getattr(request, "login", None) or "-",
'u': getattr(request, 'login', None) or '-',
't': self.time(),
'r': request.request_line,
's': response.status.split(" ", 1)[0],
'b': outheaders.get('Content-Length', '') or "-",
'f': inheaders.get('Referer', ''),
'a': inheaders.get('User-Agent', ''),
's': status,
'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 six.PY3:
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, six.text_type):
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()
month = rfc822._monthnames[now.month - 1].capitalize()
monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun',
'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
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:
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")
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"
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")
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="If True, error and access will print to stderr.")
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"
h._cpbuiltin = 'file'
log.addHandler(h)
def _set_file_handler(self, log, filename):
h = self._get_builtin_handler(log, "file")
h = self._get_builtin_handler(log, 'file')
if filename:
if h:
if h.baseFilename != os.path.abspath(filename):
@@ -166,80 +368,97 @@ 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")
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")
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")
h = self._get_builtin_handler(log, 'wsgi')
if enable:
if not h:
h = WSGIErrorHandler()
h.setFormatter(logfmt)
h._cpbuiltin = "wsgi"
h._cpbuiltin = 'wsgi'
log.addHandler(h)
elif h:
log.handlers.remove(h)
def _get_wsgi(self):
return bool(self._get_builtin_handler(self.error_log, "wsgi"))
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="If True, error messages will be sent 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``).
If you set it to False, it will remove the handler.
""")
class WSGIErrorHandler(logging.Handler):
"A handler class which writes logging records to environ['wsgi.errors']."
def flush(self):
"""Flushes the stream."""
try:
stream = cherrypy.request.wsgi_environ.get('wsgi.errors')
stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
except (AttributeError, KeyError):
pass
else:
stream.flush()
def emit(self, record):
"""Emit a record."""
try:
stream = cherrypy.request.wsgi_environ.get('wsgi.errors')
stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
except (AttributeError, KeyError):
pass
else:
try:
msg = self.format(record)
fs = "%s\n"
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:
stream.write(fs % msg)
except UnicodeError:
stream.write(fs % msg.encode("UTF-8"))
stream.write(fs % msg.encode('UTF-8'))
self.flush()
except:
self.handleError(record)

View File

@@ -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
@@ -55,47 +55,51 @@ resides in the global site-package this won't be needed.
Then restart apache2 and access http://127.0.0.1:8080
"""
import io
import logging
import StringIO
import os
import re
import sys
import cherrypy
from cherrypy._cpcompat import copyitems, ntob
from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import http
from cherrypy.lib import httputil
# ------------------------------ Request-handling
def setup(req):
from mod_python import apache
# Run any setup function 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:
atoms = options['cherrypy.setup'].split('::', 1)
if len(atoms) == 1:
mod = __import__(atoms[0], globals(), locals())
else:
modname, fname = atoms
mod = __import__(modname, globals(), locals(), [fname])
func = getattr(mod, fname)
func()
for function in options['cherrypy.setup'].split():
atoms = function.split('::', 1)
if len(atoms) == 1:
mod = __import__(atoms[0], globals(), locals())
else:
modname, fname = atoms
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'],
'tools.ignore_headers.on': True,
'tools.ignore_headers.headers': ['Range'],
})
engine = cherrypy.engine
if hasattr(engine, "signal_handler"):
if hasattr(engine, 'signal_handler'):
engine.signal_handler.unsubscribe()
if hasattr(engine, "console_control_handler"):
if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.unsubscribe()
engine.autoreload.unsubscribe()
cherrypy.server.unsubscribe()
def _log(msg, level):
newlevel = apache.APLOG_ERR
if logging.DEBUG >= level:
@@ -105,13 +109,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:
@@ -123,6 +127,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)
@@ -131,6 +136,8 @@ class _ReadOnlyRequest:
recursive = False
_isSetUp = False
def handler(req):
from mod_python import apache
try:
@@ -138,16 +145,18 @@ def handler(req):
if not _isSetUp:
setup(req)
_isSetUp = True
# Obtain a Request object from CherryPy
local = req.connection.local_addr
local = http.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 = http.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
@@ -156,74 +165,78 @@ def handler(req):
except AttributeError:
bad_value = ("You must provide a PythonOption '%s', "
"either 'on' or 'off', when running a version "
"of mod_python < 3.1")
'of mod_python < 3.1')
threaded = options.get('multithread', '').lower()
if threaded == 'on':
threaded = True
elif threaded == 'off':
threaded = False
else:
raise ValueError(bad_value % "multithread")
raise ValueError(bad_value % 'multithread')
forked = options.get('multiprocess', '').lower()
if forked == 'on':
forked = True
elif forked == 'off':
forked = False
else:
raise ValueError(bad_value % "multiprocess")
sn = cherrypy.tree.script_name(req.uri or "/")
raise ValueError(bad_value % 'multiprocess')
sn = cherrypy.tree.script_name(req.uri or '/')
if sn is None:
send_response(req, '404 Not Found', [], '')
else:
app = cherrypy.tree.apps[sn]
method = req.method
path = req.uri
qs = req.args or ""
qs = req.args or ''
reqproto = req.protocol
headers = req.headers_in.items()
headers = copyitems(req.headers_in)
rfile = _ReadOnlyRequest(req)
prev = None
try:
redirections = []
while True:
request, response = app.get_serving(local, remote, scheme,
"HTTP/1.1")
'HTTP/1.1')
request.login = req.user
request.multithread = bool(threaded)
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)
break
except cherrypy.InternalRedirect, ir:
except cherrypy.InternalRedirect:
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
qs = '?' + qs
redirections.append(sn + path + qs)
# Munge environment and try again.
method = "GET"
method = 'GET'
path = ir.path
qs = ir.query_string
rfile = StringIO.StringIO()
send_response(req, response.status, response.header_list,
response.body, response.stream)
rfile = io.BytesIO()
send_response(
req, response.output_status, response.header_list,
response.body, response.stream)
finally:
app.release_serving()
except:
@@ -237,41 +250,53 @@ 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"
req.content_type = 'text/plain'
for header, value in headers:
if header.lower() == 'content-type':
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):
if isinstance(body, text_or_bytes):
req.write(body)
else:
for seg in body:
req.write(seg)
# --------------- Startup tools for CherryPy + mod_python --------------- #
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
import os
import re
def read_process(cmd, args=""):
pipein, pipeout = os.popen4("%s %s" % (cmd, args))
def read_process(cmd, args=''):
fullcmd = '%s %s' % (cmd, args)
pipeout = popen(fullcmd)
try:
firstline = pipeout.readline()
if (re.search(r"(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:
@@ -280,7 +305,7 @@ def read_process(cmd, args=""):
class ModPythonServer(object):
template = """
# Apache2 server configuration file for running CherryPy with mod_python.
@@ -295,36 +320,35 @@ 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"):
def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
handler='cherrypy._cpmodpy::handler'):
self.loc = loc
self.port = port
self.opts = opts
self.apache_path = apache_path
self.handler = handler
def start(self):
opts = "".join([" PythonOption %s %s\n" % (k, v)
opts = ''.join([' PythonOption %s %s\n' % (k, v)
for k, v in self.opts])
conf_data = self.template % {"port": self.port,
"loc": self.loc,
"opts": opts,
"handler": self.handler,
conf_data = self.template % {'port': self.port,
'loc': self.loc,
'opts': opts,
'handler': self.handler,
}
mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
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)
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
def stop(self):
os.popen('apache -k stop')
self.ready = False

View File

@@ -0,0 +1,154 @@
"""Native adapter for serving CherryPy via its builtin server."""
import logging
import sys
import io
import cherrypy
from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import httputil
from cherrypy import wsgiserver
class NativeGateway(wsgiserver.Gateway):
recursive = False
def respond(self):
req = self.req
try:
# Obtain a Request object from CherryPy
local = req.server.bind_addr
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:
self.send_response('404 Not Found', [], [''])
else:
app = cherrypy.tree.apps[sn]
method = req.method
path = req.path
qs = req.qs or ''
headers = req.inheaders.items()
rfile = req.rfile
prev = None
try:
redirections = []
while True:
request, response = app.get_serving(
local, remote, scheme, 'HTTP/1.1')
request.multithread = True
request.multiprocess = False
request.app = app
request.prev = prev
# Run the CherryPy Request object and obtain the
# response
try:
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)
else:
# 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 = io.BytesIO()
self.send_response(
response.output_status, response.header_list,
response.body)
finally:
app.release_serving()
except:
tb = format_exc()
# 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.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)
self.ssl_adapter = adapter_class(
self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain)
self.ssl_adapter.context = self.server_adapter.ssl_context
elif self.server_adapter.ssl_certificate:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
self.ssl_adapter = adapter_class(
self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain)

1006
cherrypy/_cpreqbody.py Normal file
View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
"""Manage HTTP servers with CherryPy."""
import warnings
import six
import cherrypy
from cherrypy.lib import attributes
from cherrypy.lib.reprconf import attributes
from cherrypy._cpcompat import text_or_bytes
# We import * because we want to export check_port
# et al as attributes of this module.
@@ -11,66 +12,143 @@ 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:
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).")
'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'
reverse_dns = False
"""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.
"""
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".
"""
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."""
if six.PY3:
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,
which declares it covers WSGI version 1.0.1 but still mandates the
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 quickstart(self, server=None):
"""This does nothing now and will be removed in 3.2."""
warnings.warn('quickstart does nothing now and will be removed in '
'3.2. Call cherrypy.engine.start() instead.',
DeprecationWarning)
def httpserver_from_self(self, httpserver=None):
"""Return a (httpserver, bind_addr) pair based on self attributes."""
if httpserver is None:
@@ -78,30 +156,31 @@ class Server(ServerAdapter):
if httpserver is None:
from cherrypy import _cpwsgi_server
httpserver = _cpwsgi_server.CPWSGIServer(self)
if isinstance(httpserver, basestring):
if isinstance(httpserver, text_or_bytes):
# 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
self.socket_host = None
self.socket_port = None
elif isinstance(value, basestring):
elif isinstance(value, text_or_bytes):
self.socket_file = value
self.socket_host = None
self.socket_port = None
@@ -110,16 +189,21 @@ class Server(ServerAdapter):
self.socket_host, self.socket_port = value
self.socket_file = None
except ValueError:
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)
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.')
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.
@@ -127,17 +211,16 @@ 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:
host += ":%s" % port
else:
scheme = "http"
if port != 80:
host += ":%s" % port
return "%s://%s" % (scheme, host)
port = self.socket_port
if self.ssl_certificate:
scheme = 'https'
if port != 443:
host += ':%s' % port
else:
scheme = 'http'
if port != 80:
host += ':%s' % port
return '%s://%s' % (scheme, host)

View File

@@ -1,239 +0,0 @@
# This is a backport of Python-2.4's threading.local() implementation
"""Thread-local objects
(Note that this module provides a Python version of thread
threading.local class. Depending on the version of Python you're
using, there may be a faster one available. You should always import
the local class from threading.)
Thread-local objects support the management of thread-local data.
If you have data that you want to be local to a thread, simply create
a thread-local object and use its attributes:
>>> mydata = local()
>>> mydata.number = 42
>>> mydata.number
42
You can also access the local-object's dictionary:
>>> mydata.__dict__
{'number': 42}
>>> mydata.__dict__.setdefault('widgets', [])
[]
>>> mydata.widgets
[]
What's important about thread-local objects is that their data are
local to a thread. If we access the data in a different thread:
>>> log = []
>>> def f():
... items = mydata.__dict__.items()
... items.sort()
... log.append(items)
... mydata.number = 11
... log.append(mydata.number)
>>> import threading
>>> thread = threading.Thread(target=f)
>>> thread.start()
>>> thread.join()
>>> log
[[], 11]
we get different data. Furthermore, changes made in the other thread
don't affect data seen in this thread:
>>> mydata.number
42
Of course, values you get from a local object, including a __dict__
attribute, are for whatever thread was current at the time the
attribute was read. For that reason, you generally don't want to save
these values across threads, as they apply only to the thread they
came from.
You can create custom local objects by subclassing the local class:
>>> class MyLocal(local):
... number = 2
... initialized = False
... def __init__(self, **kw):
... if self.initialized:
... raise SystemError('__init__ called too many times')
... self.initialized = True
... self.__dict__.update(kw)
... def squared(self):
... return self.number ** 2
This can be useful to support default values, methods and
initialization. Note that if you define an __init__ method, it will be
called each time the local object is used in a separate thread. This
is necessary to initialize each thread's dictionary.
Now if we create a local object:
>>> mydata = MyLocal(color='red')
Now we have a default number:
>>> mydata.number
2
an initial color:
>>> mydata.color
'red'
>>> del mydata.color
And a method that operates on the data:
>>> mydata.squared()
4
As before, we can access the data in a separate thread:
>>> log = []
>>> thread = threading.Thread(target=f)
>>> thread.start()
>>> thread.join()
>>> log
[[('color', 'red'), ('initialized', True)], 11]
without affecting this thread's data:
>>> mydata.number
2
>>> mydata.color
Traceback (most recent call last):
...
AttributeError: 'MyLocal' object has no attribute 'color'
Note that subclasses can define slots, but they are not thread
local. They are shared across threads:
>>> class MyLocal(local):
... __slots__ = 'number'
>>> mydata = MyLocal()
>>> mydata.number = 42
>>> mydata.color = 'red'
So, the separate thread:
>>> thread = threading.Thread(target=f)
>>> thread.start()
>>> thread.join()
affects what we see:
>>> mydata.number
11
>>> del mydata
"""
# Threading import is at end
class _localbase(object):
__slots__ = '_local__key', '_local__args', '_local__lock'
def __new__(cls, *args, **kw):
self = object.__new__(cls)
key = 'thread.local.' + str(id(self))
object.__setattr__(self, '_local__key', key)
object.__setattr__(self, '_local__args', (args, kw))
object.__setattr__(self, '_local__lock', RLock())
if args or kw and (cls.__init__ is object.__init__):
raise TypeError("Initialization arguments are not supported")
# We need to create the thread dict in anticipation of
# __init__ being called, to make sure we don't call it
# again ourselves.
dict = object.__getattribute__(self, '__dict__')
currentThread().__dict__[key] = dict
return self
def _patch(self):
key = object.__getattribute__(self, '_local__key')
d = currentThread().__dict__.get(key)
if d is None:
d = {}
currentThread().__dict__[key] = d
object.__setattr__(self, '__dict__', d)
# we have a new instance dict, so call out __init__ if we have
# one
cls = type(self)
if cls.__init__ is not object.__init__:
args, kw = object.__getattribute__(self, '_local__args')
cls.__init__(self, *args, **kw)
else:
object.__setattr__(self, '__dict__', d)
class local(_localbase):
def __getattribute__(self, name):
lock = object.__getattribute__(self, '_local__lock')
lock.acquire()
try:
_patch(self)
return object.__getattribute__(self, name)
finally:
lock.release()
def __setattr__(self, name, value):
lock = object.__getattribute__(self, '_local__lock')
lock.acquire()
try:
_patch(self)
return object.__setattr__(self, name, value)
finally:
lock.release()
def __delattr__(self, name):
lock = object.__getattribute__(self, '_local__lock')
lock.acquire()
try:
_patch(self)
return object.__delattr__(self, name)
finally:
lock.release()
def __del__():
threading_enumerate = enumerate
__getattribute__ = object.__getattribute__
def __del__(self):
key = __getattribute__(self, '_local__key')
try:
threads = list(threading_enumerate())
except:
# if enumerate fails, as it seems to do during
# shutdown, we'll skip cleanup under the assumption
# that there is nothing to clean up
return
for thread in threads:
try:
__dict__ = thread.__dict__
except AttributeError:
# Thread is dying, rest in peace
continue
if key in __dict__:
try:
del __dict__[key]
except KeyError:
pass # didn't have anything in this thread
return __del__
__del__ = __del__()
from threading import currentThread, enumerate, RLock

View File

@@ -2,19 +2,19 @@
Tools are usually designed to be used in a variety of ways (although some
may only offer one if they choose):
Library calls:
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:
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:
CherryPy config
If a tool exposes a "_setup" callable, it will be called
once per Request (if the feature is "turned on" via config).
@@ -22,27 +22,48 @@ Tools may be implemented as any object with a namespace. The builtins
are generally either modules or instances of the tools.Tool class.
"""
import sys
import warnings
import cherrypy
from cherrypy._helper import expose
from cherrypy.lib import cptools, encoding, auth, static, jsontools
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
def _getargs(func):
"""Return the names of all static arguments to the given function."""
# Use this instead of importing inspect for less mem overhead.
import types
if isinstance(func, types.MethodType):
func = func.im_func
co = func.func_code
if sys.version_info >= (3, 0):
if isinstance(func, types.MethodType):
func = func.__func__
co = func.__code__
else:
if isinstance(func, types.MethodType):
func = func.im_func
co = func.func_code
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.'
)
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"
namespace = 'tools'
def __init__(self, point, callable, name=None, priority=50):
self._point = point
self.callable = callable
@@ -50,14 +71,21 @@ 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:
for arg in _getargs(self.callable):
setattr(self, arg, None)
except (TypeError, AttributeError):
if hasattr(self.callable, "__call__"):
if hasattr(self.callable, '__call__'):
for arg in _getargs(self.callable.__call__):
setattr(self, arg, None)
# IronPython 1.0 raises NotImplementedError because
@@ -69,64 +97,66 @@ 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.request.toolmaps[self.namespace]
tm = cherrypy.serving.request.toolmaps[self.namespace]
if self._name in tm:
conf.update(tm[self._name])
if "on" in conf:
del conf["on"]
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:
For example::
@expose
@tools.proxy()
def whats_my_base(self):
return cherrypy.request.base
whats_my_base.exposed = True
"""
if args:
raise TypeError("The %r Tool does not accept positional "
"arguments; you must use keyword arguments."
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"):
if not hasattr(f, '_cp_config'):
f._cp_config = {}
subspace = self.namespace + "." + self._name + "."
f._cp_config[subspace + "on"] = True
for k, v in kwargs.iteritems():
subspace = self.namespace + '.' + self._name + '.'
f._cp_config[subspace + 'on'] = True
for k, v in kwargs.items():
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.
"""
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
cherrypy.request.hooks.attach(self._point, self.callable,
priority=p, **conf)
p = getattr(self.callable, 'priority', self._priority)
cherrypy.serving.request.hooks.attach(self._point, self.callable,
priority=p, **conf)
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
@@ -134,54 +164,57 @@ 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:
For example::
class Root:
nav = tools.staticdir.handler(section="/nav", dir="nav",
root=absDir)
"""
@expose
def handle_func(*a, **kw):
handled = self.callable(*args, **self._merged_args(kwargs))
if not handled:
raise cherrypy.NotFound()
return cherrypy.response.body
handle_func.exposed = True
return cherrypy.serving.response.body
return handle_func
def _wrapper(self, **kwargs):
if self.callable(**kwargs):
cherrypy.request.handler = None
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.
"""
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
cherrypy.request.hooks.attach(self._point, self._wrapper,
priority=p, **conf)
p = getattr(self.callable, 'priority', self._priority)
cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
priority=p, **conf)
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
'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:
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)
@@ -189,83 +222,86 @@ 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):
innerfunc = cherrypy.request.handler
def callable(self, *args, **kwargs):
innerfunc = cherrypy.serving.request.handler
def wrap(*args, **kwargs):
return self.newhandler(innerfunc, *args, **kwargs)
cherrypy.request.handler = wrap
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.
"""
cherrypy.request.error_response = self._wrapper
cherrypy.serving.request.error_response = self._wrapper
# Builtin tools #
from cherrypy.lib import cptools, encoding, auth, static, tidy
from cherrypy.lib import sessions as _sessions, xmlrpc as _xmlrpc
from cherrypy.lib import caching as _caching, wsgiapp as _wsgiapp
class SessionTool(Tool):
"""Session Tool for CherryPy.
sessions.locking:
sessions.locking
When 'implicit' (the default), the session will be locked for you,
just before running the page handler.
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).
body. This is off by default for safety reasons; for example,
a large upload would block the session, denying an AJAX
progress meter
(`issue <https://github.com/cherrypy/cherrypy/issues/630>`_).
When 'explicit' (or any other value), you need to call
cherrypy.session.acquire_lock() yourself before using
session data.
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.request.hooks
hooks = cherrypy.serving.request.hooks
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
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)
@@ -276,35 +312,34 @@ 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().iteritems()
conf = dict([(k, v) for k, v in self._merged_args().items()
if k in ('path', 'path_header', 'name', 'timeout',
'domain', 'secure')])
_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:
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
@@ -312,125 +347,93 @@ 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:
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}
@expose
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):
if subhandler and getattr(subhandler, 'exposed', False):
body = subhandler(*(vpath + rpcparams), **params)
else:
# http://www.cherrypy.org/ticket/533
# https://github.com/cherrypy/cherrypy/issues/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
raise Exception, 'method "%s" is not supported' % attr
conf = cherrypy.request.toolmaps['tools'].get("xmlrpc", {})
# 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'),
conf.get('allow_none', 0))
return cherrypy.response.body
default.exposed = True
class WSGIAppTool(HandlerTool):
"""A tool for running any WSGI middleware/application within CP.
Here are the parameters:
wsgi_app - any wsgi application callable
env_update - a dictionary with arbitrary keys and values to be
merged with the WSGI environ dictionary.
Example:
class Whatever:
_cp_config = {'tools.wsgiapp.on': True,
'tools.wsgiapp.app': some_app,
'tools.wsgiapp.env': app_environ,
}
"""
def _setup(self):
# Keep request body intact so the wsgi app can have its way with it.
cherrypy.request.process_request_body = False
HandlerTool._setup(self)
return cherrypy.serving.response.body
class SessionAuthTool(HandlerTool):
def _setargs(self):
for name in dir(cptools.SessionAuth):
if not name.startswith("__"):
if not name.startswith('__'):
setattr(self, name, None)
class CachingTool(Tool):
"""Caching Tool for CherryPy."""
def _wrapper(self, invalid_methods=("POST", "PUT", "DELETE"), **kwargs):
request = cherrypy.request
if not hasattr(cherrypy, "_cache"):
# Make a process-wide Cache object.
cherrypy._cache = kwargs.pop("cache_class", _caching.MemoryCache)()
# Take all remaining kwargs and set them on the Cache object.
for k, v in kwargs.iteritems():
setattr(cherrypy._cache, k, v)
if _caching.get(invalid_methods=invalid_methods):
def _wrapper(self, **kwargs):
request = cherrypy.serving.request
if _caching.get(**kwargs):
request.handler = None
else:
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.request.hooks.attach('before_handler', self._wrapper,
priority=p, **conf)
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):
@@ -438,28 +441,58 @@ 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.request.toolmaps[self.namespace] = map = {}
cherrypy.serving.request.toolmaps[self.namespace] = map = {}
def populate(k, v):
toolname, arg = k.split(".", 1)
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.request.toolmaps.get(self.namespace)
map = cherrypy.serving.request.toolmaps.get(self.namespace)
if map:
for name, settings in map.items():
if settings.get("on", False):
if settings.get('on', False):
tool = getattr(self, name)
tool._setup()
def register(self, point, **kwargs):
"""Return a decorator which registers the function at the given hook point."""
def decorator(func):
setattr(self, kwargs.get('name', func.__name__), Tool(point, func, **kwargs))
return func
return decorator
default_toolbox = _d = Toolbox("tools")
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)
default_toolbox = _d = Toolbox('tools')
_d.session_auth = SessionAuthTool(cptools.session_auth)
_d.allow = Tool('on_start_resource', cptools.allow)
_d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
_d.response_headers = Tool('on_start_resource', cptools.response_headers)
_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback)
@@ -467,19 +500,26 @@ _d.log_headers = Tool('before_error_response', cptools.log_request_headers)
_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100)
_d.err_redirect = ErrorTool(cptools.redirect)
_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75)
_d.decode = Tool('before_handler', encoding.decode)
_d.decode = Tool('before_request_body', encoding.decode)
# the order of encoding, gzip, caching is important
_d.encode = Tool('before_finalize', encoding.encode, priority=70)
_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70)
_d.gzip = Tool('before_finalize', encoding.gzip, priority=80)
_d.staticdir = HandlerTool(static.staticdir)
_d.staticfile = HandlerTool(static.staticfile)
_d.sessions = SessionTool()
_d.xmlrpc = ErrorTool(_xmlrpc.on_error)
_d.wsgiapp = WSGIAppTool(_wsgiapp.run)
_d.caching = CachingTool('before_handler', _caching.get, 'caching')
_d.expires = Tool('before_finalize', _caching.expires)
_d.tidy = Tool('before_finalize', tidy.tidy)
_d.nsgmls = Tool('before_finalize', tidy.nsgmls)
_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)
@@ -488,5 +528,11 @@ _d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
_d.flatten = Tool('before_finalize', cptools.flatten)
_d.accept = Tool('on_start_resource', cptools.accept)
_d.redirect = Tool('on_start_resource', cptools.redirect)
_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0)
_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30)
_d.json_out = Tool('before_handler', jsontools.json_out, priority=30)
_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1)
_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1)
_d.params = Tool('before_handler', cptools.convert_params)
del _d, cptools, encoding, auth, static, tidy
del _d, cptools, encoding, auth, static

View File

@@ -1,248 +1,287 @@
"""CherryPy Application and Tree objects."""
import os
import six
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
from cherrypy.lib import http as _http
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.
"""
__metaclass__ = cherrypy._AttributeDocstrings
root = None
root__doc = """
The top-most container of page handlers for this app. Handlers should
"""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 = {}
config__doc = """
A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
"""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
log__doc = """A LogManager instance. See _cplogging."""
"""A LogManager instance. See _cplogging."""
wsgiapp = None
wsgiapp__doc = """A CPWSGIApp instance. See _cpwsgi."""
"""A CPWSGIApp instance. See _cpwsgi."""
request_class = _cprequest.Request
response_class = _cprequest.Response
relative_urls = False
def __init__(self, root, script_name="", config=None):
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.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__,
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.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("/")
value = value.rstrip('/')
self._script_name = value
script_name = property(fget=_get_script_name, fset=_set_script_name,
doc=script_name__doc)
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("/", {}))
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
elif lastslash == 0 and trail != '/':
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.iteritems():
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 = {}
apps__doc = """
"""
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):
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
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.
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.
config
A file or dict containing application config.
"""
if script_name is None:
raise TypeError(
"The 'script_name' argument may not be None. Application "
"objects may, however, possess a script_name of None (in "
"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.")
'objects may, however, possess a script_name of None (in '
'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("/")
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"
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')
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")):
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")
'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=""):
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("/")
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:
try:
path = _http.urljoin(cherrypy.request.script_name,
cherrypy.request.path_info)
request = cherrypy.serving.request
path = httputil.urljoin(request.script_name,
request.path_info)
except AttributeError:
return None
while True:
if path in self.apps:
return path
if path == "":
if path == '':
return None
# Move one node up the tree and try again.
path = path[:path.rfind("/")]
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 '').
# Try to look up the app using the full path.
path = _http.urljoin(environ.get('SCRIPT_NAME', ''),
environ.get('PATH_INFO', ''))
sn = self.script_name(path or "/")
env1x = environ
if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
env1x.get('PATH_INFO', ''))
sn = self.script_name(path or '/')
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()
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
if six.PY2 and 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:
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
return app(environ, start_response)

View File

@@ -1,237 +1,327 @@
"""WSGI interface (see PEP 333)."""
"""WSGI interface (see PEP 333 and 3333).
Note that WSGI environ keys and values are 'native strings'; that is,
whatever the type of "" is. For Python 2, that's a byte string; for Python 3,
it's a unicode string. But PEP 3333 says: "even if Python's str type is
actually Unicode "under the hood", the content of native strings must
still be translatable to bytes via the Latin-1 encoding!"
"""
import StringIO as _StringIO
import sys as _sys
import io
import six
import cherrypy as _cherrypy
from cherrypy._cpcompat import ntob, ntou
from cherrypy import _cperror
from cherrypy.lib import http as _http
from cherrypy.lib import httputil
from cherrypy.lib import is_closable_iterator
def downgrade_wsgi_ux_to_1x(environ):
"""Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.
"""
env1x = {}
url_encoding = environ[ntou('wsgi.url_encoding')]
for k, v in list(environ.items()):
if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
v = v.encode(url_encoding)
elif isinstance(v, six.text_type):
v = v.encode('ISO-8859-1')
env1x[k.encode('ISO-8859-1')] = v
return env1x
class VirtualHost(object):
"""Select a different WSGI application 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 applications. For example:
It allows several domains to point to different applications. For example::
root = Root()
RootApp = cherrypy.Application(root)
Domain2App = cherrypy.Application(root)
SecureApp = cherrypy.Application(Secure())
vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
domains={'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
})
vhost = cherrypy._cpwsgi.VirtualHost(
RootApp,
domains={
'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
},
)
cherrypy.tree.graft(vhost)
default: required. The default WSGI application.
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: application} pairs.
The incoming "Host" request header is looked up in this dict,
and, if a match is found, the corresponding WSGI application
will be called instead of the default. Note that you often need
separate entries for "example.com" and "www.example.com".
In addition, "Host" headers may contain the port number.
"""
default = None
"""Required. The default WSGI application."""
use_x_forwarded_host = True
"""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: application} pairs.
The incoming "Host" request header is looked up in this dict,
and, if a match is found, the corresponding WSGI application
will be called instead of the default. Note that you often need
separate entries for "example.com" and "www.example.com".
In addition, "Host" headers may contain the port number.
"""
def __init__(self, default, domains=None, use_x_forwarded_host=True):
self.default = default
self.domains = domains or {}
self.use_x_forwarded_host = use_x_forwarded_host
def __call__(self, environ, start_response):
domain = environ.get('HTTP_HOST', '')
if self.use_x_forwarded_host:
domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
domain = environ.get('HTTP_X_FORWARDED_HOST', domain)
nextapp = self.domains.get(domain)
if nextapp is None:
nextapp = self.default
return nextapp(environ, start_response)
class InternalRedirector(object):
"""WSGI middleware that handles raised cherrypy.InternalRedirect."""
def __init__(self, nextapp, recursive=False):
self.nextapp = nextapp
self.recursive = recursive
def __call__(self, environ, start_response):
redirections = []
while True:
environ = environ.copy()
try:
return self.nextapp(environ, start_response)
except _cherrypy.InternalRedirect:
ir = _sys.exc_info()[1]
sn = environ.get('SCRIPT_NAME', '')
path = environ.get('PATH_INFO', '')
qs = environ.get('QUERY_STRING', '')
# Add the *previous* path_info + qs to redirections.
old_uri = sn + path
if qs:
old_uri += '?' + qs
redirections.append(old_uri)
if not self.recursive:
# Check to see if the new URI has been redirected to
# already
new_uri = sn + ir.path
if ir.query_string:
new_uri += '?' + ir.query_string
if new_uri in redirections:
ir.request.close()
tmpl = (
'InternalRedirector visited the same URL twice: %r'
)
raise RuntimeError(tmpl % new_uri)
# Munge the environment and try again.
environ['REQUEST_METHOD'] = 'GET'
environ['PATH_INFO'] = ir.path
environ['QUERY_STRING'] = ir.query_string
environ['wsgi.input'] = io.BytesIO()
environ['CONTENT_LENGTH'] = '0'
environ['cherrypy.previous_request'] = ir.request
class ExceptionTrapper(object):
"""WSGI middleware that traps exceptions."""
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
self.nextapp = nextapp
self.throws = throws
def __call__(self, environ, start_response):
return _TrappedResponse(
self.nextapp,
environ,
start_response,
self.throws
)
class _TrappedResponse(object):
response = iter([])
def __init__(self, nextapp, environ, start_response, throws):
self.nextapp = nextapp
self.environ = environ
self.start_response = start_response
self.throws = throws
self.started_response = False
self.response = self.trap(
self.nextapp, self.environ, self.start_response,
)
self.iter_response = iter(self.response)
def __iter__(self):
self.started_response = True
return self
def __next__(self):
return self.trap(next, self.iter_response)
# todo: https://pythonhosted.org/six/#six.Iterator
if six.PY2:
next = __next__
def close(self):
if hasattr(self.response, 'close'):
self.response.close()
def trap(self, func, *args, **kwargs):
try:
return func(*args, **kwargs)
except self.throws:
raise
except StopIteration:
raise
except:
tb = _cperror.format_exc()
_cherrypy.log(tb, severity=40)
if not _cherrypy.request.show_tracebacks:
tb = ''
s, h, b = _cperror.bare_error(tb)
if six.PY3:
# What fun.
s = s.decode('ISO-8859-1')
h = [
(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in h
]
if self.started_response:
# Empty our iterable (so future calls raise StopIteration)
self.iter_response = iter([])
else:
self.iter_response = iter(b)
try:
self.start_response(s, h, _sys.exc_info())
except:
# "The application must not trap any exceptions raised by
# start_response, if it called start_response with exc_info.
# Instead, it should allow such exceptions to propagate
# back to the server or gateway."
# But we still log and call close() to clean up ourselves.
_cherrypy.log(traceback=True, severity=40)
raise
if self.started_response:
return ntob('').join(b)
else:
return b
# WSGI-to-CP Adapter #
class AppResponse(object):
throws = (KeyboardInterrupt, SystemExit)
request = None
def __init__(self, environ, start_response, cpapp, recursive=False):
self.redirections = []
self.recursive = recursive
self.environ = environ
self.start_response = start_response
"""WSGI response iterable for CherryPy applications."""
def __init__(self, environ, start_response, cpapp):
self.cpapp = cpapp
self.setapp()
def setapp(self):
try:
self.request = self.get_request()
s, h, b = self.get_response()
self.iter_response = iter(b)
self.write = self.start_response(s, h)
except self.throws:
if six.PY2:
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
environ = downgrade_wsgi_ux_to_1x(environ)
self.environ = environ
self.run()
r = _cherrypy.serving.response
outstatus = r.output_status
if not isinstance(outstatus, bytes):
raise TypeError('response.output_status is not a byte string.')
outheaders = []
for k, v in r.header_list:
if not isinstance(k, bytes):
tmpl = 'response.header_list key %r is not a byte string.'
raise TypeError(tmpl % k)
if not isinstance(v, bytes):
tmpl = (
'response.header_list value %r is not a byte string.'
)
raise TypeError(tmpl % v)
outheaders.append((k, v))
if six.PY3:
# According to PEP 3333, when using Python 3, the response
# status and headers must be bytes masquerading as unicode;
# that is, they must be of type "str" but are restricted to
# code points in the "latin-1" set.
outstatus = outstatus.decode('ISO-8859-1')
outheaders = [
(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in outheaders
]
self.iter_response = iter(r.body)
self.write = start_response(outstatus, outheaders)
except:
self.close()
raise
except _cherrypy.InternalRedirect, ir:
self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
self.close()
self.iredirect(ir.path, ir.query_string)
return
except:
if getattr(self.request, "throw_errors", False):
self.close()
raise
tb = _cperror.format_exc()
_cherrypy.log(tb, severity=40)
if not getattr(self.request, "show_tracebacks", True):
tb = ""
s, h, b = _cperror.bare_error(tb)
self.iter_response = iter(b)
try:
self.start_response(s, h, _sys.exc_info())
except:
# "The application must not trap any exceptions raised by
# start_response, if it called start_response with exc_info.
# Instead, it should allow such exceptions to propagate
# back to the server or gateway."
# But we still log and call close() to clean up ourselves.
_cherrypy.log(traceback=True, severity=40)
self.close()
raise
def iredirect(self, path, query_string):
"""Doctor self.environ and perform an internal redirect.
When cherrypy.InternalRedirect is raised, this method is called.
It rewrites the WSGI environ using the new path and query_string,
and calls a new CherryPy Request object. Because the wsgi.input
stream may have already been consumed by the next application,
the redirected call will always be of HTTP method "GET"; therefore,
any params must be passed in the query_string argument, which is
formed from InternalRedirect.query_string when using that exception.
If you need something more complicated, make and raise your own
exception and write your own AppResponse subclass to trap it. ;)
It would be a bad idea to redirect after you've already yielded
response content, although an enterprising soul could choose
to abuse this.
"""
env = self.environ
if not self.recursive:
sn = env.get('SCRIPT_NAME', '')
qs = query_string
if qs:
qs = "?" + qs
if sn + path + qs in self.redirections:
raise RuntimeError("InternalRedirector visited the "
"same URL twice: %r + %r + %r" %
(sn, path, qs))
else:
# Add the *previous* path_info + qs to redirections.
p = env.get('PATH_INFO', '')
qs = env.get('QUERY_STRING', '')
if qs:
qs = "?" + qs
self.redirections.append(sn + p + qs)
# Munge environment and try again.
env['REQUEST_METHOD'] = "GET"
env['PATH_INFO'] = path
env['QUERY_STRING'] = query_string
env['wsgi.input'] = _StringIO.StringIO()
env['CONTENT_LENGTH'] = "0"
self.setapp()
def __iter__(self):
return self
def next(self):
try:
chunk = self.iter_response.next()
# WSGI requires all data to be of type "str". This coercion should
# not take any time at all if chunk is already of type "str".
# If it's unicode, it could be a big performance hit (x ~500).
if not isinstance(chunk, str):
chunk = chunk.encode("ISO-8859-1")
return chunk
except self.throws:
self.close()
raise
except _cherrypy.InternalRedirect, ir:
self.environ['cherrypy.previous_request'] = _cherrypy.serving.request
self.close()
self.iredirect(ir.path, ir.query_string)
except StopIteration:
raise
except:
if getattr(self.request, "throw_errors", False):
self.close()
raise
tb = _cperror.format_exc()
_cherrypy.log(tb, severity=40)
if not getattr(self.request, "show_tracebacks", True):
tb = ""
s, h, b = _cperror.bare_error(tb)
# Empty our iterable (so future calls raise StopIteration)
self.iter_response = iter([])
try:
self.start_response(s, h, _sys.exc_info())
except:
# "The application must not trap any exceptions raised by
# start_response, if it called start_response with exc_info.
# Instead, it should allow such exceptions to propagate
# back to the server or gateway."
# But we still log and call close() to clean up ourselves.
_cherrypy.log(traceback=True, severity=40)
self.close()
raise
return "".join(b)
def __next__(self):
return next(self.iter_response)
# todo: https://pythonhosted.org/six/#six.Iterator
if six.PY2:
next = __next__
def close(self):
"""Close and de-reference the current request and response. (Core)"""
streaming = _cherrypy.serving.response.stream
self.cpapp.release_serving()
def get_response(self):
"""Run self.request and return its response."""
meth = self.environ['REQUEST_METHOD']
path = _http.urljoin(self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', ''))
qs = self.environ.get('QUERY_STRING', '')
rproto = self.environ.get('SERVER_PROTOCOL')
headers = self.translate_headers(self.environ)
rfile = self.environ['wsgi.input']
response = self.request.run(meth, path, qs, rproto, headers, rfile)
return response.status, response.header_list, response.body
def get_request(self):
# We avoid the expense of examining the iterator to see if it's
# closable unless we are streaming the response, as that's the
# only situation where we are going to have an iterator which
# may not have been exhausted yet.
if streaming and is_closable_iterator(self.iter_response):
iter_close = self.iter_response.close
try:
iter_close()
except Exception:
_cherrypy.log(traceback=True, severity=40)
def run(self):
"""Create a Request object using environ."""
env = self.environ.get
local = _http.Host('', int(env('SERVER_PORT', 80)),
env('SERVER_NAME', ''))
remote = _http.Host(env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1)),
env('REMOTE_HOST', ''))
local = httputil.Host(
'',
int(env('SERVER_PORT', 80) or -1),
env('SERVER_NAME', ''),
)
remote = httputil.Host(
env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1) or -1),
env('REMOTE_HOST', ''),
)
scheme = env('wsgi.url_scheme')
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1')
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
# LOGON_USER is served by IIS, and is the name of the
# user after having been mapped to a local account.
# Both IIS and Apache set REMOTE_USER, when possible.
@@ -240,66 +330,113 @@ class AppResponse(object):
request.multiprocess = self.environ['wsgi.multiprocess']
request.wsgi_environ = self.environ
request.prev = env('cherrypy.previous_request', None)
return request
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
'CONTENT_LENGTH': 'Content-Length',
'CONTENT_TYPE': 'Content-Type',
'REMOTE_HOST': 'Remote-Host',
'REMOTE_ADDR': 'Remote-Addr',
}
meth = self.environ['REQUEST_METHOD']
path = httputil.urljoin(
self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', ''),
)
qs = self.environ.get('QUERY_STRING', '')
path, qs = self.recode_path_qs(path, qs) or (path, qs)
rproto = self.environ.get('SERVER_PROTOCOL')
headers = self.translate_headers(self.environ)
rfile = self.environ['wsgi.input']
request.run(meth, path, qs, rproto, headers, rfile)
headerNames = {
'HTTP_CGI_AUTHORIZATION': 'Authorization',
'CONTENT_LENGTH': 'Content-Length',
'CONTENT_TYPE': 'Content-Type',
'REMOTE_HOST': 'Remote-Host',
'REMOTE_ADDR': 'Remote-Addr',
}
def recode_path_qs(self, path, qs):
if not six.PY3:
return
# This isn't perfect; if the given PATH_INFO is in the
# wrong encoding, it may fail to match the appropriate config
# section URI. But meh.
old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
new_enc = self.cpapp.find_config(
self.environ.get('PATH_INFO', ''),
'request.uri_encoding', 'utf-8',
)
if new_enc.lower() == old_enc.lower():
return
# Even though the path and qs are unicode, the WSGI server
# is required by PEP 3333 to coerce them to ISO-8859-1
# masquerading as unicode. So we have to encode back to
# bytes and then decode again using the "correct" encoding.
try:
return (
path.encode(old_enc).decode(new_enc),
qs.encode(old_enc).decode(new_enc),
)
except (UnicodeEncodeError, UnicodeDecodeError):
# Just pass them through without transcoding and hope.
pass
def translate_headers(self, environ):
"""Translate CGI-environ header names to HTTP header names."""
for cgiName in environ:
# We assume all incoming header keys are uppercase already.
if cgiName in self.headerNames:
yield self.headerNames[cgiName], environ[cgiName]
elif cgiName[:5] == "HTTP_":
elif cgiName[:5] == 'HTTP_':
# Hackish attempt at recovering original header names.
translatedHeader = cgiName[5:].replace("_", "-")
translatedHeader = cgiName[5:].replace('_', '-')
yield translatedHeader, environ[cgiName]
class CPWSGIApp(object):
"""A WSGI application object for a CherryPy Application.
pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
constructor that takes an initial, positional 'nextapp' argument,
plus optional keyword arguments, and returns a WSGI application
(that takes environ and start_response arguments). The 'name' can
be any you choose, and will correspond to keys in self.config.
head: rather than nest all apps in the pipeline on each call, it's only
done the first time, and the result is memoized into self.head. Set
this to None again if you change self.pipeline after calling self.
config: a dict whose keys match names listed in the pipeline. Each
value is a further dict which will be passed to the corresponding
named WSGI callable (from the pipeline) as keyword arguments.
"""
pipeline = []
"""A WSGI application object for a CherryPy Application."""
pipeline = [
('ExceptionTrapper', ExceptionTrapper),
('InternalRedirector', InternalRedirector),
]
"""A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
constructor that takes an initial, positional 'nextapp' argument,
plus optional keyword arguments, and returns a WSGI application
(that takes environ and start_response arguments). The 'name' can
be any you choose, and will correspond to keys in self.config."""
head = None
"""Rather than nest all apps in the pipeline on each call, it's only
done the first time, and the result is memoized into self.head. Set
this to None again if you change self.pipeline after calling self."""
config = {}
"""A dict whose keys match names listed in the pipeline. Each
value is a further dict which will be passed to the corresponding
named WSGI callable (from the pipeline) as keyword arguments."""
response_class = AppResponse
"""The class to instantiate and return as the next app in the WSGI chain.
"""
def __init__(self, cpapp, pipeline=None):
self.cpapp = cpapp
self.pipeline = self.pipeline[:]
if pipeline:
self.pipeline.extend(pipeline)
self.config = self.config.copy()
def tail(self, environ, start_response):
"""WSGI application callable for the actual CherryPy application.
You probably shouldn't call this; call self.__call__ instead,
so that any WSGI middleware in self.pipeline can run first.
"""
return self.response_class(environ, start_response, self.cpapp)
def __call__(self, environ, start_response):
head = self.head
if head is None:
@@ -311,20 +448,19 @@ class CPWSGIApp(object):
head = callable(head, **conf)
self.head = head
return head(environ, start_response)
def namespace_handler(self, k, v):
"""Config handler for the 'wsgi' namespace."""
if k == "pipeline":
if k == 'pipeline':
# Note this allows multiple 'wsgi.pipeline' config entries
# (but each entry will be processed in a 'random' order).
# It should also allow developers to set default middleware
# in code (passed to self.__init__) that deployers can add to
# (but not remove) via config.
self.pipeline.extend(v)
elif k == "response_class":
elif k == 'response_class':
self.response_class = v
else:
name, arg = k.split(".", 1)
name, arg = k.split('.', 1)
bucket = self.config.setdefault(name, {})
bucket[arg] = v

View File

@@ -1,68 +1,70 @@
"""WSGI server interface (see PEP 333). This adds some CP-specific bits to
the framework-agnostic wsgiserver package.
"""
import sys
import cherrypy
from cherrypy import wsgiserver
class CPHTTPRequest(wsgiserver.HTTPRequest):
def __init__(self, sendall, environ, wsgi_app):
s = cherrypy.server
self.max_request_header_size = s.max_request_header_size or 0
self.max_request_body_size = s.max_request_body_size or 0
wsgiserver.HTTPRequest.__init__(self, sendall, environ, wsgi_app)
class CPHTTPConnection(wsgiserver.HTTPConnection):
RequestHandlerClass = CPHTTPRequest
class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
"""Wrapper for wsgiserver.CherryPyWSGIServer.
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 set our own mount points from cherrypy.tree
and apply some attributes from config -> cherrypy.server -> wsgiserver.
"""
ConnectionClass = CPHTTPConnection
def __init__(self, server_adapter=cherrypy.server):
self.server_adapter = server_adapter
# We have to make custom subclasses of wsgiserver internals here
# so that our server.* attributes get applied to every request.
class _CPHTTPRequest(wsgiserver.HTTPRequest):
def __init__(self, sendall, environ, wsgi_app):
s = server_adapter
self.max_request_header_size = s.max_request_header_size or 0
self.max_request_body_size = s.max_request_body_size or 0
wsgiserver.HTTPRequest.__init__(self, sendall, environ, wsgi_app)
class _CPHTTPConnection(wsgiserver.HTTPConnection):
RequestHandlerClass = _CPHTTPRequest
self.ConnectionClass = _CPHTTPConnection
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
)
server_name = (self.server_adapter.socket_host or
self.server_adapter.socket_file or
None)
self.wsgi_version = self.server_adapter.wsgi_version
s = wsgiserver.CherryPyWSGIServer
s.__init__(self, server_adapter.bind_addr, cherrypy.tree,
self.server_adapter.thread_pool,
server_name,
max = self.server_adapter.thread_pool_max,
request_queue_size = self.server_adapter.socket_queue_size,
timeout = self.server_adapter.socket_timeout,
shutdown_timeout = self.server_adapter.shutdown_timeout,
max=self.server_adapter.thread_pool_max,
request_queue_size=self.server_adapter.socket_queue_size,
timeout=self.server_adapter.socket_timeout,
shutdown_timeout=self.server_adapter.shutdown_timeout,
accepted_queue_size=self.server_adapter.accepted_queue_size,
accepted_queue_timeout=self.server_adapter.accepted_queue_timeout,
)
self.protocol = self.server_adapter.protocol_version
self.nodelay = self.server_adapter.nodelay
self.ssl_context = self.server_adapter.ssl_context
self.ssl_certificate = self.server_adapter.ssl_certificate
self.ssl_certificate_chain = self.server_adapter.ssl_certificate_chain
self.ssl_private_key = self.server_adapter.ssl_private_key
if sys.version_info >= (3, 0):
ssl_module = self.server_adapter.ssl_module or 'builtin'
else:
ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
if self.server_adapter.ssl_context:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
self.ssl_adapter = adapter_class(
self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain)
self.ssl_adapter.context = self.server_adapter.ssl_context
elif self.server_adapter.ssl_certificate:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
self.ssl_adapter = adapter_class(
self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain)
self.stats['Enabled'] = getattr(
self.server_adapter, 'statistics', False)
def error_log(self, msg='', level=20, traceback=False):
cherrypy.engine.log(msg, level, traceback)

298
cherrypy/_helper.py Normal file
View File

@@ -0,0 +1,298 @@
"""
Helper functions for CP apps
"""
import six
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
from cherrypy._cpcompat import text_or_bytes
import cherrypy
def expose(func=None, alias=None):
"""
Expose the function or class, optionally providing an alias or set of aliases.
"""
def expose_(func):
func.exposed = True
if alias is not None:
if isinstance(alias, text_or_bytes):
parents[alias.replace('.', '_')] = func
else:
for a in alias:
parents[a.replace('.', '_')] = func
return func
import sys
import types
decoratable_types = types.FunctionType, types.MethodType, type,
if six.PY2:
# Old-style classes are type types.ClassType.
decoratable_types += types.ClassType,
if isinstance(func, decoratable_types):
if alias is None:
# @expose
func.exposed = True
return func
else:
# func = expose(func, alias)
parents = sys._getframe(1).f_locals
return expose_(func)
elif func is None:
if alias is None:
# @expose()
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose(alias="alias") or
# @expose(alias=["alias1", "alias2"])
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose("alias") or
# @expose(["alias1", "alias2"])
parents = sys._getframe(1).f_locals
alias = func
return expose_
def popargs(*args, **kwargs):
"""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
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
_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
request. If handler is not specified or is None, then self is used.
If handler is a function rather than an instance, then that function
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:
def index(self, year=None, month=None, day=None):
#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.
handler = None
handler_call = False
for k, v in kwargs.items():
if k == 'handler':
handler = v
else:
raise TypeError(
"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)):
handler_call = True
def decorated(cls_or_self=None, vpath=None):
if inspect.isclass(cls_or_self):
# cherrypy.popargs is a class decorator
cls = cls_or_self
setattr(cls, cherrypy.dispatch.Dispatcher.dispatch_method_name, decorated)
return cls
# 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:
cherrypy.request.params.update(parms)
return handler
cherrypy.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 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).
If True, the output will instead be a URL that is relative to the
current request path, perhaps including '..' atoms. If relative is
the string 'server', the output will instead be a URL that is
relative to the server root; i.e., it will start with a slash.
"""
if isinstance(qs, (tuple, list, dict)):
qs = _urlencode(qs)
if qs:
qs = '?' + qs
if cherrypy.request.app:
if not path.startswith('/'):
# Append/remove trailing slash from path_info as needed
# (this is to support mistyped URL's without redirecting;
# if you want to redirect, use tools.trailing_slash).
pi = cherrypy.request.path_info
if cherrypy.request.is_index is True:
if not pi.endswith('/'):
pi = pi + '/'
elif cherrypy.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 = cherrypy.request.script_name
if base is None:
base = cherrypy.request.base
newurl = base + script_name + path + qs
else:
# No request.app (we're being called outside a request).
# We'll have to guess the base from server.* attributes.
# This will produce very different results from the above
# if you're using vhosts or tools.proxy.
if base is None:
base = cherrypy.server.base()
path = (script_name or '') + path
newurl = base + path + qs
if './' in newurl:
# Normalize the URL by removing ./ and ../
atoms = []
for atom in newurl.split('/'):
if atom == '.':
pass
elif atom == '..':
atoms.pop()
else:
atoms.append(atom)
newurl = '/'.join(atoms)
# At this point, we should have a fully-qualified absolute URL.
if relative is None:
relative = getattr(cherrypy.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
# termed an absolute-path reference, as defined by <abs_path>..."
# This is also sometimes called "server-relative".
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
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(relative=False).split('/')[:-1]
new = newurl.split('/')
while old and new:
a, b = old[0], new[0]
if a != b:
break
old.pop(0)
new.pop(0)
new = (['..'] * len(old)) + new
newurl = '/'.join(new)
return newurl

90
cherrypy/cherryd Normal file → Executable file
View File

@@ -1,92 +1,6 @@
#! /usr/bin/env python
"""The CherryPy daemon."""
import sys
import cherrypy
from cherrypy.process import plugins, servers
def start(configfiles=None, daemonize=False, environment=None,
fastcgi=False, scgi=False, pidfile=None, imports=None):
"""Subscribe all engine plugins and start the engine."""
sys.path = [''] + sys.path
for i in imports or []:
exec "import %s" % i
for c in configfiles or []:
cherrypy.config.update(c)
engine = cherrypy.engine
if environment is not None:
cherrypy.config.update({'environment': environment})
# Only daemonize if asked to.
if daemonize:
# Don't print anything to stdout/sterr.
cherrypy.config.update({'log.screen': False})
plugins.Daemonizer(engine).subscribe()
if pidfile:
plugins.PIDFile(engine, pidfile).subscribe()
if hasattr(engine, "signal_handler"):
engine.signal_handler.subscribe()
if hasattr(engine, "console_control_handler"):
engine.console_control_handler.subscribe()
if fastcgi and scgi:
# fastcgi and scgi aren't allowed together.
cherrypy.log.error("fastcgi and scgi aren't allowed together.", 'ENGINE')
sys.exit(1)
elif fastcgi or scgi:
# Turn off autoreload when using fastcgi or scgi.
cherrypy.config.update({'engine.autoreload_on': False})
# Turn off the default HTTP server (which is subscribed by default).
cherrypy.server.unsubscribe()
addr = cherrypy.server.bind_addr
if fastcgi:
f = servers.FlupFCGIServer(application=cherrypy.tree,
bindAddress=addr)
else:
f = servers.FlupSCGIServer(application=cherrypy.tree,
bindAddress=addr)
s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
s.subscribe()
# Always start the engine; this will start all other services
try:
engine.start()
except:
# Assume the error has been logged already via bus.log.
sys.exit(1)
else:
engine.block()
import cherrypy.daemon
if __name__ == '__main__':
from optparse import OptionParser
p = OptionParser()
p.add_option('-c', '--config', action="append", dest='config',
help="specify config file(s)")
p.add_option('-d', action="store_true", dest='daemonize',
help="run the server as a daemon")
p.add_option('-e', '--environment', dest='environment', default=None,
help="apply the given config environment")
p.add_option('-f', action="store_true", dest='fastcgi',
help="start a fastcgi server instead of the default HTTP server")
p.add_option('-s', action="store_true", dest='scgi',
help="start a scgi server instead of the default HTTP server")
p.add_option('-i', '--import', action="append", dest='imports',
help="specify modules to import")
p.add_option('-p', '--pidfile', dest='pidfile', default=None,
help="store the process id in the given file")
options, args = p.parse_args()
start(options.config, options.daemonize,
options.environment, options.fastcgi, options.scgi, options.pidfile,
options.imports)
cherrypy.daemon.run()

106
cherrypy/daemon.py Executable file
View File

@@ -0,0 +1,106 @@
"""The CherryPy daemon."""
import sys
import cherrypy
from cherrypy.process import plugins, servers
from cherrypy import Application
def start(configfiles=None, daemonize=False, environment=None,
fastcgi=False, scgi=False, pidfile=None, imports=None,
cgi=False):
"""Subscribe all engine plugins and start the engine."""
sys.path = [''] + sys.path
for i in imports or []:
exec('import %s' % i)
for c in configfiles or []:
cherrypy.config.update(c)
# If there's only one app mounted, merge config into it.
if len(cherrypy.tree.apps) == 1:
for app in cherrypy.tree.apps.values():
if isinstance(app, Application):
app.merge(c)
engine = cherrypy.engine
if environment is not None:
cherrypy.config.update({'environment': environment})
# Only daemonize if asked to.
if daemonize:
# Don't print anything to stdout/sterr.
cherrypy.config.update({'log.screen': False})
plugins.Daemonizer(engine).subscribe()
if pidfile:
plugins.PIDFile(engine, pidfile).subscribe()
if hasattr(engine, 'signal_handler'):
engine.signal_handler.subscribe()
if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.subscribe()
if (fastcgi and (scgi or cgi)) or (scgi and cgi):
cherrypy.log.error('You may only specify one of the cgi, fastcgi, and '
'scgi options.', 'ENGINE')
sys.exit(1)
elif fastcgi or scgi or cgi:
# Turn off autoreload when using *cgi.
cherrypy.config.update({'engine.autoreload.on': False})
# Turn off the default HTTP server (which is subscribed by default).
cherrypy.server.unsubscribe()
addr = cherrypy.server.bind_addr
cls = (
servers.FlupFCGIServer if fastcgi else
servers.FlupSCGIServer if scgi else
servers.FlupCGIServer
)
f = cls(application=cherrypy.tree, bindAddress=addr)
s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
s.subscribe()
# Always start the engine; this will start all other services
try:
engine.start()
except:
# Assume the error has been logged already via bus.log.
sys.exit(1)
else:
engine.block()
def run():
from optparse import OptionParser
p = OptionParser()
p.add_option('-c', '--config', action='append', dest='config',
help='specify config file(s)')
p.add_option('-d', action='store_true', dest='daemonize',
help='run the server as a daemon')
p.add_option('-e', '--environment', dest='environment', default=None,
help='apply the given config environment')
p.add_option('-f', action='store_true', dest='fastcgi',
help='start a fastcgi server instead of the default HTTP '
'server')
p.add_option('-s', action='store_true', dest='scgi',
help='start a scgi server instead of the default HTTP server')
p.add_option('-x', action='store_true', dest='cgi',
help='start a cgi server instead of the default HTTP server')
p.add_option('-i', '--import', action='append', dest='imports',
help='specify modules to import')
p.add_option('-p', '--pidfile', dest='pidfile', default=None,
help='store the process id in the given file')
p.add_option('-P', '--Path', action='append', dest='Path',
help='add the given paths to sys.path')
options, args = p.parse_args()
if options.Path:
for p in options.Path:
sys.path.insert(0, p)
start(options.config, options.daemonize,
options.environment, options.fastcgi, options.scgi,
options.pidfile, options.imports, options.cgi)

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,156 +1,65 @@
"""CherryPy Library"""
import sys as _sys
def is_iterator(obj):
'''Returns a boolean indicating if the object provided implements
the iterator protocol (i.e. like a generator). This will return
false for objects which iterable, but not iterators themselves.'''
from types import GeneratorType
if isinstance(obj, GeneratorType):
return True
elif not hasattr(obj, '__iter__'):
return False
else:
# Types which implement the protocol must return themselves when
# invoking 'iter' upon them.
return iter(obj) is obj
def modules(modulePath):
"""Load a module and retrieve a reference to that module."""
def is_closable_iterator(obj):
# Not an iterator.
if not is_iterator(obj):
return False
# A generator - the easiest thing to deal with.
import inspect
if inspect.isgenerator(obj):
return True
# A custom iterator. Look for a close method...
if not (hasattr(obj, 'close') and callable(obj.close)):
return False
# ... which doesn't require any arguments.
try:
mod = _sys.modules[modulePath]
if mod is None:
raise KeyError()
except KeyError:
# The last [''] is important.
mod = __import__(modulePath, globals(), locals(), [''])
return mod
def attributes(full_attribute_name):
"""Load a module and retrieve an attribute of that module."""
# Parse out the path, module, and attribute
last_dot = full_attribute_name.rfind(u".")
attr_name = full_attribute_name[last_dot + 1:]
mod_path = full_attribute_name[:last_dot]
mod = modules(mod_path)
# Let an AttributeError propagate outward.
try:
attr = getattr(mod, attr_name)
except AttributeError:
raise AttributeError("'%s' object has no attribute '%s'"
% (mod_path, attr_name))
# Return a reference to the attribute.
return attr
# public domain "unrepr" implementation, found on the web and then improved.
class _Builder:
def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None:
raise TypeError("unrepr does not recognize %s" %
repr(o.__class__.__name__))
return m(o)
def build_Subscript(self, o):
expr, flags, subs = o.getChildren()
expr = self.build(expr)
subs = self.build(subs)
return expr[subs]
def build_CallFunc(self, o):
children = map(self.build, o.getChildren())
callee = children.pop(0)
kwargs = children.pop() or {}
starargs = children.pop() or ()
args = tuple(children) + tuple(starargs)
return callee(*args, **kwargs)
def build_List(self, o):
return map(self.build, o.getChildren())
def build_Const(self, o):
return o.value
def build_Dict(self, o):
d = {}
i = iter(map(self.build, o.getChildren()))
for el in i:
d[el] = i.next()
return d
def build_Tuple(self, o):
return tuple(self.build_List(o))
def build_Name(self, o):
if o.name == 'None':
return None
if o.name == 'True':
return True
if o.name == 'False':
return False
# See if the Name is a package or module. If it is, import it.
try:
return modules(o.name)
except ImportError:
pass
# See if the Name is in __builtin__.
try:
import __builtin__
return getattr(__builtin__, o.name)
except AttributeError:
pass
raise TypeError("unrepr could not resolve the name %s" % repr(o.name))
def build_Add(self, o):
left, right = map(self.build, o.getChildren())
return left + right
def build_Getattr(self, o):
parent = self.build(o.expr)
return getattr(parent, o.attrname)
def build_NoneType(self, o):
return None
def build_UnarySub(self, o):
return -self.build(o.getChildren()[0])
def build_UnaryAdd(self, o):
return self.build(o.getChildren()[0])
def unrepr(s):
"""Return a Python object compiled from a string."""
if not s:
return s
try:
import compiler
except ImportError:
# Fallback to eval when compiler package is not available,
# e.g. IronPython 1.0.
return eval(s)
p = compiler.parse("__tempvalue__ = " + s)
obj = p.getChildren()[1].getChildren()[0].getChildren()[1]
return _Builder().build(obj)
inspect.getcallargs(obj.close)
except TypeError:
return False
else:
return True
class file_generator(object):
"""Yield the given input (a file object) in chunks (default 64k). (Core)"""
def __init__(self, input, chunkSize=65536):
self.input = input
self.chunkSize = chunkSize
def __iter__(self):
return self
def next(self):
def __next__(self):
chunk = self.input.read(self.chunkSize)
if chunk:
return chunk
else:
self.input.close()
if hasattr(self.input, 'close'):
self.input.close()
raise StopIteration()
next = __next__
def file_generator_limited(fileobj, count, chunk_size=65536):
@@ -166,3 +75,11 @@ def file_generator_limited(fileobj, count, chunk_size=65536):
remaining -= chunklen
yield chunk
def set_vary_header(response, header_name):
'Add a Vary header to a response'
varies = response.headers.get('Vary', '')
varies = [x.strip() for x in varies.split(',') if x.strip()]
if header_name not in varies:
varies.append(header_name)
response.headers['Vary'] = ', '.join(varies)

View File

@@ -3,73 +3,95 @@ from cherrypy.lib import httpauth
def check_auth(users, encrypt=None, realm=None):
"""If an authorization header contains credentials, return True, else False."""
if 'authorization' in cherrypy.request.headers:
"""If an authorization header contains credentials, return True or False.
"""
request = cherrypy.serving.request
if 'authorization' in request.headers:
# make sure the provided credentials are correctly set
ah = httpauth.parseAuthorization(cherrypy.request.headers['authorization'])
ah = httpauth.parseAuthorization(request.headers['authorization'])
if ah is None:
raise cherrypy.HTTPError(400, 'Bad Request')
if not encrypt:
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
if callable(users):
if hasattr(users, '__call__'):
try:
# backward compatibility
users = users() # expect it to return a dictionary
users = users() # expect it to return a dictionary
if not isinstance(users, dict):
raise ValueError, "Authentication users must be a dictionary"
raise ValueError(
'Authentication users must be a dictionary')
# fetch the user password
password = users.get(ah["username"], None)
password = users.get(ah['username'], None)
except TypeError:
# returns a password (encrypted or clear text)
password = users(ah["username"])
password = users(ah['username'])
else:
if not isinstance(users, dict):
raise ValueError, "Authentication users must be a dictionary"
raise ValueError('Authentication users must be a dictionary')
# fetch the user password
password = users.get(ah["username"], None)
password = users.get(ah['username'], None)
# validate the authorization by re-computing it here
# and compare it with what the user-agent provided
if httpauth.checkResponse(ah, password, method=cherrypy.request.method,
if httpauth.checkResponse(ah, password, method=request.method,
encrypt=encrypt, realm=realm):
cherrypy.request.login = ah["username"]
request.login = ah['username']
return True
cherrypy.request.login = False
request.login = False
return False
def basic_auth(realm, users, encrypt=None):
def basic_auth(realm, users, encrypt=None, debug=False):
"""If auth fails, raise 401 with a basic authentication header.
realm: a string containing the authentication realm.
users: a dict of the form: {username: password} or a callable returning a dict.
encrypt: callable used to encrypt the password returned from the user-agent.
if None it defaults to a md5 encryption.
realm
A string containing the authentication realm.
users
A dict of the form: {username: password} or a callable returning
a dict.
encrypt
callable used to encrypt the password returned from the user-agent.
if None it defaults to a md5 encryption.
"""
if check_auth(users, encrypt):
if debug:
cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH')
return
# inform the user-agent this path is protected
cherrypy.response.headers['www-authenticate'] = httpauth.basicAuth(realm)
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
def digest_auth(realm, users):
# inform the user-agent this path is protected
cherrypy.serving.response.headers[
'www-authenticate'] = httpauth.basicAuth(realm)
raise cherrypy.HTTPError(
401, 'You are not authorized to access that resource')
def digest_auth(realm, users, debug=False):
"""If auth fails, raise 401 with a digest authentication header.
realm: a string containing the authentication realm.
users: a dict of the form: {username: password} or a callable returning a dict.
realm
A string containing the authentication realm.
users
A dict of the form: {username: password} or a callable returning
a dict.
"""
if check_auth(users, realm=realm):
if debug:
cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH')
return
# inform the user-agent this path is protected
cherrypy.response.headers['www-authenticate'] = httpauth.digestAuth(realm)
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
cherrypy.serving.response.headers[
'www-authenticate'] = httpauth.digestAuth(realm)
raise cherrypy.HTTPError(
401, 'You are not authorized to access that resource')

View File

@@ -0,0 +1,90 @@
# This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
import binascii
import cherrypy
from cherrypy._cpcompat import base64_decode
__doc__ = """This module provides a CherryPy 3.x tool which implements
the server-side of HTTP Basic Access Authentication, as described in
:rfc:`2617`.
Example usage, using the built-in checkpassword_dict function which uses a dict
as the credentials store::
userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'}
checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict)
basic_auth = {'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'earth',
'tools.auth_basic.checkpassword': checkpassword,
}
app_config = { '/' : basic_auth }
"""
__author__ = 'visteya'
__date__ = 'April 2009'
def checkpassword_dict(user_password_dict):
"""Returns a checkpassword function which checks credentials
against a dictionary of the form: {username : password}.
If you want a simple dictionary-based authentication scheme, use
checkpassword_dict(my_credentials_dict) as the value for the
checkpassword argument to basic_auth().
"""
def checkpassword(realm, user, password):
p = user_password_dict.get(user)
return p and p == password or False
return checkpassword
def basic_auth(realm, checkpassword, debug=False):
"""A CherryPy tool which hooks at before_handler to perform
HTTP Basic Access Authentication, as specified in :rfc:`2617`.
If the request has an 'authorization' header with a 'Basic' scheme, this
tool attempts to authenticate the credentials supplied in that header. If
the request has no 'authorization' header, or if it does but the scheme is
not 'Basic', or if authentication fails, the tool sends a 401 response with
a 'WWW-Authenticate' Basic header.
realm
A string containing the authentication realm.
checkpassword
A callable which checks the authentication credentials.
Its signature is checkpassword(realm, username, password). where
username and password are the values obtained from the request's
'authorization' header. If authentication succeeds, checkpassword
returns True, else it returns False.
"""
if '"' in realm:
raise ValueError('Realm cannot contain the " (quote) character.')
request = cherrypy.serving.request
auth_header = request.headers.get('authorization')
if auth_header is not None:
# split() error, base64.decodestring() error
with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, 'Bad Request'):
scheme, params = auth_header.split(' ', 1)
if scheme.lower() == 'basic':
username, password = base64_decode(params).split(':', 1)
if checkpassword(realm, username, password):
if debug:
cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
request.login = username
return # successful authentication
# Respond with 401 status and a WWW-Authenticate header
cherrypy.serving.response.headers[
'www-authenticate'] = 'Basic realm="%s"' % realm
raise cherrypy.HTTPError(
401, 'You are not authorized to access that resource')

390
cherrypy/lib/auth_digest.py Normal file
View File

@@ -0,0 +1,390 @@
# This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
import time
from hashlib import md5
import cherrypy
from cherrypy._cpcompat import ntob, parse_http_list, parse_keqv_list
__doc__ = """An implementation of the server-side of HTTP Digest Access
Authentication, which is described in :rfc:`2617`.
Example usage, using the built-in get_ha1_dict_plain function which uses a dict
of plaintext passwords as the credentials store::
userpassdict = {'alice' : '4x5istwelve'}
get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict)
digest_auth = {'tools.auth_digest.on': True,
'tools.auth_digest.realm': 'wonderland',
'tools.auth_digest.get_ha1': get_ha1,
'tools.auth_digest.key': 'a565c27146791cfb',
}
app_config = { '/' : digest_auth }
"""
__author__ = 'visteya'
__date__ = 'April 2009'
md5_hex = lambda s: md5(ntob(s)).hexdigest()
qop_auth = 'auth'
qop_auth_int = 'auth-int'
valid_qops = (qop_auth, qop_auth_int)
valid_algorithms = ('MD5', 'MD5-sess')
def TRACE(msg):
cherrypy.log(msg, context='TOOLS.AUTH_DIGEST')
# Three helper functions for users of the tool, providing three variants
# of get_ha1() functions for three different kinds of credential stores.
def get_ha1_dict_plain(user_password_dict):
"""Returns a get_ha1 function which obtains a plaintext password from a
dictionary of the form: {username : password}.
If you want a simple dictionary-based authentication scheme, with plaintext
passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the
get_ha1 argument to digest_auth().
"""
def get_ha1(realm, username):
password = user_password_dict.get(username)
if password:
return md5_hex('%s:%s:%s' % (username, realm, password))
return None
return get_ha1
def get_ha1_dict(user_ha1_dict):
"""Returns a get_ha1 function which obtains a HA1 password hash from a
dictionary of the form: {username : HA1}.
If you want a dictionary-based authentication scheme, but with
pre-computed HA1 hashes instead of plain-text passwords, use
get_ha1_dict(my_userha1_dict) as the value for the get_ha1
argument to digest_auth().
"""
def get_ha1(realm, username):
return user_ha1_dict.get(username)
return get_ha1
def get_ha1_file_htdigest(filename):
"""Returns a get_ha1 function which obtains a HA1 password hash from a
flat file with lines of the same format as that produced by the Apache
htdigest utility. For example, for realm 'wonderland', username 'alice',
and password '4x5istwelve', the htdigest line would be::
alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c
If you want to use an Apache htdigest file as the credentials store,
then use get_ha1_file_htdigest(my_htdigest_file) as the value for the
get_ha1 argument to digest_auth(). It is recommended that the filename
argument be an absolute path, to avoid problems.
"""
def get_ha1(realm, username):
result = None
f = open(filename, 'r')
for line in f:
u, r, ha1 = line.rstrip().split(':')
if u == username and r == realm:
result = ha1
break
f.close()
return result
return get_ha1
def synthesize_nonce(s, key, timestamp=None):
"""Synthesize a nonce value which resists spoofing and can be checked
for staleness. Returns a string suitable as the value for 'nonce' in
the www-authenticate header.
s
A string related to the resource, such as the hostname of the server.
key
A secret string known only to the server.
timestamp
An integer seconds-since-the-epoch timestamp
"""
if timestamp is None:
timestamp = int(time.time())
h = md5_hex('%s:%s:%s' % (timestamp, s, key))
nonce = '%s:%s' % (timestamp, h)
return nonce
def H(s):
"""The hash function H"""
return md5_hex(s)
class HttpDigestAuthorization (object):
"""Class to parse a Digest Authorization header and perform re-calculation
of the digest.
"""
def errmsg(self, s):
return 'Digest Authorization header: %s' % s
def __init__(self, auth_header, http_method, debug=False):
self.http_method = http_method
self.debug = debug
scheme, params = auth_header.split(' ', 1)
self.scheme = scheme.lower()
if self.scheme != 'digest':
raise ValueError('Authorization scheme is not "Digest"')
self.auth_header = auth_header
# make a dict of the params
items = parse_http_list(params)
paramsd = parse_keqv_list(items)
self.realm = paramsd.get('realm')
self.username = paramsd.get('username')
self.nonce = paramsd.get('nonce')
self.uri = paramsd.get('uri')
self.method = paramsd.get('method')
self.response = paramsd.get('response') # the response digest
self.algorithm = paramsd.get('algorithm', 'MD5').upper()
self.cnonce = paramsd.get('cnonce')
self.opaque = paramsd.get('opaque')
self.qop = paramsd.get('qop') # qop
self.nc = paramsd.get('nc') # nonce count
# perform some correctness checks
if self.algorithm not in valid_algorithms:
raise ValueError(
self.errmsg("Unsupported value for algorithm: '%s'" %
self.algorithm))
has_reqd = (
self.username and
self.realm and
self.nonce and
self.uri and
self.response
)
if not has_reqd:
raise ValueError(
self.errmsg('Not all required parameters are present.'))
if self.qop:
if self.qop not in valid_qops:
raise ValueError(
self.errmsg("Unsupported value for qop: '%s'" % self.qop))
if not (self.cnonce and self.nc):
raise ValueError(
self.errmsg('If qop is sent then '
'cnonce and nc MUST be present'))
else:
if self.cnonce or self.nc:
raise ValueError(
self.errmsg('If qop is not sent, '
'neither cnonce nor nc can be present'))
def __str__(self):
return 'authorization : %s' % self.auth_header
def validate_nonce(self, s, key):
"""Validate the nonce.
Returns True if nonce was generated by synthesize_nonce() and the
timestamp is not spoofed, else returns False.
s
A string related to the resource, such as the hostname of
the server.
key
A secret string known only to the server.
Both s and key must be the same values which were used to synthesize
the nonce we are trying to validate.
"""
try:
timestamp, hashpart = self.nonce.split(':', 1)
s_timestamp, s_hashpart = synthesize_nonce(
s, key, timestamp).split(':', 1)
is_valid = s_hashpart == hashpart
if self.debug:
TRACE('validate_nonce: %s' % is_valid)
return is_valid
except ValueError: # split() error
pass
return False
def is_nonce_stale(self, max_age_seconds=600):
"""Returns True if a validated nonce is stale. The nonce contains a
timestamp in plaintext and also a secure hash of the timestamp.
You should first validate the nonce to ensure the plaintext
timestamp is not spoofed.
"""
try:
timestamp, hashpart = self.nonce.split(':', 1)
if int(timestamp) + max_age_seconds > int(time.time()):
return False
except ValueError: # int() error
pass
if self.debug:
TRACE('nonce is stale')
return True
def HA2(self, entity_body=''):
"""Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3."""
# RFC 2617 3.2.2.3
# If the "qop" directive's value is "auth" or is unspecified,
# then A2 is:
# A2 = method ":" digest-uri-value
#
# If the "qop" value is "auth-int", then A2 is:
# A2 = method ":" digest-uri-value ":" H(entity-body)
if self.qop is None or self.qop == 'auth':
a2 = '%s:%s' % (self.http_method, self.uri)
elif self.qop == 'auth-int':
a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body))
else:
# in theory, this should never happen, since I validate qop in
# __init__()
raise ValueError(self.errmsg('Unrecognized value for qop!'))
return H(a2)
def request_digest(self, ha1, entity_body=''):
"""Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
ha1
The HA1 string obtained from the credentials store.
entity_body
If 'qop' is set to 'auth-int', then A2 includes a hash
of the "entity body". The entity body is the part of the
message which follows the HTTP headers. See :rfc:`2617` section
4.3. This refers to the entity the user agent sent in the
request which has the Authorization header. Typically GET
requests don't have an entity, and POST requests do.
"""
ha2 = self.HA2(entity_body)
# Request-Digest -- RFC 2617 3.2.2.1
if self.qop:
req = '%s:%s:%s:%s:%s' % (
self.nonce, self.nc, self.cnonce, self.qop, ha2)
else:
req = '%s:%s' % (self.nonce, ha2)
# RFC 2617 3.2.2.2
#
# If the "algorithm" directive's value is "MD5" or is unspecified,
# then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
#
# If the "algorithm" directive's value is "MD5-sess", then A1 is
# calculated only once - on the first request by the client following
# receipt of a WWW-Authenticate challenge from the server.
# A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
# ":" unq(nonce-value) ":" unq(cnonce-value)
if self.algorithm == 'MD5-sess':
ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce))
digest = H('%s:%s' % (ha1, req))
return digest
def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
stale=False):
"""Constructs a WWW-Authenticate header for Digest authentication."""
if qop not in valid_qops:
raise ValueError("Unsupported value for qop: '%s'" % qop)
if algorithm not in valid_algorithms:
raise ValueError("Unsupported value for algorithm: '%s'" % algorithm)
if nonce is None:
nonce = synthesize_nonce(realm, key)
s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
realm, nonce, algorithm, qop)
if stale:
s += ', stale="true"'
return s
def digest_auth(realm, get_ha1, key, debug=False):
"""A CherryPy tool which hooks at before_handler to perform
HTTP Digest Access Authentication, as specified in :rfc:`2617`.
If the request has an 'authorization' header with a 'Digest' scheme,
this tool authenticates the credentials supplied in that header.
If the request has no 'authorization' header, or if it does but the
scheme is not "Digest", or if authentication fails, the tool sends
a 401 response with a 'WWW-Authenticate' Digest header.
realm
A string containing the authentication realm.
get_ha1
A callable which looks up a username in a credentials store
and returns the HA1 string, which is defined in the RFC to be
MD5(username : realm : password). The function's signature is:
``get_ha1(realm, username)``
where username is obtained from the request's 'authorization' header.
If username is not found in the credentials store, get_ha1() returns
None.
key
A secret string known only to the server, used in the synthesis
of nonces.
"""
request = cherrypy.serving.request
auth_header = request.headers.get('authorization')
nonce_is_stale = False
if auth_header is not None:
with cherrypy.HTTPError.handle(ValueError, 400,
'The Authorization header could not be parsed.'):
auth = HttpDigestAuthorization(
auth_header, request.method, debug=debug)
if debug:
TRACE(str(auth))
if auth.validate_nonce(realm, key):
ha1 = get_ha1(realm, auth.username)
if ha1 is not None:
# note that for request.body to be available we need to
# hook in at before_handler, not on_start_resource like
# 3.1.x digest_auth does.
digest = auth.request_digest(ha1, entity_body=request.body)
if digest == auth.response: # authenticated
if debug:
TRACE('digest matches auth.response')
# Now check if nonce is stale.
# The choice of ten minutes' lifetime for nonce is somewhat
# arbitrary
nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600)
if not nonce_is_stale:
request.login = auth.username
if debug:
TRACE('authentication of %s successful' %
auth.username)
return
# Respond with 401 status and a WWW-Authenticate header
header = www_authenticate(realm, key, stale=nonce_is_stale)
if debug:
TRACE(header)
cherrypy.serving.response.headers['WWW-Authenticate'] = header
raise cherrypy.HTTPError(
401, 'You are not authorized to access that resource')

View File

@@ -1,32 +1,181 @@
"""
CherryPy implements a simple caching system as a pluggable Tool. This tool
tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there
yet, but it's probably good enough for most sites.
In general, GET responses are cached (along with selecting headers) and, if
another request arrives for the same resource, the caching Tool will return 304
Not Modified if possible, or serve the cached response otherwise. It also sets
request.cached to True if serving a cached representation, and sets
request.cacheable to False (so it doesn't get cached again).
If POST, PUT, or DELETE requests are made for a cached resource, they
invalidate (delete) any cached response.
Usage
=====
Configuration file example::
[/]
tools.caching.on = True
tools.caching.delay = 3600
You may use a class other than the default
:class:`MemoryCache<cherrypy.lib.caching.MemoryCache>` by supplying the config
entry ``cache_class``; supply the full dotted name of the replacement class
as the config value. It must implement the basic methods ``get``, ``put``,
``delete``, and ``clear``.
You may set any attribute, including overriding methods, on the cache
instance by providing them in config. The above sets the
:attr:`delay<cherrypy.lib.caching.MemoryCache.delay>` attribute, for example.
"""
import datetime
import sys
import threading
import time
import cherrypy
from cherrypy.lib import cptools, http
from cherrypy.lib import cptools, httputil
from cherrypy._cpcompat import copyitems, ntob, sorted, Event
class MemoryCache:
maxobjects = 1000
maxobj_size = 100000
maxsize = 10000000
delay = 600
def __init__(self):
self.clear()
t = threading.Thread(target=self.expire_cache, name='expire_cache')
self.expiration_thread = t
if hasattr(threading.Thread, "daemon"):
# Python 2.6+
t.daemon = True
else:
t.setDaemon(True)
t.start()
class Cache(object):
"""Base class for Cache implementations."""
def get(self):
"""Return the current variant if in the cache, else None."""
raise NotImplemented
def put(self, obj, size):
"""Store the current variant in the cache."""
raise NotImplemented
def delete(self):
"""Remove ALL cached variants of the current resource."""
raise NotImplemented
def clear(self):
"""Reset the cache to its initial, empty state."""
self.cache = {}
raise NotImplemented
# ------------------------------ Memory Cache ------------------------------- #
class AntiStampedeCache(dict):
"""A storage system for cached items which reduces stampede collisions."""
def wait(self, key, timeout=5, debug=False):
"""Return the cached value for the given key, or None.
If timeout is not None, and the value is already
being calculated by another thread, wait until the given timeout has
elapsed. If the value is available before the timeout expires, it is
returned. If not, None is returned, and a sentinel placed in the cache
to signal other threads to wait.
If timeout is None, no waiting is performed nor sentinels used.
"""
value = self.get(key)
if isinstance(value, Event):
if timeout is None:
# Ignore the other thread and recalc it ourselves.
if debug:
cherrypy.log('No timeout', 'TOOLS.CACHING')
return None
# Wait until it's done or times out.
if debug:
cherrypy.log('Waiting up to %s seconds' %
timeout, 'TOOLS.CACHING')
value.wait(timeout)
if value.result is not None:
# The other thread finished its calculation. Use it.
if debug:
cherrypy.log('Result!', 'TOOLS.CACHING')
return value.result
# Timed out. Stick an Event in the slot so other threads wait
# on this one to finish calculating the value.
if debug:
cherrypy.log('Timed out', 'TOOLS.CACHING')
e = threading.Event()
e.result = None
dict.__setitem__(self, key, e)
return None
elif value is None:
# Stick an Event in the slot so other threads wait
# on this one to finish calculating the value.
if debug:
cherrypy.log('Timed out', 'TOOLS.CACHING')
e = threading.Event()
e.result = None
dict.__setitem__(self, key, e)
return value
def __setitem__(self, key, value):
"""Set the cached value for the given key."""
existing = self.get(key)
dict.__setitem__(self, key, value)
if isinstance(existing, Event):
# Set Event.result so other threads waiting on it have
# immediate access without needing to poll the cache again.
existing.result = value
existing.set()
class MemoryCache(Cache):
"""An in-memory cache for varying response content.
Each key in self.store is a URI, and each value is an AntiStampedeCache.
The response for any given URI may vary based on the values of
"selecting request headers"; that is, those named in the Vary
response header. We assume the list of header names to be constant
for each URI throughout the lifetime of the application, and store
that list in ``self.store[uri].selecting_headers``.
The items contained in ``self.store[uri]`` have keys which are tuples of
request header values (in the same order as the names in its
selecting_headers), and values which are the actual responses.
"""
maxobjects = 1000
"""The maximum number of cached objects; defaults to 1000."""
maxobj_size = 100000
"""The maximum size of each cached object in bytes; defaults to 100 KB."""
maxsize = 10000000
"""The maximum size of the entire cache in bytes; defaults to 10 MB."""
delay = 600
"""Seconds until the cached content expires; defaults to 600 (10 minutes).
"""
antistampede_timeout = 5
"""Seconds to wait for other threads to release a cache lock."""
expire_freq = 0.1
"""Seconds to sleep between cache expiration sweeps."""
debug = False
def __init__(self):
self.clear()
# Run self.expire_cache in a separate daemon thread.
t = threading.Thread(target=self.expire_cache, name='expire_cache')
self.expiration_thread = t
t.daemon = True
t.start()
def clear(self):
"""Reset the cache to its initial, empty state."""
self.store = {}
self.expirations = {}
self.tot_puts = 0
self.tot_gets = 0
@@ -34,191 +183,262 @@ class MemoryCache:
self.tot_expires = 0
self.tot_non_modified = 0
self.cursize = 0
def key(self):
return cherrypy.url(qs=cherrypy.request.query_string)
def expire_cache(self):
# expire_cache runs in a separate thread which the servers are
# not aware of. It's possible that "time" will be set to None
"""Continuously examine cached objects, expiring stale ones.
This function is designed to be run in its own daemon thread,
referenced at ``self.expiration_thread``.
"""
# It's possible that "time" will be set to None
# arbitrarily, so we check "while time" to avoid exceptions.
# See tickets #99 and #180 for more information.
while time:
now = time.time()
for expiration_time, objects in self.expirations.items():
# Must make a copy of expirations so it doesn't change size
# during iteration
for expiration_time, objects in copyitems(self.expirations):
if expiration_time <= now:
for obj_size, obj_key in objects:
for obj_size, uri, sel_header_values in objects:
try:
del self.cache[obj_key]
del self.store[uri][tuple(sel_header_values)]
self.tot_expires += 1
self.cursize -= obj_size
except KeyError:
# the key may have been deleted elsewhere
pass
del self.expirations[expiration_time]
time.sleep(0.1)
time.sleep(self.expire_freq)
def get(self):
"""Return the object if in the cache, else None."""
"""Return the current variant if in the cache, else None."""
request = cherrypy.serving.request
self.tot_gets += 1
cache_item = self.cache.get(self.key(), None)
if cache_item:
self.tot_hist += 1
return cache_item
else:
uri = cherrypy.url(qs=request.query_string)
uricache = self.store.get(uri)
if uricache is None:
return None
def put(self, obj):
if len(self.cache) < self.maxobjects:
# Size check no longer includes header length
obj_size = len(obj[2])
total_size = self.cursize + obj_size
header_values = [request.headers.get(h, '')
for h in uricache.selecting_headers]
variant = uricache.wait(key=tuple(sorted(header_values)),
timeout=self.antistampede_timeout,
debug=self.debug)
if variant is not None:
self.tot_hist += 1
return variant
def put(self, variant, size):
"""Store the current variant in the cache."""
request = cherrypy.serving.request
response = cherrypy.serving.response
uri = cherrypy.url(qs=request.query_string)
uricache = self.store.get(uri)
if uricache is None:
uricache = AntiStampedeCache()
uricache.selecting_headers = [
e.value for e in response.headers.elements('Vary')]
self.store[uri] = uricache
if len(self.store) < self.maxobjects:
total_size = self.cursize + size
# checks if there's space for the object
if (obj_size < self.maxobj_size and total_size < self.maxsize):
# add to the expirations list and cache
expiration_time = cherrypy.response.time + self.delay
obj_key = self.key()
if (size < self.maxobj_size and total_size < self.maxsize):
# add to the expirations list
expiration_time = response.time + self.delay
bucket = self.expirations.setdefault(expiration_time, [])
bucket.append((obj_size, obj_key))
self.cache[obj_key] = obj
bucket.append((size, uri, uricache.selecting_headers))
# add to the cache
header_values = [request.headers.get(h, '')
for h in uricache.selecting_headers]
uricache[tuple(sorted(header_values))] = variant
self.tot_puts += 1
self.cursize = total_size
def delete(self):
self.cache.pop(self.key(), None)
"""Remove ALL cached variants of the current resource."""
uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
self.store.pop(uri, None)
def get(invalid_methods=("POST", "PUT", "DELETE"), **kwargs):
def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs):
"""Try to obtain cached output. If fresh enough, raise HTTPError(304).
If POST, PUT, or DELETE:
* invalidates (deletes) any cached response for this resource
* sets request.cached = False
* sets request.cacheable = False
else if a cached copy exists:
* sets request.cached = True
* sets request.cacheable = False
* sets response.headers to the cached values
* checks the cached Last-Modified response header against the
current If-(Un)Modified-Since request headers; raises 304
if necessary.
current If-(Un)Modified-Since request headers; raises 304
if necessary.
* sets response.status and response.body to the cached values
* returns True
otherwise:
* sets request.cached = False
* sets request.cacheable = True
* returns False
"""
request = cherrypy.request
request = cherrypy.serving.request
response = cherrypy.serving.response
if not hasattr(cherrypy, '_cache'):
# Make a process-wide Cache object.
cherrypy._cache = kwargs.pop('cache_class', MemoryCache)()
# Take all remaining kwargs and set them on the Cache object.
for k, v in kwargs.items():
setattr(cherrypy._cache, k, v)
cherrypy._cache.debug = debug
# POST, PUT, DELETE should invalidate (delete) the cached copy.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
if request.method in invalid_methods:
if debug:
cherrypy.log('request.method %r in invalid_methods %r' %
(request.method, invalid_methods), 'TOOLS.CACHING')
cherrypy._cache.delete()
request.cached = False
request.cacheable = False
return False
if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
request.cached = False
request.cacheable = True
return False
cache_data = cherrypy._cache.get()
request.cached = c = bool(cache_data)
request.cacheable = not c
if c:
response = cherrypy.response
s, h, b, create_time, original_req_headers = cache_data
# Check 'Vary' selecting headers. If any headers mentioned in "Vary"
# differ between the cached and current request, bail out and
# let the rest of CP handle the request. This should properly
# mimic the behavior of isolated caches as RFC 2616 assumes:
# "If the selecting request header fields for the cached entry
# do not match the selecting request header fields of the new
# request, then the cache MUST NOT use a cached entry to satisfy
# the request unless it first relays the new request to the origin
# server in a conditional request and the server responds with
# 304 (Not Modified), including an entity tag or Content-Location
# that indicates the entity to be used.
# TODO: can we store multiple variants based on Vary'd headers?
for header_element in h.elements('Vary'):
key = header_element.value
if original_req_headers[key] != request.headers.get(key, 'missing'):
request.cached = bool(cache_data)
request.cacheable = not request.cached
if request.cached:
# Serve the cached copy.
max_age = cherrypy._cache.delay
for v in [e.value for e in request.headers.elements('Cache-Control')]:
atoms = v.split('=', 1)
directive = atoms.pop(0)
if directive == 'max-age':
if len(atoms) != 1 or not atoms[0].isdigit():
raise cherrypy.HTTPError(
400, 'Invalid Cache-Control header')
max_age = int(atoms[0])
break
elif directive == 'no-cache':
if debug:
cherrypy.log(
'Ignoring cache due to Cache-Control: no-cache',
'TOOLS.CACHING')
request.cached = False
request.cacheable = True
return False
# Copy the response headers. See http://www.cherrypy.org/ticket/721.
response.headers = rh = http.HeaderMap()
if debug:
cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
s, h, b, create_time = cache_data
age = int(response.time - create_time)
if (age > max_age):
if debug:
cherrypy.log('Ignoring cache due to age > %d' % max_age,
'TOOLS.CACHING')
request.cached = False
request.cacheable = True
return False
# Copy the response headers. See
# https://github.com/cherrypy/cherrypy/issues/721.
response.headers = rh = httputil.HeaderMap()
for k in h:
dict.__setitem__(rh, k, dict.__getitem__(h, k))
# Add the required Age header
response.headers["Age"] = str(int(response.time - create_time))
response.headers['Age'] = str(age)
try:
# Note that validate_since depends on a Last-Modified header;
# this was put into the cached copy, and should have been
# resurrected just above (response.headers = cache_data[1]).
cptools.validate_since()
except cherrypy.HTTPRedirect, x:
except cherrypy.HTTPRedirect:
x = sys.exc_info()[1]
if x.status == 304:
cherrypy._cache.tot_non_modified += 1
raise
# serve it & get out from the request
response.status = s
response.body = b
return c
else:
if debug:
cherrypy.log('request is not cached', 'TOOLS.CACHING')
return request.cached
def tee_output():
"""Tee response output to cache storage. Internal."""
# Used by CachingTool by attaching to request.hooks
request = cherrypy.serving.request
if 'no-store' in request.headers.values('Cache-Control'):
return
def tee(body):
"""Tee response.body into a list."""
if ('no-cache' in response.headers.values('Pragma') or
'no-store' in response.headers.values('Cache-Control')):
for chunk in body:
yield chunk
return
output = []
for chunk in body:
output.append(chunk)
yield chunk
# Might as well do this here; why cache if the body isn't consumed?
if response.headers.get('Pragma', None) != 'no-cache':
# save the cache data
body = ''.join(output)
vary = [he.value for he in
cherrypy.response.headers.elements('Vary')]
if vary:
sel_headers = dict([(k, v) for k, v
in cherrypy.request.headers.iteritems()
if k in vary])
else:
sel_headers = {}
cherrypy._cache.put((response.status, response.headers or {},
body, response.time, sel_headers))
response = cherrypy.response
# save the cache data
body = ntob('').join(output)
cherrypy._cache.put((response.status, response.headers or {},
body, response.time), len(body))
response = cherrypy.serving.response
response.body = tee(response.body)
def expires(secs=0, force=False):
def expires(secs=0, force=False, debug=False):
"""Tool for influencing cache mechanisms using the 'Expires' header.
'secs' must be either an int or a datetime.timedelta, and indicates the
number of seconds between response.time and when the response should
expire. The 'Expires' header will be set to (response.time + secs).
If 'secs' is zero, the 'Expires' header is set one year in the past, and
the following "cache prevention" headers are also set:
'Pragma': 'no-cache'
'Cache-Control': 'no-cache, must-revalidate'
If 'force' is False (the default), the following headers are checked:
'Etag', 'Last-Modified', 'Age', 'Expires'. If any are already present,
none of the above response headers are set.
secs
Must be either an int or a datetime.timedelta, and indicates the
number of seconds between response.time and when the response should
expire. The 'Expires' header will be set to response.time + secs.
If secs is zero, the 'Expires' header is set one year in the past, and
the following "cache prevention" headers are also set:
* Pragma: no-cache
* Cache-Control': no-cache, must-revalidate
force
If False, the following headers are checked:
* Etag
* Last-Modified
* Age
* Expires
If any are already present, none of the above response headers are set.
"""
response = cherrypy.response
response = cherrypy.serving.response
headers = response.headers
cacheable = False
if not force:
# some header names that indicate that the response can be cached
@@ -226,20 +446,25 @@ def expires(secs=0, force=False):
if indicator in headers:
cacheable = True
break
if not cacheable:
if not cacheable and not force:
if debug:
cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
else:
if debug:
cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
if isinstance(secs, datetime.timedelta):
secs = (86400 * secs.days) + secs.seconds
if secs == 0:
if force or "Pragma" not in headers:
headers["Pragma"] = "no-cache"
if cherrypy.request.protocol >= (1, 1):
if force or "Cache-Control" not in headers:
headers["Cache-Control"] = "no-cache, must-revalidate"
if force or ('Pragma' not in headers):
headers['Pragma'] = 'no-cache'
if cherrypy.serving.request.protocol >= (1, 1):
if force or 'Cache-Control' not in headers:
headers['Cache-Control'] = 'no-cache, must-revalidate'
# Set an explicit Expires date in the past.
expiry = http.HTTPDate(1169942400.0)
expiry = httputil.HTTPDate(1169942400.0)
else:
expiry = http.HTTPDate(response.time + secs)
if force or "Expires" not in headers:
headers["Expires"] = expiry
expiry = httputil.HTTPDate(response.time + secs)
if force or 'Expires' not in headers:
headers['Expires'] = expiry

View File

@@ -1,55 +1,58 @@
"""Code-coverage tools for CherryPy.
To use this module, or the coverage tools in the test suite,
you need to download 'coverage.py', either Gareth Rees' original
implementation:
http://www.garethrees.org/2001/12/04/python-coverage/
you need to download 'coverage.py', either Gareth Rees' `original
implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
or Ned Batchelder's `enhanced version:
<http://www.nedbatchelder.com/code/modules/coverage.html>`_
or Ned Batchelder's enhanced version:
http://www.nedbatchelder.com/code/modules/coverage.html
To turn on coverage tracing, use the following code:
To turn on coverage tracing, use the following code::
cherrypy.engine.subscribe('start', covercp.start)
cherrypy.engine.subscribe('start_thread', covercp.start)
Run your code, then use the covercp.serve() function to browse the
DO NOT subscribe anything on the 'start_thread' channel, as previously
recommended. Calling start once in the main thread should be sufficient
to start coverage on all threads. Calling start again in each thread
effectively clears any coverage data gathered up to that point.
Run your code, then use the ``covercp.serve()`` function to browse the
results in a web browser. If you run this module from the command line,
it will call serve() for you.
it will call ``serve()`` for you.
"""
import re
import sys
import cgi
import urllib
import os, os.path
localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
import os
import os.path
try:
import cStringIO as StringIO
except ImportError:
import StringIO
import cherrypy
from cherrypy._cpcompat import quote_plus
localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache')
the_coverage = None
try:
from coverage import the_coverage as coverage
def start(threadid=None):
coverage.start()
from coverage import coverage
the_coverage = coverage(data_file=localFile)
def start():
the_coverage.start()
except ImportError:
# Setting coverage to None will raise errors
# Setting the_coverage to None will raise errors
# that need to be trapped downstream.
coverage = None
the_coverage = None
import warnings
warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
def start(threadid=None):
warnings.warn(
'No code coverage will be performed; '
'coverage.py could not be imported.')
def start():
pass
start.priority = 20
# Guess initial depth to hide FIXME this doesn't work for non-cherrypy stuff
import cherrypy
initial_base = os.path.dirname(cherrypy.__file__)
TEMPLATE_MENU = """<html>
<head>
<title>CherryPy Coverage Menu</title>
@@ -74,7 +77,7 @@ TEMPLATE_MENU = """<html>
font-size: small;
font-weight: bold;
font-style: italic;
margin-top: 5px;
margin-top: 5px;
}
input { border: 1px solid #ccc; padding: 2px; }
.directory {
@@ -123,15 +126,18 @@ TEMPLATE_FORM = """
<div id="options">
<form action='menu' method=GET>
<input type='hidden' name='base' value='%(base)s' />
Show percentages <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
Show percentages
<input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
Hide files over
<input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
Exclude files matching<br />
<input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' />
<input type='text' id='exclude' name='exclude'
value='%(exclude)s' size='20' />
<br />
<input type='submit' value='Change view' id="submit"/>
</form>
</div>"""
</div>"""
TEMPLATE_FRAMESET = """<html>
<head><title>CherryPy coverage data</title></head>
@@ -140,7 +146,7 @@ TEMPLATE_FRAMESET = """<html>
<frame name='main' src='' />
</frameset>
</html>
""" % initial_base.lower()
"""
TEMPLATE_COVERAGE = """<html>
<head>
@@ -178,7 +184,10 @@ TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
<td>%s</td>
</tr>\n"""
TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
TEMPLATE_ITEM = (
"%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
)
def _percent(statements, missing):
s = len(statements)
@@ -187,32 +196,40 @@ def _percent(statements, missing):
return int(round(100.0 * e / s))
return 0
def _show_branch(root, base, path, pct=0, showpct=False, exclude=""):
def _show_branch(root, base, path, pct=0, showpct=False, exclude='',
coverage=the_coverage):
# Show the directory name and any of our children
dirs = [k for k, v in root.iteritems() if v]
dirs = [k for k, v in root.items() if v]
dirs.sort()
for name in dirs:
newpath = os.path.join(path, name)
if newpath.lower().startswith(base):
relpath = newpath[len(base):]
yield "| " * relpath.count(os.sep)
yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
(newpath, urllib.quote_plus(exclude), name)
for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude):
yield '| ' * relpath.count(os.sep)
yield (
"<a class='directory' "
"href='menu?base=%s&exclude=%s'>%s</a>\n" %
(newpath, quote_plus(exclude), name)
)
for chunk in _show_branch(
root[name], base, newpath, pct, showpct,
exclude, coverage=coverage
):
yield chunk
# Now list the files
if path.lower().startswith(base):
relpath = path[len(base):]
files = [k for k, v in root.iteritems() if not v]
files = [k for k, v in root.items() if not v]
files.sort()
for name in files:
newpath = os.path.join(path, name)
pc_str = ""
pc_str = ''
if showpct:
try:
_, statements, _, missing, _ = coverage.analysis2(newpath)
@@ -221,22 +238,24 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude=""):
pass
else:
pc = _percent(statements, missing)
pc_str = ("%3d%% " % pc).replace(' ','&nbsp;')
pc_str = ('%3d%% ' % pc).replace(' ', '&nbsp;')
if pc < float(pct) or pc == -1:
pc_str = "<span class='fail'>%s</span>" % pc_str
else:
pc_str = "<span class='pass'>%s</span>" % pc_str
yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1),
pc_str, newpath, name)
def _skip_file(path, exclude):
if exclude:
return bool(re.search(exclude, path))
def _graft(path, tree):
d = tree
p = path
atoms = []
while True:
@@ -245,72 +264,82 @@ def _graft(path, tree):
break
atoms.append(tail)
atoms.append(p)
if p != "/":
atoms.append("/")
if p != '/':
atoms.append('/')
atoms.reverse()
for node in atoms:
if node:
d = d.setdefault(node, {})
def get_tree(base, exclude):
def get_tree(base, exclude, coverage=the_coverage):
"""Return covered module names as a nested dict."""
tree = {}
coverage.get_ready()
runs = coverage.cexecuted.keys()
if runs:
for path in runs:
if not _skip_file(path, exclude) and not os.path.isdir(path):
_graft(path, tree)
runs = coverage.data.executed_files()
for path in runs:
if not _skip_file(path, exclude) and not os.path.isdir(path):
_graft(path, tree)
return tree
class CoverStats(object):
def __init__(self, coverage, root=None):
self.coverage = coverage
if root is None:
# Guess initial depth. Files outside this path will not be
# reachable from the web interface.
import cherrypy
root = os.path.dirname(cherrypy.__file__)
self.root = root
@cherrypy.expose
def index(self):
return TEMPLATE_FRAMESET
index.exposed = True
def menu(self, base="/", pct="50", showpct="",
return TEMPLATE_FRAMESET % self.root.lower()
@cherrypy.expose
def menu(self, base='/', pct='50', showpct='',
exclude=r'python\d\.\d|test|tut\d|tutorial'):
# The coverage module uses all-lower-case names.
base = base.lower().rstrip(os.sep)
yield TEMPLATE_MENU
yield TEMPLATE_FORM % locals()
# Start by showing links for parent paths
yield "<div id='crumbs'>"
path = ""
path = ''
atoms = base.split(os.sep)
atoms.pop()
for atom in atoms:
path += atom + os.sep
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
% (path, urllib.quote_plus(exclude), atom, os.sep))
yield "</div>"
% (path, quote_plus(exclude), atom, os.sep))
yield '</div>'
yield "<div id='tree'>"
# Then display the tree
tree = get_tree(base, exclude)
tree = get_tree(base, exclude, self.coverage)
if not tree:
yield "<p>No modules covered.</p>"
yield '<p>No modules covered.</p>'
else:
for chunk in _show_branch(tree, base, "/", pct,
showpct=='checked', exclude):
for chunk in _show_branch(tree, base, '/', pct,
showpct == 'checked', exclude,
coverage=self.coverage):
yield chunk
yield "</div>"
yield "</body></html>"
menu.exposed = True
yield '</div>'
yield '</body></html>'
def annotated_file(self, filename, statements, excluded, missing):
source = open(filename, 'r')
buffer = []
for lineno, line in enumerate(source.readlines()):
lineno += 1
line = line.strip("\n\r")
line = line.strip('\n\r')
empty_the_buffer = True
if lineno in excluded:
template = TEMPLATE_LOC_EXCLUDED
@@ -326,10 +355,11 @@ class CoverStats(object):
yield template % (lno, cgi.escape(pastline))
buffer = []
yield template % (lineno, cgi.escape(line))
@cherrypy.expose
def report(self, name):
coverage.get_ready()
filename, statements, excluded, missing, _ = coverage.analysis2(name)
filename, statements, excluded, missing, _ = self.coverage.analysis2(
name)
pc = _percent(statements, missing)
yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
fullpath=name,
@@ -341,21 +371,21 @@ class CoverStats(object):
yield '</table>'
yield '</body>'
yield '</html>'
report.exposed = True
def serve(path=localFile, port=8080):
def serve(path=localFile, port=8080, root=None):
if coverage is None:
raise ImportError("The coverage module could not be imported.")
coverage.cache_default = path
raise ImportError('The coverage module could not be imported.')
from coverage import coverage
cov = coverage(data_file=path)
cov.load()
import cherrypy
cherrypy.config.update({'server.socket_port': port,
cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10,
'environment': "production",
'environment': 'production',
})
cherrypy.quickstart(CoverStats())
cherrypy.quickstart(CoverStats(cov, root))
if __name__ == "__main__":
if __name__ == '__main__':
serve(*tuple(sys.argv[1:]))

690
cherrypy/lib/cpstats.py Normal file
View File

@@ -0,0 +1,690 @@
"""CPStats, a package for collecting and reporting on program statistics.
Overview
========
Statistics about program operation are an invaluable monitoring and debugging
tool. Unfortunately, the gathering and reporting of these critical values is
usually ad-hoc. This package aims to add a centralized place for gathering
statistical performance data, a structure for recording that data which
provides for extrapolation of that data into more useful information,
and a method of serving that data to both human investigators and
monitoring software. Let's examine each of those in more detail.
Data Gathering
--------------
Just as Python's `logging` module provides a common importable for gathering
and sending messages, performance statistics would benefit from a similar
common mechanism, and one that does *not* require each package which wishes
to collect stats to import a third-party module. Therefore, we choose to
re-use the `logging` module by adding a `statistics` object to it.
That `logging.statistics` object is a nested dict. It is not a custom class,
because that would:
1. require libraries and applications to import a third-party module in
order to participate
2. inhibit innovation in extrapolation approaches and in reporting tools, and
3. be slow.
There are, however, some specifications regarding the structure of the dict.::
{
+----"SQLAlchemy": {
| "Inserts": 4389745,
| "Inserts per Second":
| lambda s: s["Inserts"] / (time() - s["Start"]),
| C +---"Table Statistics": {
| o | "widgets": {-----------+
N | l | "Rows": 1.3M, | Record
a | l | "Inserts": 400, |
m | e | },---------------------+
e | c | "froobles": {
s | t | "Rows": 7845,
p | i | "Inserts": 0,
a | o | },
c | n +---},
e | "Slow Queries":
| [{"Query": "SELECT * FROM widgets;",
| "Processing Time": 47.840923343,
| },
| ],
+----},
}
The `logging.statistics` dict has four levels. The topmost level is nothing
more than a set of names to introduce modularity, usually along the lines of
package names. If the SQLAlchemy project wanted to participate, for example,
it might populate the item `logging.statistics['SQLAlchemy']`, whose value
would be a second-layer dict we call a "namespace". Namespaces help multiple
packages to avoid collisions over key names, and make reports easier to read,
to boot. The maintainers of SQLAlchemy should feel free to use more than one
namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case
or other syntax constraints on the namespace names; they should be chosen
to be maximally readable by humans (neither too short nor too long).
Each namespace, then, is a dict of named statistical values, such as
'Requests/sec' or 'Uptime'. You should choose names which will look
good on a report: spaces and capitalization are just fine.
In addition to scalars, values in a namespace MAY be a (third-layer)
dict, or a list, called a "collection". For example, the CherryPy
:class:`StatsTool` keeps track of what each request is doing (or has most
recently done) in a 'Requests' collection, where each key is a thread ID; each
value in the subdict MUST be a fourth dict (whew!) of statistical data about
each thread. We call each subdict in the collection a "record". Similarly,
the :class:`StatsTool` also keeps a list of slow queries, where each record
contains data about each slow query, in order.
Values in a namespace or record may also be functions, which brings us to:
Extrapolation
-------------
The collection of statistical data needs to be fast, as close to unnoticeable
as possible to the host program. That requires us to minimize I/O, for example,
but in Python it also means we need to minimize function calls. So when you
are designing your namespace and record values, try to insert the most basic
scalar values you already have on hand.
When it comes time to report on the gathered data, however, we usually have
much more freedom in what we can calculate. Therefore, whenever reporting
tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents
of `logging.statistics` for reporting, they first call
`extrapolate_statistics` (passing the whole `statistics` dict as the only
argument). This makes a deep copy of the statistics dict so that the
reporting tool can both iterate over it and even change it without harming
the original. But it also expands any functions in the dict by calling them.
For example, you might have a 'Current Time' entry in the namespace with the
value "lambda scope: time.time()". The "scope" parameter is the current
namespace dict (or record, if we're currently expanding one of those
instead), allowing you access to existing static entries. If you're truly
evil, you can even modify more than one entry at a time.
However, don't try to calculate an entry and then use its value in further
extrapolations; the order in which the functions are called is not guaranteed.
This can lead to a certain amount of duplicated work (or a redesign of your
schema), but that's better than complicating the spec.
After the whole thing has been extrapolated, it's time for:
Reporting
---------
The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates
it all, and then transforms it to HTML for easy viewing. Each namespace gets
its own header and attribute table, plus an extra table for each collection.
This is NOT part of the statistics specification; other tools can format how
they like.
You can control which columns are output and how they are formatted by updating
StatsPage.formatting, which is a dict that mirrors the keys and nesting of
`logging.statistics`. The difference is that, instead of data values, it has
formatting values. Use None for a given key to indicate to the StatsPage that a
given column should not be output. Use a string with formatting
(such as '%.3f') to interpolate the value(s), or use a callable (such as
lambda v: v.isoformat()) for more advanced formatting. Any entry which is not
mentioned in the formatting dict is output unchanged.
Monitoring
----------
Although the HTML output takes pains to assign unique id's to each <td> with
statistical data, you're probably better off fetching /cpstats/data, which
outputs the whole (extrapolated) `logging.statistics` dict in JSON format.
That is probably easier to parse, and doesn't have any formatting controls,
so you get the "original" data in a consistently-serialized format.
Note: there's no treatment yet for datetime objects. Try time.time() instead
for now if you can. Nagios will probably thank you.
Turning Collection Off
----------------------
It is recommended each namespace have an "Enabled" item which, if False,
stops collection (but not reporting) of statistical data. Applications
SHOULD provide controls to pause and resume collection by setting these
entries to False or True, if present.
Usage
=====
To collect statistics on CherryPy applications::
from cherrypy.lib import cpstats
appconfig['/']['tools.cpstats.on'] = True
To collect statistics on your own code::
import logging
# Initialize the repository
if not hasattr(logging, 'statistics'): logging.statistics = {}
# Initialize my namespace
mystats = logging.statistics.setdefault('My Stuff', {})
# Initialize my namespace's scalars and collections
mystats.update({
'Enabled': True,
'Start Time': time.time(),
'Important Events': 0,
'Events/Second': lambda s: (
(s['Important Events'] / (time.time() - s['Start Time']))),
})
...
for event in events:
...
# Collect stats
if mystats.get('Enabled', False):
mystats['Important Events'] += 1
To report statistics::
root.cpstats = cpstats.StatsPage()
To format statistics reports::
See 'Reporting', above.
"""
import logging
import os
import sys
import threading
import time
import cherrypy
from cherrypy._cpcompat import json
# ------------------------------- Statistics -------------------------------- #
if not hasattr(logging, 'statistics'):
logging.statistics = {}
def extrapolate_statistics(scope):
"""Return an extrapolated copy of the given scope."""
c = {}
for k, v in list(scope.items()):
if isinstance(v, dict):
v = extrapolate_statistics(v)
elif isinstance(v, (list, tuple)):
v = [extrapolate_statistics(record) for record in v]
elif hasattr(v, '__call__'):
v = v(scope)
c[k] = v
return c
# -------------------- CherryPy Applications Statistics --------------------- #
appstats = logging.statistics.setdefault('CherryPy Applications', {})
appstats.update({
'Enabled': True,
'Bytes Read/Request': lambda s: (
s['Total Requests'] and
(s['Total Bytes Read'] / float(s['Total Requests'])) or
0.0
),
'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s),
'Bytes Written/Request': lambda s: (
s['Total Requests'] and
(s['Total Bytes Written'] / float(s['Total Requests'])) or
0.0
),
'Bytes Written/Second': lambda s: (
s['Total Bytes Written'] / s['Uptime'](s)
),
'Current Time': lambda s: time.time(),
'Current Requests': 0,
'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s),
'Server Version': cherrypy.__version__,
'Start Time': time.time(),
'Total Bytes Read': 0,
'Total Bytes Written': 0,
'Total Requests': 0,
'Total Time': 0,
'Uptime': lambda s: time.time() - s['Start Time'],
'Requests': {},
})
proc_time = lambda s: time.time() - s['Start Time']
class ByteCountWrapper(object):
"""Wraps a file-like object, counting the number of bytes read."""
def __init__(self, rfile):
self.rfile = rfile
self.bytes_read = 0
def read(self, size=-1):
data = self.rfile.read(size)
self.bytes_read += len(data)
return data
def readline(self, size=-1):
data = self.rfile.readline(size)
self.bytes_read += len(data)
return data
def readlines(self, sizehint=0):
# Shamelessly stolen from StringIO
total = 0
lines = []
line = self.readline()
while line:
lines.append(line)
total += len(line)
if 0 < sizehint <= total:
break
line = self.readline()
return lines
def close(self):
self.rfile.close()
def __iter__(self):
return self
def next(self):
data = self.rfile.next()
self.bytes_read += len(data)
return data
average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0
def _get_threading_ident():
if sys.version_info >= (3, 3):
return threading.get_ident()
return threading._get_ident()
class StatsTool(cherrypy.Tool):
"""Record various information about the current request."""
def __init__(self):
cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
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.
"""
if appstats.get('Enabled', False):
cherrypy.Tool._setup(self)
self.record_start()
def record_start(self):
"""Record the beginning of a request."""
request = cherrypy.serving.request
if not hasattr(request.rfile, 'bytes_read'):
request.rfile = ByteCountWrapper(request.rfile)
request.body.fp = request.rfile
r = request.remote
appstats['Current Requests'] += 1
appstats['Total Requests'] += 1
appstats['Requests'][_get_threading_ident()] = {
'Bytes Read': None,
'Bytes Written': None,
# Use a lambda so the ip gets updated by tools.proxy later
'Client': lambda s: '%s:%s' % (r.ip, r.port),
'End Time': None,
'Processing Time': proc_time,
'Request-Line': request.request_line,
'Response Status': None,
'Start Time': time.time(),
}
def record_stop(
self, uriset=None, slow_queries=1.0, slow_queries_count=100,
debug=False, **kwargs):
"""Record the end of a request."""
resp = cherrypy.serving.response
w = appstats['Requests'][_get_threading_ident()]
r = cherrypy.request.rfile.bytes_read
w['Bytes Read'] = r
appstats['Total Bytes Read'] += r
if resp.stream:
w['Bytes Written'] = 'chunked'
else:
cl = int(resp.headers.get('Content-Length', 0))
w['Bytes Written'] = cl
appstats['Total Bytes Written'] += cl
w['Response Status'] = getattr(
resp, 'output_status', None) or resp.status
w['End Time'] = time.time()
p = w['End Time'] - w['Start Time']
w['Processing Time'] = p
appstats['Total Time'] += p
appstats['Current Requests'] -= 1
if debug:
cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
if uriset:
rs = appstats.setdefault('URI Set Tracking', {})
r = rs.setdefault(uriset, {
'Min': None, 'Max': None, 'Count': 0, 'Sum': 0,
'Avg': average_uriset_time})
if r['Min'] is None or p < r['Min']:
r['Min'] = p
if r['Max'] is None or p > r['Max']:
r['Max'] = p
r['Count'] += 1
r['Sum'] += p
if slow_queries and p > slow_queries:
sq = appstats.setdefault('Slow Queries', [])
sq.append(w.copy())
if len(sq) > slow_queries_count:
sq.pop(0)
cherrypy.tools.cpstats = StatsTool()
# ---------------------- CherryPy Statistics Reporting ---------------------- #
thisdir = os.path.abspath(os.path.dirname(__file__))
missing = object()
locale_date = lambda v: time.strftime('%c', time.gmtime(v))
iso_format = lambda v: time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
def pause_resume(ns):
def _pause_resume(enabled):
pause_disabled = ''
resume_disabled = ''
if enabled:
resume_disabled = 'disabled="disabled" '
else:
pause_disabled = 'disabled="disabled" '
return """
<form action="pause" method="POST" style="display:inline">
<input type="hidden" name="namespace" value="%s" />
<input type="submit" value="Pause" %s/>
</form>
<form action="resume" method="POST" style="display:inline">
<input type="hidden" name="namespace" value="%s" />
<input type="submit" value="Resume" %s/>
</form>
""" % (ns, pause_disabled, ns, resume_disabled)
return _pause_resume
class StatsPage(object):
formatting = {
'CherryPy Applications': {
'Enabled': pause_resume('CherryPy Applications'),
'Bytes Read/Request': '%.3f',
'Bytes Read/Second': '%.3f',
'Bytes Written/Request': '%.3f',
'Bytes Written/Second': '%.3f',
'Current Time': iso_format,
'Requests/Second': '%.3f',
'Start Time': iso_format,
'Total Time': '%.3f',
'Uptime': '%.3f',
'Slow Queries': {
'End Time': None,
'Processing Time': '%.3f',
'Start Time': iso_format,
},
'URI Set Tracking': {
'Avg': '%.3f',
'Max': '%.3f',
'Min': '%.3f',
'Sum': '%.3f',
},
'Requests': {
'Bytes Read': '%s',
'Bytes Written': '%s',
'End Time': None,
'Processing Time': '%.3f',
'Start Time': None,
},
},
'CherryPy WSGIServer': {
'Enabled': pause_resume('CherryPy WSGIServer'),
'Connections/second': '%.3f',
'Start time': iso_format,
},
}
@cherrypy.expose
def index(self):
# Transform the raw data into pretty output for HTML
yield """
<html>
<head>
<title>Statistics</title>
<style>
th, td {
padding: 0.25em 0.5em;
border: 1px solid #666699;
}
table {
border-collapse: collapse;
}
table.stats1 {
width: 100%;
}
table.stats1 th {
font-weight: bold;
text-align: right;
background-color: #CCD5DD;
}
table.stats2, h2 {
margin-left: 50px;
}
table.stats2 th {
font-weight: bold;
text-align: center;
background-color: #CCD5DD;
}
</style>
</head>
<body>
"""
for title, scalars, collections in self.get_namespaces():
yield """
<h1>%s</h1>
<table class='stats1'>
<tbody>
""" % title
for i, (key, value) in enumerate(scalars):
colnum = i % 3
if colnum == 0:
yield """
<tr>"""
yield (
"""
<th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" %
vars()
)
if colnum == 2:
yield """
</tr>"""
if colnum == 0:
yield """
<th></th><td></td>
<th></th><td></td>
</tr>"""
elif colnum == 1:
yield """
<th></th><td></td>
</tr>"""
yield """
</tbody>
</table>"""
for subtitle, headers, subrows in collections:
yield """
<h2>%s</h2>
<table class='stats2'>
<thead>
<tr>""" % subtitle
for key in headers:
yield """
<th>%s</th>""" % key
yield """
</tr>
</thead>
<tbody>"""
for subrow in subrows:
yield """
<tr>"""
for value in subrow:
yield """
<td>%s</td>""" % value
yield """
</tr>"""
yield """
</tbody>
</table>"""
yield """
</body>
</html>
"""
def get_namespaces(self):
"""Yield (title, scalars, collections) for each namespace."""
s = extrapolate_statistics(logging.statistics)
for title, ns in sorted(s.items()):
scalars = []
collections = []
ns_fmt = self.formatting.get(title, {})
for k, v in sorted(ns.items()):
fmt = ns_fmt.get(k, {})
if isinstance(v, dict):
headers, subrows = self.get_dict_collection(v, fmt)
collections.append((k, ['ID'] + headers, subrows))
elif isinstance(v, (list, tuple)):
headers, subrows = self.get_list_collection(v, fmt)
collections.append((k, headers, subrows))
else:
format = ns_fmt.get(k, missing)
if format is None:
# Don't output this column.
continue
if hasattr(format, '__call__'):
v = format(v)
elif format is not missing:
v = format % v
scalars.append((k, v))
yield title, scalars, collections
def get_dict_collection(self, v, formatting):
"""Return ([headers], [rows]) for the given collection."""
# E.g., the 'Requests' dict.
headers = []
try:
# python2
vals = v.itervalues()
except AttributeError:
# python3
vals = v.values()
for record in vals:
for k3 in record:
format = formatting.get(k3, missing)
if format is None:
# Don't output this column.
continue
if k3 not in headers:
headers.append(k3)
headers.sort()
subrows = []
for k2, record in sorted(v.items()):
subrow = [k2]
for k3 in headers:
v3 = record.get(k3, '')
format = formatting.get(k3, missing)
if format is None:
# Don't output this column.
continue
if hasattr(format, '__call__'):
v3 = format(v3)
elif format is not missing:
v3 = format % v3
subrow.append(v3)
subrows.append(subrow)
return headers, subrows
def get_list_collection(self, v, formatting):
"""Return ([headers], [subrows]) for the given collection."""
# E.g., the 'Slow Queries' list.
headers = []
for record in v:
for k3 in record:
format = formatting.get(k3, missing)
if format is None:
# Don't output this column.
continue
if k3 not in headers:
headers.append(k3)
headers.sort()
subrows = []
for record in v:
subrow = []
for k3 in headers:
v3 = record.get(k3, '')
format = formatting.get(k3, missing)
if format is None:
# Don't output this column.
continue
if hasattr(format, '__call__'):
v3 = format(v3)
elif format is not missing:
v3 = format % v3
subrow.append(v3)
subrows.append(subrow)
return headers, subrows
if json is not None:
@cherrypy.expose
def data(self):
s = extrapolate_statistics(logging.statistics)
cherrypy.response.headers['Content-Type'] = 'application/json'
return json.dumps(s, sort_keys=True, indent=4)
@cherrypy.expose
def pause(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = False
raise cherrypy.HTTPRedirect('./')
pause.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']}
@cherrypy.expose
def resume(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = True
raise cherrypy.HTTPRedirect('./')
resume.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']}

View File

@@ -1,97 +1,119 @@
"""Functions for builtin CherryPy tools."""
import logging
try:
# Python 2.5+
from hashlib import md5
except ImportError:
from md5 import new as md5
import re
from hashlib import md5
import six
import cherrypy
from cherrypy.lib import http as _http
from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import httputil as _httputil
from cherrypy.lib import is_iterator
# Conditional HTTP request support #
def validate_etags(autotags=False):
def validate_etags(autotags=False, debug=False):
"""Validate the current ETag against If-Match, If-None-Match headers.
If autotags is True, an ETag response-header value will be provided
from an MD5 hash of the response body (unless some other code has
already provided an ETag header). If False (the default), the ETag
will not be automatic.
WARNING: the autotags feature is not designed for URL's which allow
methods other than GET. For example, if a POST to the same URL returns
no content, the automatic ETag will be incorrect, breaking a fundamental
use for entity tags in a possibly destructive fashion. Likewise, if you
raise 304 Not Modified, the response body will be empty, the ETag hash
will be incorrect, and your application will break.
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
See :rfc:`2616` Section 14.24.
"""
response = cherrypy.response
response = cherrypy.serving.response
# Guard against being run twice.
if hasattr(response, "ETag"):
if hasattr(response, 'ETag'):
return
status, reason, msg = _http.valid_status(response.status)
status, reason, msg = _httputil.valid_status(response.status)
etag = response.headers.get('ETag')
# Automatic ETag generation. See warning in docstring.
if (not etag) and autotags:
if status == 200:
etag = response.collapse_body()
etag = '"%s"' % md5(etag).hexdigest()
response.headers['ETag'] = etag
if etag:
if debug:
cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS')
elif not autotags:
if debug:
cherrypy.log('Autotags off', 'TOOLS.ETAGS')
elif status != 200:
if debug:
cherrypy.log('Status not 200', 'TOOLS.ETAGS')
else:
etag = response.collapse_body()
etag = '"%s"' % md5(etag).hexdigest()
if debug:
cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
response.headers['ETag'] = etag
response.ETag = etag
# "If the request would, without the If-Match header field, result in
# anything other than a 2xx or 412 status, then the If-Match header
# MUST be ignored."
if debug:
cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
if status >= 200 and status <= 299:
request = cherrypy.request
request = cherrypy.serving.request
conditions = request.headers.elements('If-Match') or []
conditions = [str(x) for x in conditions]
if conditions and not (conditions == ["*"] or etag in conditions):
raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did "
"not match %r" % (etag, conditions))
if debug:
cherrypy.log('If-Match conditions: %s' % repr(conditions),
'TOOLS.ETAGS')
if conditions and not (conditions == ['*'] or etag in conditions):
raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did '
'not match %r' % (etag, conditions))
conditions = request.headers.elements('If-None-Match') or []
conditions = [str(x) for x in conditions]
if conditions == ["*"] or etag in conditions:
if request.method in ("GET", "HEAD"):
if debug:
cherrypy.log('If-None-Match conditions: %s' % repr(conditions),
'TOOLS.ETAGS')
if conditions == ['*'] or etag in conditions:
if debug:
cherrypy.log('request.method: %s' %
request.method, 'TOOLS.ETAGS')
if request.method in ('GET', 'HEAD'):
raise cherrypy.HTTPRedirect([], 304)
else:
raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r "
"matched %r" % (etag, conditions))
raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r '
'matched %r' % (etag, conditions))
def validate_since():
"""Validate the current Last-Modified against If-Modified-Since headers.
If no code has set the Last-Modified response header, then no validation
will be performed.
"""
response = cherrypy.response
response = cherrypy.serving.response
lastmod = response.headers.get('Last-Modified')
if lastmod:
status, reason, msg = _http.valid_status(response.status)
request = cherrypy.request
status, reason, msg = _httputil.valid_status(response.status)
request = cherrypy.serving.request
since = request.headers.get('If-Unmodified-Since')
if since and since != lastmod:
if (status >= 200 and status <= 299) or status == 412:
raise cherrypy.HTTPError(412)
since = request.headers.get('If-Modified-Since')
if since and since == lastmod:
if (status >= 200 and status <= 299) or status == 304:
if request.method in ("GET", "HEAD"):
if request.method in ('GET', 'HEAD'):
raise cherrypy.HTTPRedirect([], 304)
else:
raise cherrypy.HTTPError(412)
@@ -99,28 +121,64 @@ def validate_since():
# Tool code #
def allow(methods=None, debug=False):
"""Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
The given methods are case-insensitive, and may be in any order.
If only one method is allowed, you may supply a single string;
if more than one, supply a list of strings.
Regardless of whether the current method is allowed or not, this
also emits an 'Allow' response header, containing the given methods.
"""
if not isinstance(methods, (tuple, list)):
methods = [methods]
methods = [m.upper() for m in methods if m]
if not methods:
methods = ['GET', 'HEAD']
elif 'GET' in methods and 'HEAD' not in methods:
methods.append('HEAD')
cherrypy.response.headers['Allow'] = ', '.join(methods)
if cherrypy.request.method not in methods:
if debug:
cherrypy.log('request.method %r not in methods %r' %
(cherrypy.request.method, methods), 'TOOLS.ALLOW')
raise cherrypy.HTTPError(405)
else:
if debug:
cherrypy.log('request.method %r in methods %r' %
(cherrypy.request.method, methods), 'TOOLS.ALLOW')
def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
scheme='X-Forwarded-Proto'):
scheme='X-Forwarded-Proto', debug=False):
"""Change the base URL (scheme://host[:port][/path]).
For running a CP server behind Apache, lighttpd, or other HTTP server.
For Apache and lighttpd, you should leave the 'local' argument at the
default value of 'X-Forwarded-Host'. For Squid, you probably want to set
tools.proxy.local = 'Origin'.
If you want the new request.base to include path info (not just the host),
you must explicitly set base to the full base path, and ALSO set 'local'
to '', so that the X-Forwarded-Host request header (which never includes
path info) does not override it. Regardless, the value for 'base' MUST
NOT end in a slash.
cherrypy.request.remote.ip (the IP address of the client) will be
rewritten if the header specified by the 'remote' arg is valid.
By default, 'remote' is set to 'X-Forwarded-For'. If you do not
want to rewrite remote.ip, set the 'remote' arg to an empty string.
"""
request = cherrypy.request
request = cherrypy.serving.request
if scheme:
s = request.headers.get(scheme, None)
if debug:
cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY')
if s == 'on' and 'ssl' in scheme.lower():
# This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header
scheme = 'https'
@@ -128,165 +186,227 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
# This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
scheme = s
if not scheme:
scheme = request.base[:request.base.find("://")]
scheme = request.base[:request.base.find('://')]
if local:
base = request.headers.get(local, base)
lbase = request.headers.get(local, None)
if debug:
cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY')
if lbase is not None:
base = lbase.split(',')[0]
if not base:
port = cherrypy.request.local.port
if port == 80:
base = '127.0.0.1'
else:
base = '127.0.0.1:%s' % port
if base.find("://") == -1:
base = request.headers.get('Host', '127.0.0.1')
port = request.local.port
if port != 80:
base += ':%s' % port
if base.find('://') == -1:
# add http:// or https:// if needed
base = scheme + "://" + base
base = scheme + '://' + base
request.base = base
if remote:
xff = request.headers.get(remote)
if debug:
cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
if xff:
if remote == 'X-Forwarded-For':
# See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/
xff = xff.split(',')[-1].strip()
# Bug #1268
xff = xff.split(',')[0].strip()
request.remote.ip = xff
def ignore_headers(headers=('Range',)):
def ignore_headers(headers=('Range',), debug=False):
"""Delete request headers whose field names are included in 'headers'.
This is a useful tool for working behind certain HTTP servers;
for example, Apache duplicates the work that CP does for 'Range'
headers, and will doubly-truncate the response.
"""
request = cherrypy.request
request = cherrypy.serving.request
for name in headers:
if name in request.headers:
if debug:
cherrypy.log('Ignoring request header %r' % name,
'TOOLS.IGNORE_HEADERS')
del request.headers[name]
def response_headers(headers=None):
def response_headers(headers=None, debug=False):
"""Set headers on the response."""
if debug:
cherrypy.log('Setting response headers: %s' % repr(headers),
'TOOLS.RESPONSE_HEADERS')
for name, value in (headers or []):
cherrypy.response.headers[name] = value
cherrypy.serving.response.headers[name] = value
response_headers.failsafe = True
def referer(pattern, accept=True, accept_missing=False, error=403,
message='Forbidden Referer header.'):
message='Forbidden Referer header.', debug=False):
"""Raise HTTPError if Referer header does/does not match the given pattern.
pattern: a regular expression pattern to test against the Referer.
accept: if True, the Referer must match the pattern; if False,
pattern
A regular expression pattern to test against the Referer.
accept
If True, the Referer must match the pattern; if False,
the Referer must NOT match the pattern.
accept_missing: if True, permit requests with no Referer header.
error: the HTTP error code to return to the client on failure.
message: a string to include in the response body on failure.
accept_missing
If True, permit requests with no Referer header.
error
The HTTP error code to return to the client on failure.
message
A string to include in the response body on failure.
"""
try:
match = bool(re.match(pattern, cherrypy.request.headers['Referer']))
ref = cherrypy.serving.request.headers['Referer']
match = bool(re.match(pattern, ref))
if debug:
cherrypy.log('Referer %r matches %r' % (ref, pattern),
'TOOLS.REFERER')
if accept == match:
return
except KeyError:
if debug:
cherrypy.log('No Referer header', 'TOOLS.REFERER')
if accept_missing:
return
raise cherrypy.HTTPError(error, message)
class SessionAuth(object):
"""Assert that the user is logged in."""
session_key = "username"
session_key = 'username'
debug = False
def check_username_and_password(self, username, password):
pass
def anonymous(self):
"""Provide a temporary user name for anonymous users."""
pass
def on_login(self, username):
pass
def on_logout(self, username):
pass
def on_check(self, username):
pass
def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
return """<html><body>
def login_screen(self, from_page='..', username='', error_msg='',
**kwargs):
return (six.text_type("""<html><body>
Message: %(error_msg)s
<form method="post" action="do_login">
Login: <input type="text" name="username" value="%(username)s" size="10" /><br />
Password: <input type="password" name="password" size="10" /><br />
<input type="hidden" name="from_page" value="%(from_page)s" /><br />
Login: <input type="text" name="username" value="%(username)s" size="10" />
<br />
Password: <input type="password" name="password" size="10" />
<br />
<input type="hidden" name="from_page" value="%(from_page)s" />
<br />
<input type="submit" />
</form>
</body></html>""" % {'from_page': from_page, 'username': username,
'error_msg': error_msg}
</body></html>""") % vars()).encode('utf-8')
def do_login(self, username, password, from_page='..', **kwargs):
"""Login. May raise redirect, or return True if request handled."""
response = cherrypy.serving.response
error_msg = self.check_username_and_password(username, password)
if error_msg:
body = self.login_screen(from_page, username, error_msg)
cherrypy.response.body = body
if cherrypy.response.headers.has_key("Content-Length"):
response.body = body
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del cherrypy.response.headers["Content-Length"]
del response.headers['Content-Length']
return True
else:
cherrypy.session[self.session_key] = cherrypy.request.login = username
cherrypy.serving.request.login = username
cherrypy.session[self.session_key] = username
self.on_login(username)
raise cherrypy.HTTPRedirect(from_page or "/")
raise cherrypy.HTTPRedirect(from_page or '/')
def do_logout(self, from_page='..', **kwargs):
"""Logout. May raise redirect, or return True if request handled."""
sess = cherrypy.session
username = sess.get(self.session_key)
sess[self.session_key] = None
if username:
cherrypy.request.login = None
cherrypy.serving.request.login = None
self.on_logout(username)
raise cherrypy.HTTPRedirect(from_page)
def do_check(self):
"""Assert username. May raise redirect, or return True if request handled."""
"""Assert username. Raise redirect, or return True if request handled.
"""
sess = cherrypy.session
request = cherrypy.request
request = cherrypy.serving.request
response = cherrypy.serving.response
username = sess.get(self.session_key)
if not username:
sess[self.session_key] = username = self.anonymous()
self._debug_message('No session[username], trying anonymous')
if not username:
cherrypy.response.body = self.login_screen(cherrypy.url(qs=request.query_string))
if cherrypy.response.headers.has_key("Content-Length"):
url = cherrypy.url(qs=request.query_string)
self._debug_message(
'No username, routing to login_screen with from_page %(url)r',
locals(),
)
response.body = self.login_screen(url)
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del cherrypy.response.headers["Content-Length"]
del response.headers['Content-Length']
return True
cherrypy.request.login = username
self._debug_message('Setting request.login to %(username)r', locals())
request.login = username
self.on_check(username)
def _debug_message(self, template, context={}):
if not self.debug:
return
cherrypy.log(template % context, 'TOOLS.SESSAUTH')
def run(self):
request = cherrypy.request
request = cherrypy.serving.request
response = cherrypy.serving.response
path = request.path_info
if path.endswith('login_screen'):
return self.login_screen(**request.params)
self._debug_message('routing %(path)r to login_screen', locals())
response.body = self.login_screen()
return True
elif path.endswith('do_login'):
if request.method != 'POST':
response.headers['Allow'] = 'POST'
self._debug_message('do_login requires POST')
raise cherrypy.HTTPError(405)
self._debug_message('routing %(path)r to do_login', locals())
return self.do_login(**request.params)
elif path.endswith('do_logout'):
if request.method != 'POST':
response.headers['Allow'] = 'POST'
raise cherrypy.HTTPError(405)
self._debug_message('routing %(path)r to do_logout', locals())
return self.do_logout(**request.params)
else:
self._debug_message('No special path, running do_check')
return self.do_check()
def session_auth(**kwargs):
sa = SessionAuth()
for k, v in kwargs.iteritems():
for k, v in kwargs.items():
setattr(sa, k, v)
return sa.run()
session_auth.__doc__ = """Session authentication hook.
@@ -294,140 +414,235 @@ session_auth.__doc__ = """Session authentication hook.
Any attribute of the SessionAuth class may be overridden via a keyword arg
to this function:
""" + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__)
for k in dir(SessionAuth) if not k.startswith("__")])
""" + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__)
for k in dir(SessionAuth) if not k.startswith('__')])
def log_traceback(severity=logging.DEBUG):
def log_traceback(severity=logging.ERROR, debug=False):
"""Write the last error's traceback to the cherrypy error log."""
cherrypy.log("", "HTTP", severity=severity, traceback=True)
cherrypy.log('', 'HTTP', severity=severity, traceback=True)
def log_request_headers():
def log_request_headers(debug=False):
"""Write request headers to the cherrypy error log."""
h = [" %s: %s" % (k, v) for k, v in cherrypy.request.header_list]
cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP")
h = [' %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list]
cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP')
def log_hooks():
def log_hooks(debug=False):
"""Write request.hooks to the cherrypy error log."""
request = cherrypy.serving.request
msg = []
# Sort by the standard points if possible.
from cherrypy import _cprequest
points = _cprequest.hookpoints
for k in cherrypy.request.hooks.keys():
for k in request.hooks.keys():
if k not in points:
points.append(k)
for k in points:
msg.append(" %s:" % k)
v = cherrypy.request.hooks.get(k, [])
msg.append(' %s:' % k)
v = request.hooks.get(k, [])
v.sort()
for h in v:
msg.append(" %r" % h)
msg.append(' %r' % h)
cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
':\n' + '\n'.join(msg), "HTTP")
':\n' + '\n'.join(msg), 'HTTP')
def redirect(url='', internal=True):
def redirect(url='', internal=True, debug=False):
"""Raise InternalRedirect or HTTPRedirect to the given url."""
if debug:
cherrypy.log('Redirecting %sto: %s' %
({True: 'internal ', False: ''}[internal], url),
'TOOLS.REDIRECT')
if internal:
raise cherrypy.InternalRedirect(url)
else:
raise cherrypy.HTTPRedirect(url)
def trailing_slash(missing=True, extra=False):
def trailing_slash(missing=True, extra=False, status=None, debug=False):
"""Redirect if path_info has (missing|extra) trailing slash."""
request = cherrypy.request
request = cherrypy.serving.request
pi = request.path_info
if debug:
cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
(request.is_index, missing, extra, pi),
'TOOLS.TRAILING_SLASH')
if request.is_index is True:
if missing:
if not pi.endswith('/'):
new_url = cherrypy.url(pi + '/', request.query_string)
raise cherrypy.HTTPRedirect(new_url)
raise cherrypy.HTTPRedirect(new_url, status=status or 301)
elif request.is_index is False:
if extra:
# If pi == '/', don't redirect to ''!
if pi.endswith('/') and pi != '/':
new_url = cherrypy.url(pi[:-1], request.query_string)
raise cherrypy.HTTPRedirect(new_url)
raise cherrypy.HTTPRedirect(new_url, status=status or 301)
def flatten():
def flatten(debug=False):
"""Wrap response.body in a generator that recursively iterates over body.
This allows cherrypy.response.body to consist of 'nested generators';
that is, a set of generators that yield generators.
"""
import types
def flattener(input):
numchunks = 0
for x in input:
if not isinstance(x, types.GeneratorType):
if not is_iterator(x):
numchunks += 1
yield x
else:
for y in flattener(x):
yield y
response = cherrypy.response
numchunks += 1
yield y
if debug:
cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN')
response = cherrypy.serving.response
response.body = flattener(response.body)
def accept(media=None):
def accept(media=None, debug=False):
"""Return the client's preferred media-type (from the given Content-Types).
If 'media' is None (the default), no test will be performed.
If 'media' is provided, it should be the Content-Type value (as a string)
or values (as a list or tuple of strings) which the current request
or values (as a list or tuple of strings) which the current resource
can emit. The client's acceptable media ranges (as declared in the
Accept request header) will be matched in order to these Content-Type
values; the first such string is returned. That is, the return value
will always be one of the strings provided in the 'media' arg (or None
if 'media' is None).
If no match is found, then HTTPError 406 (Not Acceptable) is raised.
Note that most web browsers send */* as a (low-quality) acceptable
media range, which should match any Content-Type. In addition, "...if
no Accept header field is present, then it is assumed that the client
accepts all media types."
Matching types are checked in order of client preference first,
and then in the order of the given 'media' values.
Note that this function does not honor accept-params (other than "q").
"""
if not media:
return
if isinstance(media, basestring):
if isinstance(media, text_or_bytes):
media = [media]
request = cherrypy.serving.request
# Parse the Accept request header, and try to match one
# of the requested media-ranges (in order of preference).
ranges = cherrypy.request.headers.elements('Accept')
ranges = request.headers.elements('Accept')
if not ranges:
# Any media type is acceptable.
if debug:
cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT')
return media[0]
else:
# Note that 'ranges' is sorted in order of preference
for element in ranges:
if element.qvalue > 0:
if element.value == "*/*":
if element.value == '*/*':
# Matches any type or subtype
if debug:
cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
return media[0]
elif element.value.endswith("/*"):
elif element.value.endswith('/*'):
# Matches any subtype
mtype = element.value[:-1] # Keep the slash
for m in media:
if m.startswith(mtype):
if debug:
cherrypy.log('Match due to %s' % element.value,
'TOOLS.ACCEPT')
return m
else:
# Matches exact value
if element.value in media:
if debug:
cherrypy.log('Match due to %s' % element.value,
'TOOLS.ACCEPT')
return element.value
# No suitable media-range found.
ah = cherrypy.request.headers.get('Accept')
ah = request.headers.get('Accept')
if ah is None:
msg = "Your client did not send an Accept header."
msg = 'Your client did not send an Accept header.'
else:
msg = "Your client sent this Accept header: %s." % ah
msg += (" But this resource only emits these media types: %s." %
", ".join(media))
msg = 'Your client sent this Accept header: %s.' % ah
msg += (' But this resource only emits these media types: %s.' %
', '.join(media))
raise cherrypy.HTTPError(406, msg)
class MonitoredHeaderMap(_httputil.HeaderMap):
def __init__(self):
self.accessed_headers = set()
def __getitem__(self, key):
self.accessed_headers.add(key)
return _httputil.HeaderMap.__getitem__(self, key)
def __contains__(self, key):
self.accessed_headers.add(key)
return _httputil.HeaderMap.__contains__(self, key)
def get(self, key, default=None):
self.accessed_headers.add(key)
return _httputil.HeaderMap.get(self, key, default=default)
if hasattr({}, 'has_key'):
# Python 2
def has_key(self, key):
self.accessed_headers.add(key)
return _httputil.HeaderMap.has_key(self, key)
def autovary(ignore=None, debug=False):
"""Auto-populate the Vary response header based on request.header access.
"""
request = cherrypy.serving.request
req_h = request.headers
request.headers = MonitoredHeaderMap()
request.headers.update(req_h)
if ignore is None:
ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
def set_response_header():
resp_h = cherrypy.serving.response.headers
v = set([e.value for e in resp_h.elements('Vary')])
if debug:
cherrypy.log(
'Accessed headers: %s' % request.headers.accessed_headers,
'TOOLS.AUTOVARY')
v = v.union(request.headers.accessed_headers)
v = v.difference(ignore)
v = list(v)
v.sort()
resp_h['Vary'] = ', '.join(v)
request.hooks.attach('before_finalize', set_response_header, 95)
def convert_params(exception=ValueError, error=400):
"""Convert request params based on function annotations, with error handling.
exception
Exception class to catch.
status
The HTTP error code to return to the client on failure.
"""
request = cherrypy.serving.request
types = request.handler.callable.__annotations__
with cherrypy.HTTPError.handle(exception, error):
for key in set(types).intersection(request.params):
request.params[key] = types[key](request.params[key])

View File

@@ -1,189 +1,290 @@
import struct
import time
import io
import six
import cherrypy
from cherrypy._cpcompat import text_or_bytes, ntob
from cherrypy.lib import file_generator
from cherrypy.lib import is_closable_iterator
from cherrypy.lib import set_vary_header
def decode(encoding=None, default_encoding='utf-8'):
"""Decode cherrypy.request.params from str to unicode objects."""
if not encoding:
ct = cherrypy.request.headers.elements("Content-Type")
if ct:
ct = ct[0]
encoding = ct.params.get("charset", None)
if (not encoding) and ct.value.lower().startswith("text/"):
# 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
# to have a default charset value of "ISO-8859-1" when
# received via HTTP.
encoding = "ISO-8859-1"
if not encoding:
encoding = default_encoding
try:
decode_params(encoding)
except UnicodeDecodeError:
# IE and Firefox don't supply a charset when submitting form
# params with a CT of application/x-www-form-urlencoded.
# So after all our guessing, it could *still* be wrong.
# Start over with ISO-8859-1, since that seems to be preferred.
decode_params("ISO-8859-1")
"""Replace or extend the list of charsets used to decode a request entity.
def decode_params(encoding):
decoded_params = {}
for key, value in cherrypy.request.params.items():
if not hasattr(value, 'file'):
# Skip the value if it is an uploaded file
if isinstance(value, list):
# value is a list: decode each element
value = [v.decode(encoding) for v in value]
elif isinstance(value, str):
# value is a regular string: decode it
value = value.decode(encoding)
decoded_params[key] = value
# Decode all or nothing, so we can try again on error.
cherrypy.request.params = decoded_params
Either argument may be a single string or a list of strings.
encoding
If not None, restricts the set of charsets attempted while decoding
a request entity to the given set (even if a different charset is
given in the Content-Type request header).
# Encoding
default_encoding
Only in effect if the 'encoding' argument is not given.
If given, the set of charsets attempted while decoding a request
entity is *extended* with the given value(s).
def encode(encoding=None, errors='strict', text_only=True, add_charset=True):
# Guard against running twice
if getattr(cherrypy.request, "_encoding_attempted", False):
return
cherrypy.request._encoding_attempted = True
ct = cherrypy.response.headers.elements("Content-Type")
if ct:
ct = ct[0]
if (not text_only) or ct.value.lower().startswith("text/"):
# Set "charset=..." param on response Content-Type header
ct.params['charset'] = find_acceptable_charset(encoding, errors=errors)
if add_charset:
cherrypy.response.headers["Content-Type"] = str(ct)
def encode_stream(encoding, errors='strict'):
"""Encode a streaming response body.
Use a generator wrapper, and just pray it works as the stream is
being written out.
"""
def encoder(body):
for chunk in body:
if isinstance(chunk, unicode):
chunk = chunk.encode(encoding, errors)
yield chunk
cherrypy.response.body = encoder(cherrypy.response.body)
return True
body = cherrypy.request.body
if encoding is not None:
if not isinstance(encoding, list):
encoding = [encoding]
body.attempt_charsets = encoding
elif default_encoding:
if not isinstance(default_encoding, list):
default_encoding = [default_encoding]
body.attempt_charsets = body.attempt_charsets + default_encoding
def encode_string(encoding, errors='strict'):
"""Encode a buffered response body."""
try:
body = []
for chunk in cherrypy.response.body:
if isinstance(chunk, unicode):
chunk = chunk.encode(encoding, errors)
body.append(chunk)
cherrypy.response.body = body
except (LookupError, UnicodeError):
return False
else:
class UTF8StreamEncoder:
def __init__(self, iterator):
self._iterator = iterator
def __iter__(self):
return self
def next(self):
return self.__next__()
def __next__(self):
res = next(self._iterator)
if isinstance(res, six.text_type):
res = res.encode('utf-8')
return res
def close(self):
if is_closable_iterator(self._iterator):
self._iterator.close()
def __getattr__(self, attr):
if attr.startswith('__'):
raise AttributeError(self, attr)
return getattr(self._iterator, attr)
class ResponseEncoder:
default_encoding = 'utf-8'
failmsg = 'Response body could not be encoded with %r.'
encoding = None
errors = 'strict'
text_only = True
add_charset = True
debug = False
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
self.attempted_charsets = set()
request = cherrypy.serving.request
if request.handler is not None:
# Replace request.handler with self
if self.debug:
cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
self.oldhandler = request.handler
request.handler = self
def encode_stream(self, encoding):
"""Encode a streaming response body.
Use a generator wrapper, and just pray it works as the stream is
being written out.
"""
if encoding in self.attempted_charsets:
return False
self.attempted_charsets.add(encoding)
def encoder(body):
for chunk in body:
if isinstance(chunk, six.text_type):
chunk = chunk.encode(encoding, self.errors)
yield chunk
self.body = encoder(self.body)
return True
def find_acceptable_charset(encoding=None, default_encoding='utf-8', errors='strict'):
response = cherrypy.response
if cherrypy.response.stream:
encoder = encode_stream
else:
response.collapse_body()
encoder = encode_string
if response.headers.has_key("Content-Length"):
# Delete Content-Length header so finalize() recalcs it.
# Encoded strings may be of different lengths from their
# unicode equivalents, and even from each other. For example:
# >>> t = u"\u7007\u3040"
# >>> len(t)
# 2
# >>> len(t.encode("UTF-8"))
# 6
# >>> len(t.encode("utf7"))
# 8
del response.headers["Content-Length"]
# Parse the Accept-Charset request header, and try to provide one
# of the requested charsets (in order of user preference).
encs = cherrypy.request.headers.elements('Accept-Charset')
charsets = [enc.value.lower() for enc in encs]
attempted_charsets = []
if encoding is not None:
# If specified, force this encoding to be used, or fail.
encoding = encoding.lower()
if (not charsets) or "*" in charsets or encoding in charsets:
if encoder(encoding, errors):
return encoding
else:
if not encs:
# Any character-set is acceptable.
if encoder(default_encoding, errors):
return default_encoding
else:
raise cherrypy.HTTPError(500, failmsg % default_encoding)
else:
if "*" not in charsets:
# If no "*" is present in an Accept-Charset field, then all
# character sets not explicitly mentioned get a quality
# value of 0, except for ISO-8859-1, which gets a quality
# value of 1 if not explicitly mentioned.
iso = 'iso-8859-1'
if iso not in charsets:
attempted_charsets.append(iso)
if encoder(iso, errors):
return iso
for element in encs:
if element.qvalue > 0:
if element.value == "*":
# Matches any charset. Try our default.
if default_encoding not in attempted_charsets:
attempted_charsets.append(default_encoding)
if encoder(default_encoding, errors):
return default_encoding
else:
encoding = element.value
if encoding not in attempted_charsets:
attempted_charsets.append(encoding)
if encoder(encoding, errors):
return encoding
# No suitable encoding found.
ac = cherrypy.request.headers.get('Accept-Charset')
if ac is None:
msg = "Your client did not send an Accept-Charset header."
else:
msg = "Your client sent this Accept-Charset header: %s." % ac
msg += " We tried these charsets: %s." % ", ".join(attempted_charsets)
raise cherrypy.HTTPError(406, msg)
def encode_string(self, encoding):
"""Encode a buffered response body."""
if encoding in self.attempted_charsets:
return False
self.attempted_charsets.add(encoding)
body = []
for chunk in self.body:
if isinstance(chunk, six.text_type):
try:
chunk = chunk.encode(encoding, self.errors)
except (LookupError, UnicodeError):
return False
body.append(chunk)
self.body = body
return True
def find_acceptable_charset(self):
request = cherrypy.serving.request
response = cherrypy.serving.response
if self.debug:
cherrypy.log('response.stream %r' %
response.stream, 'TOOLS.ENCODE')
if response.stream:
encoder = self.encode_stream
else:
encoder = self.encode_string
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
# Encoded strings may be of different lengths from their
# unicode equivalents, and even from each other. For example:
# >>> t = u"\u7007\u3040"
# >>> len(t)
# 2
# >>> len(t.encode("UTF-8"))
# 6
# >>> len(t.encode("utf7"))
# 8
del response.headers['Content-Length']
# Parse the Accept-Charset request header, and try to provide one
# of the requested charsets (in order of user preference).
encs = request.headers.elements('Accept-Charset')
charsets = [enc.value.lower() for enc in encs]
if self.debug:
cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
if self.encoding is not None:
# If specified, force this encoding to be used, or fail.
encoding = self.encoding.lower()
if self.debug:
cherrypy.log('Specified encoding %r' %
encoding, 'TOOLS.ENCODE')
if (not charsets) or '*' in charsets or encoding in charsets:
if self.debug:
cherrypy.log('Attempting encoding %r' %
encoding, 'TOOLS.ENCODE')
if encoder(encoding):
return encoding
else:
if not encs:
if self.debug:
cherrypy.log('Attempting default encoding %r' %
self.default_encoding, 'TOOLS.ENCODE')
# Any character-set is acceptable.
if encoder(self.default_encoding):
return self.default_encoding
else:
raise cherrypy.HTTPError(500, self.failmsg %
self.default_encoding)
else:
for element in encs:
if element.qvalue > 0:
if element.value == '*':
# Matches any charset. Try our default.
if self.debug:
cherrypy.log('Attempting default encoding due '
'to %r' % element, 'TOOLS.ENCODE')
if encoder(self.default_encoding):
return self.default_encoding
else:
encoding = element.value
if self.debug:
cherrypy.log('Attempting encoding %s (qvalue >'
'0)' % element, 'TOOLS.ENCODE')
if encoder(encoding):
return encoding
if '*' not in charsets:
# If no "*" is present in an Accept-Charset field, then all
# character sets not explicitly mentioned get a quality
# value of 0, except for ISO-8859-1, which gets a quality
# value of 1 if not explicitly mentioned.
iso = 'iso-8859-1'
if iso not in charsets:
if self.debug:
cherrypy.log('Attempting ISO-8859-1 encoding',
'TOOLS.ENCODE')
if encoder(iso):
return iso
# No suitable encoding found.
ac = request.headers.get('Accept-Charset')
if ac is None:
msg = 'Your client did not send an Accept-Charset header.'
else:
msg = 'Your client sent this Accept-Charset header: %s.' % ac
_charsets = ', '.join(sorted(self.attempted_charsets))
msg += ' We tried these charsets: %s.' % (_charsets,)
raise cherrypy.HTTPError(406, msg)
def __call__(self, *args, **kwargs):
response = cherrypy.serving.response
self.body = self.oldhandler(*args, **kwargs)
if isinstance(self.body, text_or_bytes):
# strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character
# in a long string.
if self.body:
self.body = [self.body]
else:
# [''] doesn't evaluate to False, so replace it with [].
self.body = []
elif hasattr(self.body, 'read'):
self.body = file_generator(self.body)
elif self.body is None:
self.body = []
ct = response.headers.elements('Content-Type')
if self.debug:
cherrypy.log('Content-Type: %r' % [str(h)
for h in ct], 'TOOLS.ENCODE')
if ct and self.add_charset:
ct = ct[0]
if self.text_only:
if ct.value.lower().startswith('text/'):
if self.debug:
cherrypy.log(
'Content-Type %s starts with "text/"' % ct,
'TOOLS.ENCODE')
do_find = True
else:
if self.debug:
cherrypy.log('Not finding because Content-Type %s '
'does not start with "text/"' % ct,
'TOOLS.ENCODE')
do_find = False
else:
if self.debug:
cherrypy.log('Finding because not text_only',
'TOOLS.ENCODE')
do_find = True
if do_find:
# Set "charset=..." param on response Content-Type header
ct.params['charset'] = self.find_acceptable_charset()
if self.debug:
cherrypy.log('Setting Content-Type %s' % ct,
'TOOLS.ENCODE')
response.headers['Content-Type'] = str(ct)
return self.body
# GZIP
def compress(body, compress_level):
"""Compress 'body' at the given compress_level."""
import zlib
yield '\037\213' # magic header
yield '\010' # compression method
yield '\0'
yield struct.pack("<L", long(time.time()))
yield '\002'
yield '\377'
crc = zlib.crc32("")
# See http://www.gzip.org/zlib/rfc-gzip.html
yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker
yield ntob('\x08') # CM: compression method
yield ntob('\x00') # FLG: none set
# MTIME: 4 bytes
yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16))
yield ntob('\x02') # XFL: max compression, slowest algo
yield ntob('\xff') # OS: unknown
crc = zlib.crc32(ntob(''))
size = 0
zobj = zlib.compressobj(compress_level,
zlib.DEFLATED, -zlib.MAX_WBITS,
@@ -193,13 +294,17 @@ def compress(body, compress_level):
crc = zlib.crc32(line, crc)
yield zobj.compress(line)
yield zobj.flush()
yield struct.pack("<l", crc)
yield struct.pack("<L", size & 0xFFFFFFFFL)
# CRC32: 4 bytes
yield struct.pack('<L', crc & int('FFFFFFFF', 16))
# ISIZE: 4 bytes
yield struct.pack('<L', size & int('FFFFFFFF', 16))
def decompress(body):
import gzip, StringIO
zbuf = StringIO.StringIO()
import gzip
zbuf = io.BytesIO()
zbuf.write(body)
zbuf.seek(0)
zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
@@ -208,29 +313,44 @@ def decompress(body):
return data
def gzip(compress_level=9, mime_types=['text/html', 'text/plain']):
def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
debug=False):
"""Try to gzip the response body if Content-Type in mime_types.
cherrypy.response.headers['Content-Type'] must be set to one of the
values in the mime_types arg before calling this function.
The provided list of mime-types must be of one of the following form:
* type/subtype
* type/*
* type/*+subtype
No compression is performed if any of the following hold:
* The client sends no Accept-Encoding request header
* No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
* No 'gzip' or 'x-gzip' with a qvalue > 0 is present
* The 'identity' value is given with a qvalue > 0.
"""
response = cherrypy.response
request = cherrypy.serving.request
response = cherrypy.serving.response
set_vary_header(response, 'Accept-Encoding')
if not response.body:
# Response body is empty (might be a 304 for instance)
if debug:
cherrypy.log('No response body', context='TOOLS.GZIP')
return
# If returning cached content (which should already have been gzipped),
# don't re-zip.
if getattr(cherrypy.request, "cached", False):
if getattr(request, 'cached', False):
if debug:
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
return
acceptable = cherrypy.request.headers.elements('Accept-Encoding')
acceptable = request.headers.elements('Accept-Encoding')
if not acceptable:
# If no Accept-Encoding field is present in a request,
# the server MAY assume that the client will accept any
@@ -239,27 +359,66 @@ def gzip(compress_level=9, mime_types=['text/html', 'text/plain']):
# the "identity" content-coding, unless it has additional
# information that a different content-coding is meaningful
# to the client.
if debug:
cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
return
ct = response.headers.get('Content-Type', '').split(';')[0]
for coding in acceptable:
if coding.value == 'identity' and coding.qvalue != 0:
if debug:
cherrypy.log('Non-zero identity qvalue: %s' % coding,
context='TOOLS.GZIP')
return
if coding.value in ('gzip', 'x-gzip'):
if coding.qvalue == 0:
if debug:
cherrypy.log('Zero gzip qvalue: %s' % coding,
context='TOOLS.GZIP')
return
if ct in mime_types:
# Return a generator that compresses the page
varies = response.headers.get("Vary", "")
varies = [x.strip() for x in varies.split(",") if x.strip()]
if "Accept-Encoding" not in varies:
varies.append("Accept-Encoding")
response.headers['Vary'] = ", ".join(varies)
response.headers['Content-Encoding'] = 'gzip'
response.body = compress(response.body, compress_level)
if response.headers.has_key("Content-Length"):
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
if ct not in mime_types:
# If the list of provided mime-types contains tokens
# such as 'text/*' or 'application/*+xml',
# we go through them and find the most appropriate one
# based on the given content-type.
# The pattern matching is only caring about the most
# common cases, as stated above, and doesn't support
# for extra parameters.
found = False
if '/' in ct:
ct_media_type, ct_sub_type = ct.split('/')
for mime_type in mime_types:
if '/' in mime_type:
media_type, sub_type = mime_type.split('/')
if ct_media_type == media_type:
if sub_type == '*':
found = True
break
elif '+' in sub_type and '+' in ct_sub_type:
ct_left, ct_right = ct_sub_type.split('+')
left, right = sub_type.split('+')
if left == '*' and ct_right == right:
found = True
break
if not found:
if debug:
cherrypy.log('Content-Type %s not in mime_types %r' %
(ct, mime_types), context='TOOLS.GZIP')
return
if debug:
cherrypy.log('Gzipping', context='TOOLS.GZIP')
# Return a generator that compresses the page
response.headers['Content-Encoding'] = 'gzip'
response.body = compress(response.body, compress_level)
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers['Content-Length']
return
cherrypy.HTTPError(406, "identity, gzip").set_response()
if debug:
cherrypy.log('No acceptable encoding found.', context='GZIP')
cherrypy.HTTPError(406, 'identity, gzip').set_response()

216
cherrypy/lib/gctools.py Normal file
View File

@@ -0,0 +1,216 @@
import gc
import inspect
import sys
import time
try:
import objgraph
except ImportError:
objgraph = None
import cherrypy
from cherrypy import _cprequest, _cpwsgi
from cherrypy.process.plugins import SimplePlugin
class ReferrerTree(object):
"""An object which gathers all referrers of an object to a given depth."""
peek_length = 40
def __init__(self, ignore=None, maxdepth=2, maxparents=10):
self.ignore = ignore or []
self.ignore.append(inspect.currentframe().f_back)
self.maxdepth = maxdepth
self.maxparents = maxparents
def ascend(self, obj, depth=1):
"""Return a nested list containing referrers of the given object."""
depth += 1
parents = []
# Gather all referrers in one step to minimize
# cascading references due to repr() logic.
refs = gc.get_referrers(obj)
self.ignore.append(refs)
if len(refs) > self.maxparents:
return [('[%s referrers]' % len(refs), [])]
try:
ascendcode = self.ascend.__code__
except AttributeError:
ascendcode = self.ascend.im_func.func_code
for parent in refs:
if inspect.isframe(parent) and parent.f_code is ascendcode:
continue
if parent in self.ignore:
continue
if depth <= self.maxdepth:
parents.append((parent, self.ascend(parent, depth)))
else:
parents.append((parent, []))
return parents
def peek(self, s):
"""Return s, restricted to a sane length."""
if len(s) > (self.peek_length + 3):
half = self.peek_length // 2
return s[:half] + '...' + s[-half:]
else:
return s
def _format(self, obj, descend=True):
"""Return a string representation of a single object."""
if inspect.isframe(obj):
filename, lineno, func, context, index = inspect.getframeinfo(obj)
return "<frame of function '%s'>" % func
if not descend:
return self.peek(repr(obj))
if isinstance(obj, dict):
return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False),
self._format(v, descend=False))
for k, v in obj.items()]) + '}'
elif isinstance(obj, list):
return '[' + ', '.join([self._format(item, descend=False)
for item in obj]) + ']'
elif isinstance(obj, tuple):
return '(' + ', '.join([self._format(item, descend=False)
for item in obj]) + ')'
r = self.peek(repr(obj))
if isinstance(obj, (str, int, float)):
return r
return '%s: %s' % (type(obj), r)
def format(self, tree):
"""Return a list of string reprs from a nested list of referrers."""
output = []
def ascend(branch, depth=1):
for parent, grandparents in branch:
output.append((' ' * depth) + self._format(parent))
if grandparents:
ascend(grandparents, depth + 1)
ascend(tree)
return output
def get_instances(cls):
return [x for x in gc.get_objects() if isinstance(x, cls)]
class RequestCounter(SimplePlugin):
def start(self):
self.count = 0
def before_request(self):
self.count += 1
def after_request(self):
self.count -= 1
request_counter = RequestCounter(cherrypy.engine)
request_counter.subscribe()
def get_context(obj):
if isinstance(obj, _cprequest.Request):
return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
elif isinstance(obj, _cprequest.Response):
return 'status=%s' % obj.status
elif isinstance(obj, _cpwsgi.AppResponse):
return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '')
elif hasattr(obj, 'tb_lineno'):
return 'tb_lineno=%s' % obj.tb_lineno
return ''
class GCRoot(object):
"""A CherryPy page handler for testing reference leaks."""
classes = [
(_cprequest.Request, 2, 2,
'Should be 1 in this request thread and 1 in the main thread.'),
(_cprequest.Response, 2, 2,
'Should be 1 in this request thread and 1 in the main thread.'),
(_cpwsgi.AppResponse, 1, 1,
'Should be 1 in this request thread only.'),
]
@cherrypy.expose
def index(self):
return 'Hello, world!'
@cherrypy.expose
def stats(self):
output = ['Statistics:']
for trial in range(10):
if request_counter.count > 0:
break
time.sleep(0.5)
else:
output.append('\nNot all requests closed properly.')
# gc_collect isn't perfectly synchronous, because it may
# break reference cycles that then take time to fully
# finalize. Call it thrice and hope for the best.
gc.collect()
gc.collect()
unreachable = gc.collect()
if unreachable:
if objgraph is not None:
final = objgraph.by_type('Nondestructible')
if final:
objgraph.show_backrefs(final, filename='finalizers.png')
trash = {}
for x in gc.garbage:
trash[type(x)] = trash.get(type(x), 0) + 1
if trash:
output.insert(0, '\n%s unreachable objects:' % unreachable)
trash = [(v, k) for k, v in trash.items()]
trash.sort()
for pair in trash:
output.append(' ' + repr(pair))
# Check declared classes to verify uncollected instances.
# These don't have to be part of a cycle; they can be
# any objects that have unanticipated referrers that keep
# them from being collected.
allobjs = {}
for cls, minobj, maxobj, msg in self.classes:
allobjs[cls] = get_instances(cls)
for cls, minobj, maxobj, msg in self.classes:
objs = allobjs[cls]
lenobj = len(objs)
if lenobj < minobj or lenobj > maxobj:
if minobj == maxobj:
output.append(
'\nExpected %s %r references, got %s.' %
(minobj, cls, lenobj))
else:
output.append(
'\nExpected %s to %s %r references, got %s.' %
(minobj, maxobj, cls, lenobj))
for obj in objs:
if objgraph is not None:
ig = [id(objs), id(inspect.currentframe())]
fname = 'graph_%s_%s.png' % (cls.__name__, id(obj))
objgraph.show_backrefs(
obj, extra_ignore=ig, max_depth=4, too_many=20,
filename=fname, extra_info=get_context)
output.append('\nReferrers for %s (refcount=%s):' %
(repr(obj), sys.getrefcount(obj)))
t = ReferrerTree(ignore=[objs], maxdepth=3)
tree = t.ascend(obj)
output.extend(t.format(tree))
return '\n'.join(output)

View File

@@ -1,410 +0,0 @@
"""HTTP library functions."""
# This module contains functions for building an HTTP application
# framework: any one, not just one whose name starts with "Ch". ;) If you
# reference any modules from some popular framework inside *this* module,
# FuManChu will personally hang you up by your thumbs and submit you
# to a public caning.
from BaseHTTPServer import BaseHTTPRequestHandler
response_codes = BaseHTTPRequestHandler.responses.copy()
# From http://www.cherrypy.org/ticket/361
response_codes[500] = ('Internal Server Error',
'The server encountered an unexpected condition '
'which prevented it from fulfilling the request.')
response_codes[503] = ('Service Unavailable',
'The server is currently unable to handle the '
'request due to a temporary overloading or '
'maintenance of the server.')
import cgi
import re
from rfc822 import formatdate as HTTPDate
def urljoin(*atoms):
"""Return the given path *atoms, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank.
"""
url = "/".join([x for x in atoms if x])
while "//" in url:
url = url.replace("//", "/")
# Special-case the final url of "", and return "/" instead.
return url or "/"
def protocol_from_http(protocol_str):
"""Return a protocol tuple from the given 'HTTP/x.y' string."""
return int(protocol_str[5]), int(protocol_str[7])
def get_ranges(headervalue, content_length):
"""Return a list of (start, stop) indices from a Range header, or None.
Each (start, stop) tuple will be composed of two ints, which are suitable
for use in a slicing operation. That is, the header "Range: bytes=3-6",
if applied against a Python string, is requesting resource[3:7]. This
function will return the list [(3, 7)].
If this function returns an empty list, you should return HTTP 416.
"""
if not headervalue:
return None
result = []
bytesunit, byteranges = headervalue.split("=", 1)
for brange in byteranges.split(","):
start, stop = [x.strip() for x in brange.split("-", 1)]
if start:
if not stop:
stop = content_length - 1
start, stop = map(int, (start, stop))
if start >= content_length:
# From rfc 2616 sec 14.16:
# "If the server receives a request (other than one
# including an If-Range request-header field) with an
# unsatisfiable Range request-header field (that is,
# all of whose byte-range-spec values have a first-byte-pos
# value greater than the current length of the selected
# resource), it SHOULD return a response code of 416
# (Requested range not satisfiable)."
continue
if stop < start:
# From rfc 2616 sec 14.16:
# "If the server ignores a byte-range-spec because it
# is syntactically invalid, the server SHOULD treat
# the request as if the invalid Range header field
# did not exist. (Normally, this means return a 200
# response containing the full entity)."
return None
result.append((start, stop + 1))
else:
if not stop:
# See rfc quote above.
return None
# Negative subscript (last N bytes)
result.append((content_length - int(stop), content_length))
return result
class HeaderElement(object):
"""An element (with parameters) from an HTTP header's element list."""
def __init__(self, value, params=None):
self.value = value
if params is None:
params = {}
self.params = params
def __unicode__(self):
p = [";%s=%s" % (k, v) for k, v in self.params.iteritems()]
return u"%s%s" % (self.value, "".join(p))
def __str__(self):
return str(self.__unicode__())
def parse(elementstr):
"""Transform 'token;key=val' to ('token', {'key': 'val'})."""
# Split the element into a value and parameters. The 'value' may
# be of the form, "token=token", but we don't split that here.
atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
initial_value = atoms.pop(0).strip()
params = {}
for atom in atoms:
atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
key = atom.pop(0)
if atom:
val = atom[0]
else:
val = ""
params[key] = val
return initial_value, params
parse = staticmethod(parse)
def from_str(cls, elementstr):
"""Construct an instance from a string of the form 'token;key=val'."""
ival, params = cls.parse(elementstr)
return cls(ival, params)
from_str = classmethod(from_str)
q_separator = re.compile(r'; *q *=')
class AcceptElement(HeaderElement):
"""An element (with parameters) from an Accept* header's element list.
AcceptElement objects are comparable; the more-preferred object will be
"less than" the less-preferred object. They are also therefore sortable;
if you sort a list of AcceptElement objects, they will be listed in
priority order; the most preferred value will be first. Yes, it should
have been the other way around, but it's too late to fix now.
"""
def from_str(cls, elementstr):
qvalue = None
# The first "q" parameter (if any) separates the initial
# media-range parameter(s) (if any) from the accept-params.
atoms = q_separator.split(elementstr, 1)
media_range = atoms.pop(0).strip()
if atoms:
# The qvalue for an Accept header can have extensions. The other
# headers cannot, but it's easier to parse them as if they did.
qvalue = HeaderElement.from_str(atoms[0].strip())
media_type, params = cls.parse(media_range)
if qvalue is not None:
params["q"] = qvalue
return cls(media_type, params)
from_str = classmethod(from_str)
def qvalue(self):
val = self.params.get("q", "1")
if isinstance(val, HeaderElement):
val = val.value
return float(val)
qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
def __cmp__(self, other):
diff = cmp(other.qvalue, self.qvalue)
if diff == 0:
diff = cmp(str(other), str(self))
return diff
def header_elements(fieldname, fieldvalue):
"""Return a HeaderElement list from a comma-separated header str."""
if not fieldvalue:
return None
headername = fieldname.lower()
result = []
for element in fieldvalue.split(","):
if headername.startswith("accept") or headername == 'te':
hv = AcceptElement.from_str(element)
else:
hv = HeaderElement.from_str(element)
result.append(hv)
result.sort()
return result
def decode_TEXT(value):
"""Decode RFC-2047 TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> u"f\xfcr")."""
from email.Header import decode_header
atoms = decode_header(value)
decodedvalue = ""
for atom, charset in atoms:
if charset is not None:
atom = atom.decode(charset)
decodedvalue += atom
return decodedvalue
def valid_status(status):
"""Return legal HTTP status Code, Reason-phrase and Message.
The status arg must be an int, or a str that begins with an int.
If status is an int, or a str and no reason-phrase is supplied,
a default reason-phrase will be provided.
"""
if not status:
status = 200
status = str(status)
parts = status.split(" ", 1)
if len(parts) == 1:
# No reason supplied.
code, = parts
reason = None
else:
code, reason = parts
reason = reason.strip()
try:
code = int(code)
except ValueError:
raise ValueError("Illegal response status from server "
"(%s is non-numeric)." % repr(code))
if code < 100 or code > 599:
raise ValueError("Illegal response status from server "
"(%s is out of range)." % repr(code))
if code not in response_codes:
# code is unknown but not illegal
default_reason, message = "", ""
else:
default_reason, message = response_codes[code]
if reason is None:
reason = default_reason
return code, reason, message
image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
def parse_query_string(query_string, keep_blank_values=True):
"""Build a params dictionary from a query_string.
Duplicate key/value pairs in the provided query_string will be
returned as {'key': [val1, val2, ...]}. Single key/values will
be returned as strings: {'key': 'value'}.
"""
if image_map_pattern.match(query_string):
# Server-side image map. Map the coords to 'x' and 'y'
# (like CGI::Request does).
pm = query_string.split(",")
pm = {'x': int(pm[0]), 'y': int(pm[1])}
else:
pm = cgi.parse_qs(query_string, keep_blank_values)
for key, val in pm.items():
if len(val) == 1:
pm[key] = val[0]
return pm
def params_from_CGI_form(form):
params = {}
for key in form.keys():
value_list = form[key]
if key is None:
# multipart/* message parts that have no Content-Disposition
# have a .name of None, but Python kwarg keys must be strings.
# See http://www.cherrypy.org/ticket/890.
key = 'parts'
if isinstance(value_list, list):
params[key] = []
for item in value_list:
if item.filename is not None:
value = item # It's a file upload
else:
value = item.value # It's a regular field
params[key].append(value)
else:
if value_list.filename is not None:
value = value_list # It's a file upload
else:
value = value_list.value # It's a regular field
params[key] = value
return params
class CaseInsensitiveDict(dict):
"""A case-insensitive dict subclass.
Each key is changed on entry to str(key).title().
"""
def __getitem__(self, key):
return dict.__getitem__(self, str(key).title())
def __setitem__(self, key, value):
dict.__setitem__(self, str(key).title(), value)
def __delitem__(self, key):
dict.__delitem__(self, str(key).title())
def __contains__(self, key):
return dict.__contains__(self, str(key).title())
def get(self, key, default=None):
return dict.get(self, str(key).title(), default)
def has_key(self, key):
return dict.has_key(self, str(key).title())
def update(self, E):
for k in E.keys():
self[str(k).title()] = E[k]
def fromkeys(cls, seq, value=None):
newdict = cls()
for k in seq:
newdict[str(k).title()] = value
return newdict
fromkeys = classmethod(fromkeys)
def setdefault(self, key, x=None):
key = str(key).title()
try:
return self[key]
except KeyError:
self[key] = x
return x
def pop(self, key, default):
return dict.pop(self, str(key).title(), default)
class HeaderMap(CaseInsensitiveDict):
"""A dict subclass for HTTP request and response headers.
Each key is changed on entry to str(key).title(). This allows headers
to be case-insensitive and avoid duplicates.
Values are header values (decoded according to RFC 2047 if necessary).
"""
def elements(self, key):
"""Return a list of HeaderElements for the given header (or None)."""
key = str(key).title()
h = self.get(key)
if h is None:
return []
return header_elements(key, h)
def output(self, protocol=(1, 1)):
"""Transform self into a list of (name, value) tuples."""
header_list = []
for key, v in self.iteritems():
if isinstance(v, unicode):
# HTTP/1.0 says, "Words of *TEXT may contain octets
# from character sets other than US-ASCII." and
# "Recipients of header field TEXT containing octets
# outside the US-ASCII character set may assume that
# they represent ISO-8859-1 characters."
try:
v = v.encode("iso-8859-1")
except UnicodeEncodeError:
if protocol >= (1, 1):
# Encode RFC-2047 TEXT
# (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
from email.Header import Header
v = Header(v, 'utf-8').encode()
else:
raise
else:
# This coercion should not take any time at all
# if value is already of type "str".
v = str(v)
header_list.append((key, v))
return header_list
class Host(object):
"""An internet address.
name should be the client's host name. If not available (because no DNS
lookup is performed), the IP address should be used instead.
"""
ip = "0.0.0.0"
port = 80
name = "unknown.tld"
def __init__(self, ip, port, name=None):
self.ip = ip
self.port = port
if name is None:
name = ip
self.name = name
def __repr__(self):
return "http.Host(%r, %r, %r)" % (self.ip, self.port, self.name)

View File

@@ -1,28 +1,40 @@
"""
httpauth modules defines functions to implement HTTP Digest Authentication (RFC 2617).
This module defines functions to implement HTTP Digest Authentication
(:rfc:`2617`).
This has full compliance with 'Digest' and 'Basic' authentication methods. In
'Digest' it supports both MD5 and MD5-sess algorithms.
Usage:
First use 'doAuth' to request the client authentication for a
certain resource. You should send an httplib.UNAUTHORIZED response to the
client so he knows he has to authenticate itself.
Then use 'parseAuthorization' to retrieve the 'auth_map' used in
'checkResponse'.
To use 'checkResponse' you must have already verified the password associated
with the 'username' key in 'auth_map' dict. Then you use the 'checkResponse'
function to verify if the password matches the one sent by the client.
To use 'checkResponse' you must have already verified the password
associated with the 'username' key in 'auth_map' dict. Then you use the
'checkResponse' function to verify if the password matches the one sent
by the client.
SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
SUPPORTED_QOP - list of supported 'Digest' 'qop'.
"""
import time
from hashlib import md5
from cherrypy._cpcompat import (
base64_decode, ntob,
parse_http_list, parse_keqv_list
)
__version__ = 1, 0, 1
__author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>"
__author__ = 'Tiago Cogumbreiro <cogumbreiro@users.sf.net>'
__credits__ = """
Peter van Kampen for its recipe which implement most of Digest authentication:
Peter van Kampen for its recipe which implement most of Digest
authentication:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
"""
@@ -30,62 +42,55 @@ __license__ = """
Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
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,
* 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
* 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 Sylvain Hellegouarch nor the names of his contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
* Neither the name of Sylvain Hellegouarch nor the names of his
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
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.
"""
__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
"parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey",
"calculateNonce", "SUPPORTED_QOP")
__all__ = ('digestAuth', 'basicAuth', 'doAuth', 'checkResponse',
'parseAuthorization', 'SUPPORTED_ALGORITHM', 'md5SessionKey',
'calculateNonce', 'SUPPORTED_QOP')
################################################################################
try:
# Python 2.5+
from hashlib import md5
except ImportError:
from md5 import new as md5
import time
import base64
import urllib2
##########################################################################
MD5 = "MD5"
MD5_SESS = "MD5-sess"
AUTH = "auth"
AUTH_INT = "auth-int"
MD5 = 'MD5'
MD5_SESS = 'MD5-sess'
AUTH = 'auth'
AUTH_INT = 'auth-int'
SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
SUPPORTED_QOP = (AUTH, AUTH_INT)
################################################################################
##########################################################################
# doAuth
#
DIGEST_AUTH_ENCODERS = {
MD5: lambda val: md5(val).hexdigest(),
MD5_SESS: lambda val: md5(val).hexdigest(),
# SHA: lambda val: sha.new (val).hexdigest (),
MD5: lambda val: md5(ntob(val)).hexdigest(),
MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
# SHA: lambda val: sha.new(ntob(val)).hexdigest (),
}
def calculateNonce (realm, algorithm = MD5):
def calculateNonce(realm, algorithm=MD5):
"""This is an auxaliary function that calculates 'nonce' value. It is used
to handle sessions."""
@@ -95,105 +100,109 @@ def calculateNonce (realm, algorithm = MD5):
try:
encoder = DIGEST_AUTH_ENCODERS[algorithm]
except KeyError:
raise NotImplementedError ("The chosen algorithm (%s) does not have "\
"an implementation yet" % algorithm)
raise NotImplementedError('The chosen algorithm (%s) does not have '
'an implementation yet' % algorithm)
return encoder ("%d:%s" % (time.time(), realm))
return encoder('%d:%s' % (time.time(), realm))
def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH):
def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH):
"""Challenges the client for a Digest authentication."""
global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP
assert algorithm in SUPPORTED_ALGORITHM
assert qop in SUPPORTED_QOP
if nonce is None:
nonce = calculateNonce (realm, algorithm)
nonce = calculateNonce(realm, algorithm)
return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
realm, nonce, algorithm, qop
)
def basicAuth (realm):
def basicAuth(realm):
"""Challengenes the client for a Basic authentication."""
assert '"' not in realm, "Realms cannot contain the \" (quote) character."
return 'Basic realm="%s"' % realm
def doAuth (realm):
def doAuth(realm):
"""'doAuth' function returns the challenge string b giving priority over
Digest and fallback to Basic authentication when the browser doesn't
support the first one.
This should be set in the HTTP header under the key 'WWW-Authenticate'."""
return digestAuth (realm) + " " + basicAuth (realm)
return digestAuth(realm) + ' ' + basicAuth(realm)
################################################################################
##########################################################################
# Parse authorization parameters
#
def _parseDigestAuthorization (auth_params):
def _parseDigestAuthorization(auth_params):
# Convert the auth params to a dict
items = urllib2.parse_http_list (auth_params)
params = urllib2.parse_keqv_list (items)
items = parse_http_list(auth_params)
params = parse_keqv_list(items)
# Now validate the params
# Check for required parameters
required = ["username", "realm", "nonce", "uri", "response"]
required = ['username', 'realm', 'nonce', 'uri', 'response']
for k in required:
if not params.has_key(k):
if k not in params:
return None
# If qop is sent then cnonce and nc MUST be present
if params.has_key("qop") and not (params.has_key("cnonce") \
and params.has_key("nc")):
if 'qop' in params and not ('cnonce' in params
and 'nc' in params):
return None
# If qop is not sent, neither cnonce nor nc can be present
if (params.has_key("cnonce") or params.has_key("nc")) and \
not params.has_key("qop"):
if ('cnonce' in params or 'nc' in params) and \
'qop' not in params:
return None
return params
def _parseBasicAuthorization (auth_params):
username, password = base64.decodestring (auth_params).split (":", 1)
return {"username": username, "password": password}
def _parseBasicAuthorization(auth_params):
username, password = base64_decode(auth_params).split(':', 1)
return {'username': username, 'password': password}
AUTH_SCHEMES = {
"basic": _parseBasicAuthorization,
"digest": _parseDigestAuthorization,
'basic': _parseBasicAuthorization,
'digest': _parseDigestAuthorization,
}
def parseAuthorization (credentials):
def parseAuthorization(credentials):
"""parseAuthorization will convert the value of the 'Authorization' key in
the HTTP header to a map itself. If the parsing fails 'None' is returned.
"""
global AUTH_SCHEMES
auth_scheme, auth_params = credentials.split(" ", 1)
auth_scheme = auth_scheme.lower ()
auth_scheme, auth_params = credentials.split(' ', 1)
auth_scheme = auth_scheme.lower()
parser = AUTH_SCHEMES[auth_scheme]
params = parser (auth_params)
params = parser(auth_params)
if params is None:
return
assert "auth_scheme" not in params
params["auth_scheme"] = auth_scheme
assert 'auth_scheme' not in params
params['auth_scheme'] = auth_scheme
return params
################################################################################
##########################################################################
# Check provided response for a valid password
#
def md5SessionKey (params, password):
def md5SessionKey(params, password):
"""
If the "algorithm" directive's value is "MD5-sess", then A1
If the "algorithm" directive's value is "MD5-sess", then A1
[the session key] is calculated only once - on the first request by the
client following receipt of a WWW-Authenticate challenge from the server.
@@ -210,67 +219,70 @@ def md5SessionKey (params, password):
specification.
"""
keys = ("username", "realm", "nonce", "cnonce")
keys = ('username', 'realm', 'nonce', 'cnonce')
params_copy = {}
for key in keys:
params_copy[key] = params[key]
params_copy["algorithm"] = MD5_SESS
return _A1 (params_copy, password)
params_copy['algorithm'] = MD5_SESS
return _A1(params_copy, password)
def _A1(params, password):
algorithm = params.get ("algorithm", MD5)
algorithm = params.get('algorithm', MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
if algorithm == MD5:
# If the "algorithm" directive's value is "MD5" or is
# unspecified, then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
return "%s:%s:%s" % (params["username"], params["realm"], password)
return '%s:%s:%s' % (params['username'], params['realm'], password)
elif algorithm == MD5_SESS:
# This is A1 if qop is set
# A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
# ":" unq(nonce-value) ":" unq(cnonce-value)
h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password))
return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
h_a1 = H('%s:%s:%s' % (params['username'], params['realm'], password))
return '%s:%s:%s' % (h_a1, params['nonce'], params['cnonce'])
def _A2(params, method, kwargs):
# If the "qop" directive's value is "auth" or is unspecified, then A2 is:
# A2 = Method ":" digest-uri-value
qop = params.get ("qop", "auth")
if qop == "auth":
return method + ":" + params["uri"]
elif qop == "auth-int":
qop = params.get('qop', 'auth')
if qop == 'auth':
return method + ':' + params['uri']
elif qop == 'auth-int':
# If the "qop" value is "auth-int", then A2 is:
# A2 = Method ":" digest-uri-value ":" H(entity-body)
entity_body = kwargs.get ("entity_body", "")
H = kwargs["H"]
entity_body = kwargs.get('entity_body', '')
H = kwargs['H']
return "%s:%s:%s" % (
return '%s:%s:%s' % (
method,
params["uri"],
params['uri'],
H(entity_body)
)
else:
raise NotImplementedError ("The 'qop' method is unknown: %s" % qop)
raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs):
def _computeDigestResponse(auth_map, password, method='GET', A1=None,
**kwargs):
"""
Generates a response respecting the algorithm defined in RFC 2617
"""
params = auth_map
algorithm = params.get ("algorithm", MD5)
algorithm = params.get('algorithm', MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
KD = lambda secret, data: H(secret + ":" + data)
KD = lambda secret, data: H(secret + ':' + data)
qop = params.get ("qop", None)
qop = params.get('qop', None)
H_A2 = H(_A2(params, method, kwargs))
@@ -279,7 +291,7 @@ def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwarg
else:
H_A1 = H(_A1(params, password))
if qop in ("auth", "auth-int"):
if qop in ('auth', 'auth-int'):
# If the "qop" value is "auth" or "auth-int":
# request-digest = <"> < KD ( H(A1), unq(nonce-value)
# ":" nc-value
@@ -287,11 +299,11 @@ def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwarg
# ":" unq(qop-value)
# ":" H(A2)
# ) <">
request = "%s:%s:%s:%s:%s" % (
params["nonce"],
params["nc"],
params["cnonce"],
params["qop"],
request = '%s:%s:%s:%s:%s' % (
params['nonce'],
params['nc'],
params['cnonce'],
params['qop'],
H_A2,
)
elif qop is None:
@@ -299,11 +311,12 @@ def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwarg
# for compatibility with RFC 2069):
# request-digest =
# <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
request = "%s:%s" % (params["nonce"], H_A2)
request = '%s:%s' % (params['nonce'], H_A2)
return KD(H_A1, request)
def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs):
def _checkDigestResponse(auth_map, password, method='GET', A1=None, **kwargs):
"""This function is used to verify the response given by the client when
he tries to authenticate.
Optional arguments:
@@ -318,44 +331,48 @@ def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs
if auth_map['realm'] != kwargs.get('realm', None):
return False
response = _computeDigestResponse(auth_map, password, method, A1,**kwargs)
response = _computeDigestResponse(
auth_map, password, method, A1, **kwargs)
return response == auth_map["response"]
return response == auth_map['response']
def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs):
def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
**kwargs):
# Note that the Basic response doesn't provide the realm value so we cannot
# test it
pass_through = lambda password, username=None: password
encrypt = encrypt or pass_through
try:
return encrypt(auth_map["password"], auth_map["username"]) == password
candidate = encrypt(auth_map['password'], auth_map['username'])
except TypeError:
return encrypt(auth_map["password"]) == password
# if encrypt only takes one parameter, it's the password
candidate = encrypt(auth_map['password'])
return candidate == password
AUTH_RESPONSES = {
"basic": _checkBasicResponse,
"digest": _checkDigestResponse,
'basic': _checkBasicResponse,
'digest': _checkDigestResponse,
}
def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs):
def checkResponse(auth_map, password, method='GET', encrypt=None, **kwargs):
"""'checkResponse' compares the auth_map with the password and optionally
other arguments that each implementation might need.
If the response is of type 'Basic' then the function has the following
signature:
checkBasicResponse (auth_map, password) -> bool
signature::
checkBasicResponse(auth_map, password) -> bool
If the response is of type 'Digest' then the function has the following
signature:
checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool
signature::
checkDigestResponse(auth_map, password, method='GET', A1=None) -> bool
The 'A1' argument is only used in MD5_SESS algorithm based responses.
Check md5SessionKey() for more info.
"""
global AUTH_RESPONSES
checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
checker = AUTH_RESPONSES[auth_map['auth_scheme']]
return checker(auth_map, password, method=method, encrypt=encrypt,
**kwargs)

530
cherrypy/lib/httputil.py Normal file
View File

@@ -0,0 +1,530 @@
"""HTTP library functions.
This module contains functions for building an HTTP application
framework: any one, not just one whose name starts with "Ch". ;) If you
reference any modules from some popular framework inside *this* module,
FuManChu will personally hang you up by your thumbs and submit you
to a public caning.
"""
import functools
import email.utils
import re
from binascii import b2a_base64
from cgi import parse_header
try:
# Python 3
from email.header import decode_header
except ImportError:
from email.Header import decode_header
import six
from cherrypy._cpcompat import BaseHTTPRequestHandler, ntob, ntou
from cherrypy._cpcompat import text_or_bytes, iteritems
from cherrypy._cpcompat import reversed, sorted, unquote_qs
response_codes = BaseHTTPRequestHandler.responses.copy()
# From https://github.com/cherrypy/cherrypy/issues/361
response_codes[500] = ('Internal Server Error',
'The server encountered an unexpected condition '
'which prevented it from fulfilling the request.')
response_codes[503] = ('Service Unavailable',
'The server is currently unable to handle the '
'request due to a temporary overloading or '
'maintenance of the server.')
HTTPDate = functools.partial(email.utils.formatdate, usegmt=True)
def urljoin(*atoms):
"""Return the given path \*atoms, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank.
"""
url = '/'.join([x for x in atoms if x])
while '//' in url:
url = url.replace('//', '/')
# Special-case the final url of "", and return "/" instead.
return url or '/'
def urljoin_bytes(*atoms):
"""Return the given path *atoms, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank.
"""
url = ntob('/').join([x for x in atoms if x])
while ntob('//') in url:
url = url.replace(ntob('//'), ntob('/'))
# Special-case the final url of "", and return "/" instead.
return url or ntob('/')
def protocol_from_http(protocol_str):
"""Return a protocol tuple from the given 'HTTP/x.y' string."""
return int(protocol_str[5]), int(protocol_str[7])
def get_ranges(headervalue, content_length):
"""Return a list of (start, stop) indices from a Range header, or None.
Each (start, stop) tuple will be composed of two ints, which are suitable
for use in a slicing operation. That is, the header "Range: bytes=3-6",
if applied against a Python string, is requesting resource[3:7]. This
function will return the list [(3, 7)].
If this function returns an empty list, you should return HTTP 416.
"""
if not headervalue:
return None
result = []
bytesunit, byteranges = headervalue.split('=', 1)
for brange in byteranges.split(','):
start, stop = [x.strip() for x in brange.split('-', 1)]
if start:
if not stop:
stop = content_length - 1
start, stop = int(start), int(stop)
if start >= content_length:
# From rfc 2616 sec 14.16:
# "If the server receives a request (other than one
# including an If-Range request-header field) with an
# unsatisfiable Range request-header field (that is,
# all of whose byte-range-spec values have a first-byte-pos
# value greater than the current length of the selected
# resource), it SHOULD return a response code of 416
# (Requested range not satisfiable)."
continue
if stop < start:
# From rfc 2616 sec 14.16:
# "If the server ignores a byte-range-spec because it
# is syntactically invalid, the server SHOULD treat
# the request as if the invalid Range header field
# did not exist. (Normally, this means return a 200
# response containing the full entity)."
return None
result.append((start, stop + 1))
else:
if not stop:
# See rfc quote above.
return None
# Negative subscript (last N bytes)
#
# RFC 2616 Section 14.35.1:
# If the entity is shorter than the specified suffix-length,
# the entire entity-body is used.
if int(stop) > content_length:
result.append((0, content_length))
else:
result.append((content_length - int(stop), content_length))
return result
class HeaderElement(object):
"""An element (with parameters) from an HTTP header's element list."""
def __init__(self, value, params=None):
self.value = value
if params is None:
params = {}
self.params = params
def __cmp__(self, other):
return cmp(self.value, other.value)
def __lt__(self, other):
return self.value < other.value
def __str__(self):
p = [';%s=%s' % (k, v) for k, v in iteritems(self.params)]
return str('%s%s' % (self.value, ''.join(p)))
def __bytes__(self):
return ntob(self.__str__())
def __unicode__(self):
return ntou(self.__str__())
@staticmethod
def parse(elementstr):
"""Transform 'token;key=val' to ('token', {'key': 'val'})."""
initial_value, params = parse_header(elementstr)
return initial_value, params
@classmethod
def from_str(cls, elementstr):
"""Construct an instance from a string of the form 'token;key=val'."""
ival, params = cls.parse(elementstr)
return cls(ival, params)
q_separator = re.compile(r'; *q *=')
class AcceptElement(HeaderElement):
"""An element (with parameters) from an Accept* header's element list.
AcceptElement objects are comparable; the more-preferred object will be
"less than" the less-preferred object. They are also therefore sortable;
if you sort a list of AcceptElement objects, they will be listed in
priority order; the most preferred value will be first. Yes, it should
have been the other way around, but it's too late to fix now.
"""
@classmethod
def from_str(cls, elementstr):
qvalue = None
# The first "q" parameter (if any) separates the initial
# media-range parameter(s) (if any) from the accept-params.
atoms = q_separator.split(elementstr, 1)
media_range = atoms.pop(0).strip()
if atoms:
# The qvalue for an Accept header can have extensions. The other
# headers cannot, but it's easier to parse them as if they did.
qvalue = HeaderElement.from_str(atoms[0].strip())
media_type, params = cls.parse(media_range)
if qvalue is not None:
params['q'] = qvalue
return cls(media_type, params)
@property
def qvalue(self):
'The qvalue, or priority, of this value.'
val = self.params.get('q', '1')
if isinstance(val, HeaderElement):
val = val.value
return float(val)
def __cmp__(self, other):
diff = cmp(self.qvalue, other.qvalue)
if diff == 0:
diff = cmp(str(self), str(other))
return diff
def __lt__(self, other):
if self.qvalue == other.qvalue:
return str(self) < str(other)
else:
return self.qvalue < other.qvalue
RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
def header_elements(fieldname, fieldvalue):
"""Return a sorted HeaderElement list from a comma-separated header string.
"""
if not fieldvalue:
return []
result = []
for element in RE_HEADER_SPLIT.split(fieldvalue):
if fieldname.startswith('Accept') or fieldname == 'TE':
hv = AcceptElement.from_str(element)
else:
hv = HeaderElement.from_str(element)
result.append(hv)
return list(reversed(sorted(result)))
def decode_TEXT(value):
r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr")."""
atoms = decode_header(value)
decodedvalue = ''
for atom, charset in atoms:
if charset is not None:
atom = atom.decode(charset)
decodedvalue += atom
return decodedvalue
def valid_status(status):
"""Return legal HTTP status Code, Reason-phrase and Message.
The status arg must be an int, or a str that begins with an int.
If status is an int, or a str and no reason-phrase is supplied,
a default reason-phrase will be provided.
"""
if not status:
status = 200
status = str(status)
parts = status.split(' ', 1)
if len(parts) == 1:
# No reason supplied.
code, = parts
reason = None
else:
code, reason = parts
reason = reason.strip()
try:
code = int(code)
except ValueError:
raise ValueError('Illegal response status from server '
'(%s is non-numeric).' % repr(code))
if code < 100 or code > 599:
raise ValueError('Illegal response status from server '
'(%s is out of range).' % repr(code))
if code not in response_codes:
# code is unknown but not illegal
default_reason, message = '', ''
else:
default_reason, message = response_codes[code]
if reason is None:
reason = default_reason
return code, reason, message
# NOTE: the parse_qs functions that follow are modified version of those
# in the python3.0 source - we need to pass through an encoding to the unquote
# method, but the default parse_qs function doesn't allow us to. These do.
def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
"""Parse a query given as a string argument.
Arguments:
qs: URL-encoded query string to be parsed
keep_blank_values: flag indicating whether blank values in
URL encoded queries should be treated as blank strings. A
true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
strict_parsing: flag indicating what to do with parsing errors. If
false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
Returns a dict, as G-d intended.
"""
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
d = {}
for name_value in pairs:
if not name_value and not strict_parsing:
continue
nv = name_value.split('=', 1)
if len(nv) != 2:
if strict_parsing:
raise ValueError('bad query field: %r' % (name_value,))
# Handle case of a control-name with no equal sign
if keep_blank_values:
nv.append('')
else:
continue
if len(nv[1]) or keep_blank_values:
name = unquote_qs(nv[0], encoding)
value = unquote_qs(nv[1], encoding)
if name in d:
if not isinstance(d[name], list):
d[name] = [d[name]]
d[name].append(value)
else:
d[name] = value
return d
image_map_pattern = re.compile(r'[0-9]+,[0-9]+')
def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
"""Build a params dictionary from a query_string.
Duplicate key/value pairs in the provided query_string will be
returned as {'key': [val1, val2, ...]}. Single key/values will
be returned as strings: {'key': 'value'}.
"""
if image_map_pattern.match(query_string):
# Server-side image map. Map the coords to 'x' and 'y'
# (like CGI::Request does).
pm = query_string.split(',')
pm = {'x': int(pm[0]), 'y': int(pm[1])}
else:
pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
return pm
class CaseInsensitiveDict(dict):
"""A case-insensitive dict subclass.
Each key is changed on entry to str(key).title().
"""
def __getitem__(self, key):
return dict.__getitem__(self, str(key).title())
def __setitem__(self, key, value):
dict.__setitem__(self, str(key).title(), value)
def __delitem__(self, key):
dict.__delitem__(self, str(key).title())
def __contains__(self, key):
return dict.__contains__(self, str(key).title())
def get(self, key, default=None):
return dict.get(self, str(key).title(), default)
if hasattr({}, 'has_key'):
def has_key(self, key):
return str(key).title() in self
def update(self, E):
for k in E.keys():
self[str(k).title()] = E[k]
@classmethod
def fromkeys(cls, seq, value=None):
newdict = cls()
for k in seq:
newdict[str(k).title()] = value
return newdict
def setdefault(self, key, x=None):
key = str(key).title()
try:
return self[key]
except KeyError:
self[key] = x
return x
def pop(self, key, default):
return dict.pop(self, str(key).title(), default)
# TEXT = <any OCTET except CTLs, but including LWS>
#
# A CRLF is allowed in the definition of TEXT only as part of a header
# field continuation. It is expected that the folding LWS will be
# replaced with a single SP before interpretation of the TEXT value."
if str == bytes:
header_translate_table = ''.join([chr(i) for i in xrange(256)])
header_translate_deletechars = ''.join(
[chr(i) for i in xrange(32)]) + chr(127)
else:
header_translate_table = None
header_translate_deletechars = bytes(range(32)) + bytes([127])
class HeaderMap(CaseInsensitiveDict):
"""A dict subclass for HTTP request and response headers.
Each key is changed on entry to str(key).title(). This allows headers
to be case-insensitive and avoid duplicates.
Values are header values (decoded according to :rfc:`2047` if necessary).
"""
protocol = (1, 1)
encodings = ['ISO-8859-1']
# Someday, when http-bis is done, this will probably get dropped
# since few servers, clients, or intermediaries do it. But until then,
# we're going to obey the spec as is.
# "Words of *TEXT MAY contain characters from character sets other than
# ISO-8859-1 only when encoded according to the rules of RFC 2047."
use_rfc_2047 = True
def elements(self, key):
"""Return a sorted list of HeaderElements for the given header."""
key = str(key).title()
value = self.get(key)
return header_elements(key, value)
def values(self, key):
"""Return a sorted list of HeaderElement.value for the given header."""
return [e.value for e in self.elements(key)]
def output(self):
"""Transform self into a list of (name, value) tuples."""
return list(self.encode_header_items(self.items()))
@classmethod
def encode_header_items(cls, header_items):
"""
Prepare the sequence of name, value tuples into a form suitable for
transmitting on the wire for HTTP.
"""
for k, v in header_items:
if isinstance(k, six.text_type):
k = cls.encode(k)
if not isinstance(v, text_or_bytes):
v = str(v)
if isinstance(v, six.text_type):
v = cls.encode(v)
# See header_translate_* constants above.
# Replace only if you really know what you're doing.
k = k.translate(header_translate_table,
header_translate_deletechars)
v = v.translate(header_translate_table,
header_translate_deletechars)
yield (k, v)
@classmethod
def encode(cls, v):
"""Return the given header name or value, encoded for HTTP output."""
for enc in cls.encodings:
try:
return v.encode(enc)
except UnicodeEncodeError:
continue
if cls.protocol == (1, 1) and cls.use_rfc_2047:
# Encode RFC-2047 TEXT
# (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
# We do our own here instead of using the email module
# because we never want to fold lines--folding has
# been deprecated by the HTTP working group.
v = b2a_base64(v.encode('utf-8'))
return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
raise ValueError('Could not encode header part %r using '
'any of the encodings %r.' %
(v, cls.encodings))
class Host(object):
"""An internet address.
name
Should be the client's host name. If not available (because no DNS
lookup is performed), the IP address should be used instead.
"""
ip = '0.0.0.0'
port = 80
name = 'unknown.tld'
def __init__(self, ip, port, name=None):
self.ip = ip
self.port = port
if name is None:
name = ip
self.name = name
def __repr__(self):
return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name)

94
cherrypy/lib/jsontools.py Normal file
View File

@@ -0,0 +1,94 @@
import cherrypy
from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode
def json_processor(entity):
"""Read application/json data into request.json."""
if not entity.headers.get(ntou('Content-Length'), ntou('')):
raise cherrypy.HTTPError(411)
body = entity.fp.read()
with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'):
cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
force=True, debug=False, processor=json_processor):
"""Add a processor to parse JSON request entities:
The default processor places the parsed data into request.json.
Incoming request entities which match the given content_type(s) will
be deserialized from JSON to the Python equivalent, and the result
stored at cherrypy.request.json. The 'content_type' argument may
be a Content-Type string or a list of allowable Content-Type strings.
If the 'force' argument is True (the default), then entities of other
content types will not be allowed; "415 Unsupported Media Type" is
raised instead.
Supply your own processor to use a custom decoder, or to handle the parsed
data differently. The processor can be configured via
tools.json_in.processor or via the decorator method.
Note that the deserializer requires the client send a Content-Length
request header, or it will raise "411 Length Required". If for any
other reason the request entity cannot be deserialized from JSON,
it will raise "400 Bad Request: Invalid JSON document".
You must be using Python 2.6 or greater, or have the 'simplejson'
package importable; otherwise, ValueError is raised during processing.
"""
request = cherrypy.serving.request
if isinstance(content_type, text_or_bytes):
content_type = [content_type]
if force:
if debug:
cherrypy.log('Removing body processors %s' %
repr(request.body.processors.keys()), 'TOOLS.JSON_IN')
request.body.processors.clear()
request.body.default_proc = cherrypy.HTTPError(
415, 'Expected an entity of content type %s' %
', '.join(content_type))
for ct in content_type:
if debug:
cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN')
request.body.processors[ct] = processor
def json_handler(*args, **kwargs):
value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
return json_encode(value)
def json_out(content_type='application/json', debug=False,
handler=json_handler):
"""Wrap request.handler to serialize its output to JSON. Sets Content-Type.
If the given content_type is None, the Content-Type response header
is not set.
Provide your own handler to use a custom encoder. For example
cherrypy.config['tools.json_out.handler'] = <function>, or
@json_out(handler=function).
You must be using Python 2.6 or greater, or have the 'simplejson'
package importable; otherwise, ValueError is raised during processing.
"""
request = cherrypy.serving.request
# request.handler may be set to None by e.g. the caching tool
# to signal to all components that a response body has already
# been attached, in which case we don't need to wrap anything.
if request.handler is None:
return
if debug:
cherrypy.log('Replacing %s with JSON handler' % request.handler,
'TOOLS.JSON_OUT')
request._json_inner_handler = request.handler
request.handler = handler
if content_type is not None:
if debug:
cherrypy.log('Setting Content-Type to %s' %
content_type, 'TOOLS.JSON_OUT')
cherrypy.serving.response.headers['Content-Type'] = content_type

142
cherrypy/lib/lockfile.py Normal file
View File

@@ -0,0 +1,142 @@
"""
Platform-independent file locking. Inspired by and modeled after zc.lockfile.
"""
import os
try:
import msvcrt
except ImportError:
pass
try:
import fcntl
except ImportError:
pass
class LockError(Exception):
'Could not obtain a lock'
msg = 'Unable to lock %r'
def __init__(self, path):
super(LockError, self).__init__(self.msg % path)
class UnlockError(LockError):
'Could not release a lock'
msg = 'Unable to unlock %r'
# first, a default, naive locking implementation
class LockFile(object):
"""
A default, naive locking implementation. Always fails if the file
already exists.
"""
def __init__(self, path):
self.path = path
try:
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
except OSError:
raise LockError(self.path)
os.close(fd)
def release(self):
os.remove(self.path)
def remove(self):
pass
class SystemLockFile(object):
"""
An abstract base class for platform-specific locking.
"""
def __init__(self, path):
self.path = path
try:
# Open lockfile for writing without truncation:
self.fp = open(path, 'r+')
except IOError:
# If the file doesn't exist, IOError is raised; Use a+ instead.
# Note that there may be a race here. Multiple processes
# could fail on the r+ open and open the file a+, but only
# one will get the the lock and write a pid.
self.fp = open(path, 'a+')
try:
self._lock_file()
except:
self.fp.seek(1)
self.fp.close()
del self.fp
raise
self.fp.write(' %s\n' % os.getpid())
self.fp.truncate()
self.fp.flush()
def release(self):
if not hasattr(self, 'fp'):
return
self._unlock_file()
self.fp.close()
del self.fp
def remove(self):
"""
Attempt to remove the file
"""
try:
os.remove(self.path)
except:
pass
def _unlock_file(self):
"""Attempt to obtain the lock on self.fp. Raise UnlockError if not
released."""
class WindowsLockFile(SystemLockFile):
def _lock_file(self):
# Lock just the first byte
try:
msvcrt.locking(self.fp.fileno(), msvcrt.LK_NBLCK, 1)
except IOError:
raise LockError(self.fp.name)
def _unlock_file(self):
try:
self.fp.seek(0)
msvcrt.locking(self.fp.fileno(), msvcrt.LK_UNLCK, 1)
except IOError:
raise UnlockError(self.fp.name)
if 'msvcrt' in globals():
LockFile = WindowsLockFile
class UnixLockFile(SystemLockFile):
def _lock_file(self):
flags = fcntl.LOCK_EX | fcntl.LOCK_NB
try:
fcntl.flock(self.fp.fileno(), flags)
except IOError:
raise LockError(self.fp.name)
# no need to implement _unlock_file, it will be unlocked on close()
if 'fcntl' in globals():
LockFile = UnixLockFile

47
cherrypy/lib/locking.py Normal file
View File

@@ -0,0 +1,47 @@
import datetime
class NeverExpires(object):
def expired(self):
return False
class Timer(object):
"""
A simple timer that will indicate when an expiration time has passed.
"""
def __init__(self, expiration):
'Create a timer that expires at `expiration` (UTC datetime)'
self.expiration = expiration
@classmethod
def after(cls, elapsed):
"""
Return a timer that will expire after `elapsed` passes.
"""
return cls(datetime.datetime.utcnow() + elapsed)
def expired(self):
return datetime.datetime.utcnow() >= self.expiration
class LockTimeout(Exception):
'An exception when a lock could not be acquired before a timeout period'
class LockChecker(object):
"""
Keep track of the time and detect if a timeout has expired
"""
def __init__(self, session_id, timeout):
self.session_id = session_id
if timeout:
self.timer = Timer.after(timeout)
else:
self.timer = NeverExpires()
def expired(self):
if self.timer.expired():
raise LockTimeout(
'Timeout acquiring lock for %(session_id)s' % vars(self))
return False

View File

@@ -3,97 +3,95 @@
CherryPy users
==============
You can profile any of your pages as follows:
You can profile any of your pages as follows::
from cherrypy.lib import profiler
class Root:
p = profile.Profiler("/path/to/profile/dir")
p = profiler.Profiler("/path/to/profile/dir")
@cherrypy.expose
def index(self):
self.p.run(self._index)
index.exposed = True
def _index(self):
return "Hello, world!"
cherrypy.tree.mount(Root())
You can also turn on profiling for all requests
using the make_app function as WSGI middleware.
using the ``make_app`` function as WSGI middleware.
CherryPy developers
===================
This module can be used whenever you make changes to CherryPy,
to get a quick sanity-check on overall CP performance. Use the
"--profile" flag when running the test suite. Then, use the serve()
``--profile`` flag when running the test suite. Then, use the ``serve()``
function to browse the results in a web browser. If you run this
module from the command line, it will call serve() for you.
module from the command line, it will call ``serve()`` for you.
"""
import io
import os
import os.path
import sys
import warnings
import cherrypy
# Make profiler output more readable by adding __init__ modules' parents.
def new_func_strip_path(func_name):
filename, line, name = func_name
if filename.endswith("__init__.py"):
return os.path.basename(filename[:-12]) + filename[-12:], line, name
return os.path.basename(filename), line, name
try:
import profile
import pstats
def new_func_strip_path(func_name):
"""Make profiler output more readable by adding `__init__` modules' parents
"""
filename, line, name = func_name
if filename.endswith('__init__.py'):
return os.path.basename(filename[:-12]) + filename[-12:], line, name
return os.path.basename(filename), line, name
pstats.func_strip_path = new_func_strip_path
except ImportError:
profile = None
pstats = None
import warnings
msg = ("Your installation of Python does not have a profile module. "
"If you're on Debian, try `sudo apt-get install python-profiler`. "
"See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
warnings.warn(msg)
import os, os.path
import sys
try:
import cStringIO as StringIO
except ImportError:
import StringIO
_count = 0
class Profiler(object):
def __init__(self, path=None):
if not path:
path = os.path.join(os.path.dirname(__file__), "profile")
path = os.path.join(os.path.dirname(__file__), 'profile')
self.path = path
if not os.path.exists(path):
os.makedirs(path)
def run(self, func, *args, **params):
"""Dump profile data into self.path."""
global _count
c = _count = _count + 1
path = os.path.join(self.path, "cp_%04d.prof" % c)
path = os.path.join(self.path, 'cp_%04d.prof' % c)
prof = profile.Profile()
result = prof.runcall(func, *args, **params)
prof.dump_stats(path)
return result
def statfiles(self):
"""statfiles() -> list of available profiles."""
""":rtype: list of available profiles.
"""
return [f for f in os.listdir(self.path)
if f.startswith("cp_") and f.endswith(".prof")]
if f.startswith('cp_') and f.endswith('.prof')]
def stats(self, filename, sortby='cumulative'):
"""stats(index) -> output of print_stats() for the given profile."""
sio = StringIO.StringIO()
""":rtype stats(index): output of print_stats() for the given profile.
"""
sio = io.StringIO()
if sys.version_info >= (2, 5):
s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
s.strip_dirs()
@@ -114,7 +112,8 @@ class Profiler(object):
response = sio.getvalue()
sio.close()
return response
@cherrypy.expose
def index(self):
return """<html>
<head><title>CherryPy profile data</title></head>
@@ -124,57 +123,71 @@ class Profiler(object):
</frameset>
</html>
"""
index.exposed = True
@cherrypy.expose
def menu(self):
yield "<h2>Profiling runs</h2>"
yield "<p>Click on one of the runs below to see profiling data.</p>"
yield '<h2>Profiling runs</h2>'
yield '<p>Click on one of the runs below to see profiling data.</p>'
runs = self.statfiles()
runs.sort()
for i in runs:
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (i, i)
menu.exposed = True
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (
i, i)
@cherrypy.expose
def report(self, filename):
import cherrypy
cherrypy.response.headers['Content-Type'] = 'text/plain'
return self.stats(filename)
report.exposed = True
class ProfileAggregator(Profiler):
def __init__(self, path=None):
Profiler.__init__(self, path)
global _count
self.count = _count = _count + 1
self.profiler = profile.Profile()
def run(self, func, *args):
path = os.path.join(self.path, "cp_%04d.prof" % self.count)
result = self.profiler.runcall(func, *args)
def run(self, func, *args, **params):
path = os.path.join(self.path, 'cp_%04d.prof' % self.count)
result = self.profiler.runcall(func, *args, **params)
self.profiler.dump_stats(path)
return result
class make_app:
def __init__(self, nextapp, path=None, aggregate=False):
"""Make a WSGI middleware app which wraps 'nextapp' with profiling.
nextapp: the WSGI application to wrap, usually an instance of
nextapp
the WSGI application to wrap, usually an instance of
cherrypy.Application.
path: where to dump the profiling output.
aggregate: if True, profile data for all HTTP requests will go in
path
where to dump the profiling output.
aggregate
if True, profile data for all HTTP requests will go in
a single file. If False (the default), each HTTP request will
dump its profile data into a separate file.
"""
if profile is None or pstats is None:
msg = ('Your installation of Python does not have a profile '
"module. If you're on Debian, try "
'`sudo apt-get install python-profiler`. '
'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
'for details.')
warnings.warn(msg)
self.nextapp = nextapp
self.aggregate = aggregate
if aggregate:
self.profiler = ProfileAggregator(path)
else:
self.profiler = Profiler(path)
def __call__(self, environ, start_response):
def gather():
result = []
@@ -185,14 +198,20 @@ class make_app:
def serve(path=None, port=8080):
import cherrypy
if profile is None or pstats is None:
msg = ('Your installation of Python does not have a profile module. '
"If you're on Debian, try "
'`sudo apt-get install python-profiler`. '
'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
'for details.')
warnings.warn(msg)
cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10,
'environment': "production",
'environment': 'production',
})
cherrypy.quickstart(Profiler(path))
if __name__ == "__main__":
if __name__ == '__main__':
serve(*tuple(sys.argv[1:]))

534
cherrypy/lib/reprconf.py Normal file
View File

@@ -0,0 +1,534 @@
"""Generic configuration system using unrepr.
Configuration data may be supplied as a Python dictionary, as a filename,
or as an open file object. When you supply a filename or file, Python's
builtin ConfigParser is used (with some extensions).
Namespaces
----------
Configuration keys are separated into namespaces by the first "." in the key.
The only key that cannot exist in a namespace is the "environment" entry.
This special entry 'imports' other config entries from a template stored in
the Config.environments dict.
You can define your own namespaces to be called when new config is merged
by adding a named handler to Config.namespaces. The name can be any string,
and the handler must be either a callable or a context manager.
"""
try:
# Python 3.0+
from configparser import ConfigParser
except ImportError:
from ConfigParser import ConfigParser
try:
text_or_bytes
except NameError:
text_or_bytes = str
try:
# Python 3
import builtins
except ImportError:
# Python 2
import __builtin__ as builtins
import operator as _operator
import sys
def as_dict(config):
"""Return a dict from 'config' whether it is a dict, file, or filename."""
if isinstance(config, text_or_bytes):
config = Parser().dict_from_file(config)
elif hasattr(config, 'read'):
config = Parser().dict_from_file(config)
return config
class NamespaceSet(dict):
"""A dict of config namespace names and handlers.
Each config entry should begin with a namespace name; the corresponding
namespace handler will be called once for each config entry in that
namespace, and will be passed two arguments: the config key (with the
namespace removed) and the config value.
Namespace handlers may be any Python callable; they may also be
Python 2.5-style 'context managers', in which case their __enter__
method should return a callable to be used as the handler.
See cherrypy.tools (the Toolbox class) for an example.
"""
def __call__(self, config):
"""Iterate through config and pass it to each namespace handler.
config
A flat dict, where keys use dots to separate
namespaces, and values are arbitrary.
The first name in each config key is used to look up the corresponding
namespace handler. For example, a config entry of {'tools.gzip.on': v}
will call the 'tools' namespace handler with the args: ('gzip.on', v)
"""
# Separate the given config into namespaces
ns_confs = {}
for k in config:
if '.' in k:
ns, name = k.split('.', 1)
bucket = ns_confs.setdefault(ns, {})
bucket[name] = config[k]
# I chose __enter__ and __exit__ so someday this could be
# rewritten using Python 2.5's 'with' statement:
# for ns, handler in self.iteritems():
# with handler as callable:
# for k, v in ns_confs.get(ns, {}).iteritems():
# callable(k, v)
for ns, handler in self.items():
exit = getattr(handler, '__exit__', None)
if exit:
callable = handler.__enter__()
no_exc = True
try:
try:
for k, v in ns_confs.get(ns, {}).items():
callable(k, v)
except:
# The exceptional case is handled here
no_exc = False
if exit is None:
raise
if not exit(*sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if no_exc and exit:
exit(None, None, None)
else:
for k, v in ns_confs.get(ns, {}).items():
handler(k, v)
def __repr__(self):
return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
dict.__repr__(self))
def __copy__(self):
newobj = self.__class__()
newobj.update(self)
return newobj
copy = __copy__
class Config(dict):
"""A dict-like set of configuration data, with defaults and namespaces.
May take a file, filename, or dict.
"""
defaults = {}
environments = {}
namespaces = NamespaceSet()
def __init__(self, file=None, **kwargs):
self.reset()
if file is not None:
self.update(file)
if kwargs:
self.update(kwargs)
def reset(self):
"""Reset self to default values."""
self.clear()
dict.update(self, self.defaults)
def update(self, config):
"""Update self from a dict, file or filename."""
if isinstance(config, text_or_bytes):
# Filename
config = Parser().dict_from_file(config)
elif hasattr(config, 'read'):
# Open file object
config = Parser().dict_from_file(config)
else:
config = config.copy()
self._apply(config)
def _apply(self, config):
"""Update self from a dict."""
which_env = config.get('environment')
if which_env:
env = self.environments[which_env]
for k in env:
if k not in config:
config[k] = env[k]
dict.update(self, config)
self.namespaces(config)
def __setitem__(self, k, v):
dict.__setitem__(self, k, v)
self.namespaces({k: v})
class Parser(ConfigParser):
"""Sub-class of ConfigParser that keeps the case of options and that
raises an exception if the file cannot be read.
"""
def optionxform(self, optionstr):
return optionstr
def read(self, filenames):
if isinstance(filenames, text_or_bytes):
filenames = [filenames]
for filename in filenames:
# try:
# fp = open(filename)
# except IOError:
# continue
fp = open(filename)
try:
self._read(fp, filename)
finally:
fp.close()
def as_dict(self, raw=False, vars=None):
"""Convert an INI file to a dictionary"""
# Load INI file into a dict
result = {}
for section in self.sections():
if section not in result:
result[section] = {}
for option in self.options(section):
value = self.get(section, option, raw=raw, vars=vars)
try:
value = unrepr(value)
except Exception:
x = sys.exc_info()[1]
msg = ('Config error in section: %r, option: %r, '
'value: %r. Config values must be valid Python.' %
(section, option, value))
raise ValueError(msg, x.__class__.__name__, x.args)
result[section][option] = value
return result
def dict_from_file(self, file):
if hasattr(file, 'read'):
self.readfp(file)
else:
self.read(file)
return self.as_dict()
# public domain "unrepr" implementation, found on the web and then improved.
class _Builder2:
def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None:
raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__))
return m(o)
def astnode(self, s):
"""Return a Python2 ast Node compiled from a string."""
try:
import compiler
except ImportError:
# Fallback to eval when compiler package is not available,
# e.g. IronPython 1.0.
return eval(s)
p = compiler.parse('__tempvalue__ = ' + s)
return p.getChildren()[1].getChildren()[0].getChildren()[1]
def build_Subscript(self, o):
expr, flags, subs = o.getChildren()
expr = self.build(expr)
subs = self.build(subs)
return expr[subs]
def build_CallFunc(self, o):
children = o.getChildren()
# Build callee from first child
callee = self.build(children[0])
# Build args and kwargs from remaining children
args = []
kwargs = {}
for child in children[1:]:
class_name = child.__class__.__name__
# None is ignored
if class_name == 'NoneType':
continue
# Keywords become kwargs
if class_name == 'Keyword':
kwargs.update(self.build(child))
# Everything else becomes args
else :
args.append(self.build(child))
return callee(*args, **kwargs)
def build_Keyword(self, o):
key, value_obj = o.getChildren()
value = self.build(value_obj)
kw_dict = {key: value}
return kw_dict
def build_List(self, o):
return map(self.build, o.getChildren())
def build_Const(self, o):
return o.value
def build_Dict(self, o):
d = {}
i = iter(map(self.build, o.getChildren()))
for el in i:
d[el] = i.next()
return d
def build_Tuple(self, o):
return tuple(self.build_List(o))
def build_Name(self, o):
name = o.name
if name == 'None':
return None
if name == 'True':
return True
if name == 'False':
return False
# See if the Name is a package or module. If it is, import it.
try:
return modules(name)
except ImportError:
pass
# See if the Name is in builtins.
try:
return getattr(builtins, name)
except AttributeError:
pass
raise TypeError('unrepr could not resolve the name %s' % repr(name))
def build_Add(self, o):
left, right = map(self.build, o.getChildren())
return left + right
def build_Mul(self, o):
left, right = map(self.build, o.getChildren())
return left * right
def build_Getattr(self, o):
parent = self.build(o.expr)
return getattr(parent, o.attrname)
def build_NoneType(self, o):
return None
def build_UnarySub(self, o):
return -self.build(o.getChildren()[0])
def build_UnaryAdd(self, o):
return self.build(o.getChildren()[0])
class _Builder3:
def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None:
raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__))
return m(o)
def astnode(self, s):
"""Return a Python3 ast Node compiled from a string."""
try:
import ast
except ImportError:
# Fallback to eval when ast package is not available,
# e.g. IronPython 1.0.
return eval(s)
p = ast.parse('__tempvalue__ = ' + s)
return p.body[0].value
def build_Subscript(self, o):
return self.build(o.value)[self.build(o.slice)]
def build_Index(self, o):
return self.build(o.value)
def _build_call35(self, o):
"""
Workaround for python 3.5 _ast.Call signature, docs found here
https://greentreesnakes.readthedocs.org/en/latest/nodes.html
"""
import ast
callee = self.build(o.func)
args = []
if o.args is not None:
for a in o.args:
if isinstance(a, ast.Starred):
args.append(self.build(a.value))
else:
args.append(self.build(a))
kwargs = {}
for kw in o.keywords:
if kw.arg is None: # double asterix `**`
rst = self.build(kw.value)
if not isinstance(rst, dict):
raise TypeError('Invalid argument for call.'
'Must be a mapping object.')
# give preference to the keys set directly from arg=value
for k, v in rst.items():
if k not in kwargs:
kwargs[k] = v
else: # defined on the call as: arg=value
kwargs[kw.arg] = self.build(kw.value)
return callee(*args, **kwargs)
def build_Call(self, o):
if sys.version_info >= (3, 5):
return self._build_call35(o)
callee = self.build(o.func)
if o.args is None:
args = ()
else:
args = tuple([self.build(a) for a in o.args])
if o.starargs is None:
starargs = ()
else:
starargs = tuple(self.build(o.starargs))
if o.kwargs is None:
kwargs = {}
else:
kwargs = self.build(o.kwargs)
if o.keywords is not None: # direct a=b keywords
for kw in o.keywords:
# preference because is a direct keyword against **kwargs
kwargs[kw.arg] = self.build(kw.value)
return callee(*(args + starargs), **kwargs)
def build_List(self, o):
return list(map(self.build, o.elts))
def build_Str(self, o):
return o.s
def build_Num(self, o):
return o.n
def build_Dict(self, o):
return dict([(self.build(k), self.build(v))
for k, v in zip(o.keys, o.values)])
def build_Tuple(self, o):
return tuple(self.build_List(o))
def build_Name(self, o):
name = o.id
if name == 'None':
return None
if name == 'True':
return True
if name == 'False':
return False
# See if the Name is a package or module. If it is, import it.
try:
return modules(name)
except ImportError:
pass
# See if the Name is in builtins.
try:
import builtins
return getattr(builtins, name)
except AttributeError:
pass
raise TypeError('unrepr could not resolve the name %s' % repr(name))
def build_NameConstant(self, o):
return o.value
def build_UnaryOp(self, o):
op, operand = map(self.build, [o.op, o.operand])
return op(operand)
def build_BinOp(self, o):
left, op, right = map(self.build, [o.left, o.op, o.right])
return op(left, right)
def build_Add(self, o):
return _operator.add
def build_Mult(self, o):
return _operator.mul
def build_USub(self, o):
return _operator.neg
def build_Attribute(self, o):
parent = self.build(o.value)
return getattr(parent, o.attr)
def build_NoneType(self, o):
return None
def unrepr(s):
"""Return a Python object compiled from a string."""
if not s:
return s
if sys.version_info < (3, 0):
b = _Builder2()
else:
b = _Builder3()
obj = b.astnode(s)
return b.build(obj)
def modules(modulePath):
"""Load a module and retrieve a reference to that module."""
__import__(modulePath)
return sys.modules[modulePath]
def attributes(full_attribute_name):
"""Load a module and retrieve an attribute of that module."""
# Parse out the path, module, and attribute
last_dot = full_attribute_name.rfind('.')
attr_name = full_attribute_name[last_dot + 1:]
mod_path = full_attribute_name[:last_dot]
mod = modules(mod_path)
# Let an AttributeError propagate outward.
try:
attr = getattr(mod, attr_name)
except AttributeError:
raise AttributeError("'%s' object has no attribute '%s'"
% (mod_path, attr_name))
# Return a reference to the attribute.
return attr

View File

@@ -1,128 +0,0 @@
import cherrypy
class MultipartWrapper(object):
"""Wraps a file-like object, returning '' when Content-Length is reached.
The cgi module's logic for reading multipart MIME messages doesn't
allow the parts to know when the Content-Length for the entire message
has been reached, and doesn't allow for multipart-MIME messages that
omit the trailing CRLF (Flash 8's FileReference.upload(url), for example,
does this). The read_lines_to_outerboundary function gets stuck in a loop
until the socket times out.
This rfile wrapper simply monitors the incoming stream. When a read is
attempted past the Content-Length, it returns an empty string rather
than timing out (of course, if the last read *overlaps* the C-L, you'll
get the last bit of data up to C-L, and then the next read will return
an empty string).
"""
def __init__(self, rfile, clen):
self.rfile = rfile
self.clen = clen
self.bytes_read = 0
def read(self, size = None):
if self.clen:
# Return '' if we've read all the data.
if self.bytes_read >= self.clen:
return ''
# Reduce 'size' if it's over our limit.
new_bytes_read = self.bytes_read + size
if new_bytes_read > self.clen:
size = self.clen - self.bytes_read
data = self.rfile.read(size)
self.bytes_read += len(data)
return data
def readline(self, size = None):
if size is not None:
if self.clen:
# Return '' if we've read all the data.
if self.bytes_read >= self.clen:
return ''
# Reduce 'size' if it's over our limit.
new_bytes_read = self.bytes_read + size
if new_bytes_read > self.clen:
size = self.clen - self.bytes_read
data = self.rfile.readline(size)
self.bytes_read += len(data)
return data
# User didn't specify a size ...
# We read the line in chunks to make sure it's not a 100MB line !
res = []
size = 256
while True:
if self.clen:
# Return if we've read all the data.
if self.bytes_read >= self.clen:
return ''.join(res)
# Reduce 'size' if it's over our limit.
new_bytes_read = self.bytes_read + size
if new_bytes_read > self.clen:
size = self.clen - self.bytes_read
data = self.rfile.readline(size)
self.bytes_read += len(data)
res.append(data)
# See http://www.cherrypy.org/ticket/421
if len(data) < size or data[-1:] == "\n":
return ''.join(res)
def readlines(self, sizehint = 0):
# Shamelessly stolen from StringIO
total = 0
lines = []
line = self.readline()
while line:
lines.append(line)
total += len(line)
if 0 < sizehint <= total:
break
line = self.readline()
return lines
def close(self):
self.rfile.close()
def __iter__(self):
return self.rfile
def next(self):
if self.clen:
# Return '' if we've read all the data.
if self.bytes_read >= self.clen:
return ''
data = self.rfile.next()
self.bytes_read += len(data)
return data
def safe_multipart(flash_only=False):
"""Wrap request.rfile in a reader that won't crash on no trailing CRLF."""
h = cherrypy.request.headers
if not h.get('Content-Type','').startswith('multipart/'):
return
if flash_only and not 'Shockwave Flash' in h.get('User-Agent', ''):
return
clen = h.get('Content-Length', '0')
try:
clen = int(clen)
except ValueError:
return
cherrypy.request.rfile = MultipartWrapper(cherrypy.request.rfile, clen)
def init():
"""Create a Tool for safe_multipart and add it to cherrypy.tools."""
cherrypy.tools.safe_multipart = cherrypy.Tool('before_request_body',
safe_multipart)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,141 +1,256 @@
import mimetypes
mimetypes.init()
mimetypes.types_map['.dwg']='image/x-dwg'
mimetypes.types_map['.ico']='image/x-icon'
import os
import re
import stat
import time
import urllib
import mimetypes
try:
from io import UnsupportedOperation
except ImportError:
UnsupportedOperation = object()
import cherrypy
from cherrypy.lib import cptools, http, file_generator_limited
from cherrypy._cpcompat import ntob, unquote
from cherrypy.lib import cptools, httputil, file_generator_limited
def serve_file(path, content_type=None, disposition=None, name=None):
"""Set status, headers, and body in order to serve the given file.
mimetypes.init()
mimetypes.types_map['.dwg'] = 'image/x-dwg'
mimetypes.types_map['.ico'] = 'image/x-icon'
mimetypes.types_map['.bz2'] = 'application/x-bzip2'
mimetypes.types_map['.gz'] = 'application/x-gzip'
def serve_file(path, content_type=None, disposition=None, name=None,
debug=False):
"""Set status, headers, and body in order to serve the given path.
The Content-Type header will be set to the content_type arg, if provided.
If not provided, the Content-Type will be guessed by the file extension
of the 'path' argument.
If disposition is not None, the Content-Disposition header will be set
to "<disposition>; filename=<name>". If name is None, it will be set
to the basename of path. If disposition is None, no Content-Disposition
header will be written.
"""
response = cherrypy.response
response = cherrypy.serving.response
# If path is relative, users should fix it by making path absolute.
# That is, CherryPy should not guess where the application root is.
# It certainly should *not* use cwd (since CP may be invoked from a
# variety of paths). If using tools.staticdir, you can make your relative
# paths become absolute by supplying a value for "tools.staticdir.root".
if not os.path.isabs(path):
raise ValueError("'%s' is not an absolute path." % path)
msg = "'%s' is not an absolute path." % path
if debug:
cherrypy.log(msg, 'TOOLS.STATICFILE')
raise ValueError(msg)
try:
st = os.stat(path)
except OSError:
except (OSError, TypeError, ValueError):
# OSError when file fails to stat
# TypeError on Python 2 when there's a null byte
# ValueError on Python 3 when there's a null byte
if debug:
cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
raise cherrypy.NotFound()
# Check if path is a directory.
if stat.S_ISDIR(st.st_mode):
# Let the caller deal with it as they like.
if debug:
cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
raise cherrypy.NotFound()
# Set the Last-Modified response header, so that
# modified-since validation code can work.
response.headers['Last-Modified'] = http.HTTPDate(st.st_mtime)
response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
cptools.validate_since()
if content_type is None:
# Set content-type based on filename extension
ext = ""
ext = ''
i = path.rfind('.')
if i != -1:
ext = path[i:].lower()
content_type = mimetypes.types_map.get(ext, "text/plain")
response.headers['Content-Type'] = content_type
content_type = mimetypes.types_map.get(ext, None)
if content_type is not None:
response.headers['Content-Type'] = content_type
if debug:
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
cd = None
if disposition is not None:
if name is None:
name = os.path.basename(path)
cd = '%s; filename="%s"' % (disposition, name)
response.headers["Content-Disposition"] = cd
response.headers['Content-Disposition'] = cd
if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
# Set Content-Length and use an iterable (file object)
# this way CP won't load the whole file in memory
c_len = st.st_size
bodyfile = open(path, 'rb')
content_length = st.st_size
fileobj = open(path, 'rb')
return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
debug=False):
"""Set status, headers, and body in order to serve the given file object.
The Content-Type header will be set to the content_type arg, if provided.
If disposition is not None, the Content-Disposition header will be set
to "<disposition>; filename=<name>". If name is None, 'filename' will
not be set. If disposition is None, no Content-Disposition header will
be written.
CAUTION: If the request contains a 'Range' header, one or more seek()s will
be performed on the file object. This may cause undesired behavior if
the file object is not seekable. It could also produce undesired results
if the caller set the read position of the file object prior to calling
serve_fileobj(), expecting that the data would be served starting from that
position.
"""
response = cherrypy.serving.response
try:
st = os.fstat(fileobj.fileno())
except AttributeError:
if debug:
cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC')
content_length = None
except UnsupportedOperation:
content_length = None
else:
# Set the Last-Modified response header, so that
# modified-since validation code can work.
response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
cptools.validate_since()
content_length = st.st_size
if content_type is not None:
response.headers['Content-Type'] = content_type
if debug:
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
cd = None
if disposition is not None:
if name is None:
cd = disposition
else:
cd = '%s; filename="%s"' % (disposition, name)
response.headers['Content-Disposition'] = cd
if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
def _serve_fileobj(fileobj, content_type, content_length, debug=False):
"""Internal. Set response.body to the given file object, perhaps ranged."""
response = cherrypy.serving.response
# HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
if cherrypy.request.protocol >= (1, 1):
response.headers["Accept-Ranges"] = "bytes"
r = http.get_ranges(cherrypy.request.headers.get('Range'), c_len)
request = cherrypy.serving.request
if request.protocol >= (1, 1):
response.headers['Accept-Ranges'] = 'bytes'
r = httputil.get_ranges(request.headers.get('Range'), content_length)
if r == []:
response.headers['Content-Range'] = "bytes */%s" % c_len
message = "Invalid Range (first-byte-pos greater than Content-Length)"
response.headers['Content-Range'] = 'bytes */%s' % content_length
message = ('Invalid Range (first-byte-pos greater than '
'Content-Length)')
if debug:
cherrypy.log(message, 'TOOLS.STATIC')
raise cherrypy.HTTPError(416, message)
if r:
if len(r) == 1:
# Return a single-part response.
start, stop = r[0]
if stop > c_len:
stop = c_len
if stop > content_length:
stop = content_length
r_len = stop - start
response.status = "206 Partial Content"
response.headers['Content-Range'] = ("bytes %s-%s/%s" %
(start, stop - 1, c_len))
if debug:
cherrypy.log(
'Single part; start: %r, stop: %r' % (start, stop),
'TOOLS.STATIC')
response.status = '206 Partial Content'
response.headers['Content-Range'] = (
'bytes %s-%s/%s' % (start, stop - 1, content_length))
response.headers['Content-Length'] = r_len
bodyfile.seek(start)
response.body = file_generator_limited(bodyfile, r_len)
fileobj.seek(start)
response.body = file_generator_limited(fileobj, r_len)
else:
# Return a multipart/byteranges response.
response.status = "206 Partial Content"
import mimetools
boundary = mimetools.choose_boundary()
ct = "multipart/byteranges; boundary=%s" % boundary
response.status = '206 Partial Content'
try:
# Python 3
from email.generator import _make_boundary as make_boundary
except ImportError:
# Python 2
from mimetools import choose_boundary as make_boundary
boundary = make_boundary()
ct = 'multipart/byteranges; boundary=%s' % boundary
response.headers['Content-Type'] = ct
if response.headers.has_key("Content-Length"):
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
del response.headers['Content-Length']
def file_ranges():
# Apache compatibility:
yield "\r\n"
yield ntob('\r\n')
for start, stop in r:
yield "--" + boundary
yield "\r\nContent-type: %s" % content_type
yield ("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
% (start, stop - 1, c_len))
bodyfile.seek(start)
for chunk in file_generator_limited(bodyfile, stop-start):
if debug:
cherrypy.log(
'Multipart; start: %r, stop: %r' % (
start, stop),
'TOOLS.STATIC')
yield ntob('--' + boundary, 'ascii')
yield ntob('\r\nContent-type: %s' % content_type,
'ascii')
yield ntob(
'\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % (
start, stop - 1, content_length),
'ascii')
fileobj.seek(start)
gen = file_generator_limited(fileobj, stop - start)
for chunk in gen:
yield chunk
yield "\r\n"
yield ntob('\r\n')
# Final boundary
yield "--" + boundary + "--"
yield ntob('--' + boundary + '--', 'ascii')
# Apache compatibility:
yield "\r\n"
yield ntob('\r\n')
response.body = file_ranges()
return response.body
response.headers['Content-Length'] = c_len
response.body = bodyfile
else:
if debug:
cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
# Set Content-Length and use an iterable (file object)
# this way CP won't load the whole file in memory
response.headers['Content-Length'] = content_length
response.body = fileobj
return response.body
def serve_download(path, name=None):
"""Serve 'path' as an application/x-download attachment."""
# This is such a common idiom I felt it deserved its own wrapper.
return serve_file(path, "application/x-download", "attachment", name)
return serve_file(path, 'application/x-download', 'attachment', name)
def _attempt(filename, content_types):
def _attempt(filename, content_types, debug=False):
if debug:
cherrypy.log('Attempting %r (content_types %r)' %
(filename, content_types), 'TOOLS.STATICDIR')
try:
# you can set the content types for a
# complete directory per extension
@@ -143,87 +258,124 @@ def _attempt(filename, content_types):
if content_types:
r, ext = os.path.splitext(filename)
content_type = content_types.get(ext[1:], None)
serve_file(filename, content_type=content_type)
serve_file(filename, content_type=content_type, debug=debug)
return True
except cherrypy.NotFound:
# If we didn't find the static file, continue handling the
# request. We might find a dynamic handler instead.
if debug:
cherrypy.log('NotFound', 'TOOLS.STATICFILE')
return False
def staticdir(section, dir, root="", match="", content_types=None, index=""):
def staticdir(section, dir, root='', match='', content_types=None, index='',
debug=False):
"""Serve a static resource from the given (root +) dir.
If 'match' is given, request.path_info will be searched for the given
regular expression before attempting to serve static content.
If content_types is given, it should be a Python dictionary of
{file-extension: content-type} pairs, where 'file-extension' is
a string (e.g. "gif") and 'content-type' is the value to write
out in the Content-Type response header (e.g. "image/gif").
If 'index' is provided, it should be the (relative) name of a file to
serve for directory requests. For example, if the dir argument is
'/home/me', the Request-URI is 'myapp', and the index arg is
'index.html', the file '/home/me/myapp/index.html' will be sought.
match
If given, request.path_info will be searched for the given
regular expression before attempting to serve static content.
content_types
If given, it should be a Python dictionary of
{file-extension: content-type} pairs, where 'file-extension' is
a string (e.g. "gif") and 'content-type' is the value to write
out in the Content-Type response header (e.g. "image/gif").
index
If provided, it should be the (relative) name of a file to
serve for directory requests. For example, if the dir argument is
'/home/me', the Request-URI is 'myapp', and the index arg is
'index.html', the file '/home/me/myapp/index.html' will be sought.
"""
if match and not re.search(match, cherrypy.request.path_info):
request = cherrypy.serving.request
if request.method not in ('GET', 'HEAD'):
if debug:
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
return False
if match and not re.search(match, request.path_info):
if debug:
cherrypy.log('request.path_info %r does not match pattern %r' %
(request.path_info, match), 'TOOLS.STATICDIR')
return False
# Allow the use of '~' to refer to a user's home directory.
dir = os.path.expanduser(dir)
# If dir is relative, make absolute using "root".
if not os.path.isabs(dir):
if not root:
msg = "Static dir requires an absolute dir (or root)."
msg = 'Static dir requires an absolute dir (or root).'
if debug:
cherrypy.log(msg, 'TOOLS.STATICDIR')
raise ValueError(msg)
dir = os.path.join(root, dir)
# Determine where we are in the object tree relative to 'section'
# (where the static tool was defined).
if section == 'global':
section = "/"
section = section.rstrip(r"\/")
branch = cherrypy.request.path_info[len(section) + 1:]
branch = urllib.unquote(branch.lstrip(r"\/"))
section = '/'
section = section.rstrip(r'\/')
branch = request.path_info[len(section) + 1:]
branch = unquote(branch.lstrip(r'\/'))
# If branch is "", filename will end in a slash
filename = os.path.join(dir, branch)
if debug:
cherrypy.log('Checking file %r to fulfill %r' %
(filename, request.path_info), 'TOOLS.STATICDIR')
# There's a chance that the branch pulled from the URL might
# have ".." or similar uplevel attacks in it. Check that the final
# filename is a child of dir.
if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
raise cherrypy.HTTPError(403) # Forbidden
raise cherrypy.HTTPError(403) # Forbidden
handled = _attempt(filename, content_types)
if not handled:
# Check for an index file if a folder was requested.
if index:
handled = _attempt(os.path.join(filename, index), content_types)
if handled:
cherrypy.request.is_index = filename[-1] in (r"\/")
request.is_index = filename[-1] in (r'\/')
return handled
def staticfile(filename, root=None, match="", content_types=None):
def staticfile(filename, root=None, match='', content_types=None, debug=False):
"""Serve a static resource from the given (root +) filename.
If 'match' is given, request.path_info will be searched for the given
regular expression before attempting to serve static content.
If content_types is given, it should be a Python dictionary of
{file-extension: content-type} pairs, where 'file-extension' is
a string (e.g. "gif") and 'content-type' is the value to write
out in the Content-Type response header (e.g. "image/gif").
match
If given, request.path_info will be searched for the given
regular expression before attempting to serve static content.
content_types
If given, it should be a Python dictionary of
{file-extension: content-type} pairs, where 'file-extension' is
a string (e.g. "gif") and 'content-type' is the value to write
out in the Content-Type response header (e.g. "image/gif").
"""
if match and not re.search(match, cherrypy.request.path_info):
request = cherrypy.serving.request
if request.method not in ('GET', 'HEAD'):
if debug:
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
return False
if match and not re.search(match, request.path_info):
if debug:
cherrypy.log('request.path_info %r does not match pattern %r' %
(request.path_info, match), 'TOOLS.STATICFILE')
return False
# If filename is relative, make absolute using "root".
if not os.path.isabs(filename):
if not root:
msg = "Static tool requires an absolute filename (got '%s')." % filename
msg = "Static tool requires an absolute filename (got '%s')." % (
filename,)
if debug:
cherrypy.log(msg, 'TOOLS.STATICFILE')
raise ValueError(msg)
filename = os.path.join(root, filename)
return _attempt(filename, content_types)
return _attempt(filename, content_types, debug=debug)

View File

@@ -1,184 +0,0 @@
"""Functions to run cherrypy.response through Tidy or NSGML."""
import cgi
import os
import StringIO
import traceback
import cherrypy
def tidy(temp_dir, tidy_path, strict_xml=False, errors_to_ignore=None,
indent=False, wrap=False, warnings=True):
"""Run cherrypy.response through Tidy.
If either 'indent' or 'wrap' are specified, then response.body will be
set to the output of tidy. Otherwise, only errors (including warnings,
if warnings is True) will change the body.
Note that we use the standalone Tidy tool rather than the python
mxTidy module. This is because this module does not seem to be
stable and it crashes on some HTML pages (which means that the
server would also crash)
"""
response = cherrypy.response
# the tidy tool, by its very nature it's not generator friendly,
# so we just collapse the body and work with it.
orig_body = response.collapse_body()
fct = response.headers.get('Content-Type', '')
ct = fct.split(';')[0]
encoding = ''
i = fct.find('charset=')
if i != -1:
encoding = fct[i + 8:]
if ct == 'text/html':
page_file = os.path.join(temp_dir, 'page.html')
open(page_file, 'wb').write(orig_body)
out_file = os.path.join(temp_dir, 'tidy.out')
err_file = os.path.join(temp_dir, 'tidy.err')
tidy_enc = encoding.replace('-', '')
if tidy_enc:
tidy_enc = '-' + tidy_enc
strict_xml = ("", " -xml")[bool(strict_xml)]
if indent:
indent = ' -indent'
else:
indent = ''
if wrap is False:
wrap = ''
else:
try:
wrap = ' -wrap %d' % int(tidyWrap)
except:
wrap = ''
result = os.system('"%s" %s%s%s%s -f %s -o %s %s' %
(tidy_path, tidy_enc, strict_xml, indent, wrap,
err_file, out_file, page_file))
use_output = bool(indent or wrap) and not result
if use_output:
output = open(out_file, 'rb').read()
new_errs = []
for err in open(err_file, 'rb').read().splitlines():
if (err.find('Error') != -1 or
(warnings and err.find('Warning') != -1)):
ignore = 0
for err_ign in errors_to_ignore or []:
if err.find(err_ign) != -1:
ignore = 1
break
if not ignore:
new_errs.append(err)
if new_errs:
response.body = wrong_content('<br />'.join(new_errs), orig_body)
if response.headers.has_key("Content-Length"):
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
return
elif strict_xml:
# The HTML is OK, but is it valid XML?
# Use elementtree to parse XML
from elementtree.ElementTree import parse
tag_list = ['nbsp', 'quot']
for tag in tag_list:
orig_body = orig_body.replace('&' + tag + ';', tag.upper())
if encoding:
enctag = '<?xml version="1.0" encoding="%s"?>' % encoding
orig_body = enctag + orig_body
f = StringIO.StringIO(orig_body)
try:
tree = parse(f)
except:
# Wrong XML
body_file = StringIO.StringIO()
traceback.print_exc(file = body_file)
body_file = '<br />'.join(body_file.getvalue())
response.body = wrong_content(body_file, orig_body, "XML")
if response.headers.has_key("Content-Length"):
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
return
if use_output:
response.body = [output]
if response.headers.has_key("Content-Length"):
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
def html_space(text):
"""Escape text, replacing space with nbsp and tab with 4 nbsp's."""
return cgi.escape(text).replace('\t', ' ').replace(' ', '&nbsp;')
def html_break(text):
"""Escape text, replacing newline with HTML br element."""
return cgi.escape(text).replace('\n', '<br />')
def wrong_content(header, body, content_type="HTML"):
output = ["Wrong %s:<br />%s<br />" % (content_type, html_break(header))]
for i, line in enumerate(body.splitlines()):
output.append("%03d - %s" % (i + 1, html_space(line)))
return "<br />".join(output)
def nsgmls(temp_dir, nsgmls_path, catalog_path, errors_to_ignore=None):
response = cherrypy.response
# the tidy tool, by its very nature it's not generator friendly,
# so we just collect the body and work with it.
orig_body = response.collapse_body()
fct = response.headers.get('Content-Type', '')
ct = fct.split(';')[0]
encoding = ''
i = fct.find('charset=')
if i != -1:
encoding = fct[i + 8:]
if ct == 'text/html':
# Remove bits of Javascript (nsgmls doesn't seem to handle
# them correctly (for instance, if <a appears in your
# Javascript code nsgmls complains about it)
while True:
i = orig_body.find('<script')
if i == -1:
break
j = orig_body.find('</script>', i)
if j == -1:
break
orig_body = orig_body[:i] + orig_body[j+9:]
page_file = os.path.join(temp_dir, 'page.html')
open(page_file, 'wb').write(orig_body)
err_file = os.path.join(temp_dir, 'nsgmls.err')
command = ('%s -c%s -f%s -s -E10 %s' %
(nsgmls_path, catalog_path, err_file, page_file))
command = command.replace('\\', '/')
os.system(command)
errs = open(err_file, 'rb').read()
new_errs = []
for err in errs.splitlines():
ignore = False
for err_ign in errors_to_ignore or []:
if err.find(err_ign) != -1:
ignore = True
break
if not ignore:
new_errs.append(err)
if new_errs:
response.body = wrong_content('<br />'.join(new_errs), orig_body)
if response.headers.has_key("Content-Length"):
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]

View File

@@ -1,77 +0,0 @@
"""A CherryPy tool for hosting a foreign WSGI application."""
import sys
import warnings
import cherrypy
# is this sufficient for start_response?
def start_response(status, response_headers, exc_info=None):
cherrypy.response.status = status
headers_dict = dict(response_headers)
cherrypy.response.headers.update(headers_dict)
def make_environ():
"""grabbed some of below from wsgiserver.py
for hosting WSGI apps in non-WSGI environments (yikes!)
"""
request = cherrypy.request
# create and populate the wsgi environ
environ = dict()
environ["wsgi.version"] = (1,0)
environ["wsgi.url_scheme"] = request.scheme
environ["wsgi.input"] = request.rfile
environ["wsgi.errors"] = sys.stderr
environ["wsgi.multithread"] = True
environ["wsgi.multiprocess"] = False
environ["wsgi.run_once"] = False
environ["REQUEST_METHOD"] = request.method
environ["SCRIPT_NAME"] = request.script_name
environ["PATH_INFO"] = request.path_info
environ["QUERY_STRING"] = request.query_string
environ["SERVER_PROTOCOL"] = request.protocol
environ["SERVER_NAME"] = request.local.name
environ["SERVER_PORT"] = request.local.port
environ["REMOTE_HOST"] = request.remote.name
environ["REMOTE_ADDR"] = request.remote.ip
environ["REMOTE_PORT"] = request.remote.port
# then all the http headers
headers = request.headers
environ["CONTENT_TYPE"] = headers.get("Content-type", "")
environ["CONTENT_LENGTH"] = headers.get("Content-length", "")
for (k, v) in headers.iteritems():
envname = "HTTP_" + k.upper().replace("-","_")
environ[envname] = v
return environ
def run(app, env=None):
"""Run the given WSGI app and set response.body to its output."""
warnings.warn("This module is deprecated and will be removed in "
"Cherrypy 3.2. See http://www.cherrypy.org/ticket/700 "
"for more information.")
try:
environ = cherrypy.request.wsgi_environ.copy()
environ['SCRIPT_NAME'] = cherrypy.request.script_name
environ['PATH_INFO'] = cherrypy.request.path_info
except AttributeError:
environ = make_environ()
if env:
environ.update(env)
# run the wsgi app and have it set response.body
response = app(environ, start_response)
try:
cherrypy.response.body = [x for x in response]
finally:
if hasattr(response, "close"):
response.close()
return True

View File

@@ -1,13 +1,21 @@
import sys
import cherrypy
from cherrypy._cpcompat import ntob
def get_xmlrpclib():
try:
import xmlrpc.client as x
except ImportError:
import xmlrpclib as x
return x
def process_body():
"""Return (params, method) from request body."""
try:
import xmlrpclib
return xmlrpclib.loads(cherrypy.request.body.read())
return get_xmlrpclib().loads(cherrypy.request.body.read())
except Exception:
return ('ERROR PARAMS', ), 'ERRORMETHOD'
@@ -29,21 +37,21 @@ def _set_response(body):
# as a "Protocol Error", we'll just return 200 every time.
response = cherrypy.response
response.status = '200 OK'
response.body = body
response.body = ntob(body, 'utf-8')
response.headers['Content-Type'] = 'text/xml'
response.headers['Content-Length'] = len(body)
def respond(body, encoding='utf-8', allow_none=0):
import xmlrpclib
xmlrpclib = get_xmlrpclib()
if not isinstance(body, xmlrpclib.Fault):
body = (body,)
_set_response(xmlrpclib.dumps(body, methodresponse=1,
encoding=encoding,
allow_none=allow_none))
def on_error(*args, **kwargs):
body = str(sys.exc_info()[1])
import xmlrpclib
xmlrpclib = get_xmlrpclib()
_set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))

View File

@@ -10,5 +10,5 @@ use with the bus. Some use tool-specific channels; see the documentation
for each class.
"""
from cherrypy.process.wspbus import bus
from cherrypy.process import plugins, servers
from cherrypy.process.wspbus import bus # noqa
from cherrypy.process import plugins, servers # noqa

View File

@@ -2,22 +2,44 @@
import os
import re
try:
set
except NameError:
from sets import Set as set
import signal as _signal
import sys
import time
import threading
from cherrypy._cpcompat import text_or_bytes, get_thread_ident
from cherrypy._cpcompat import ntob, Timer
# _module__file__base is used by Autoreload to make
# absolute any filenames retrieved from sys.modules which are not
# already absolute paths. This is to work around Python's quirk
# of importing the startup script and using a relative filename
# for it in sys.modules.
#
# Autoreload examines sys.modules afresh every time it runs. If an application
# changes the current directory by executing os.chdir(), then the next time
# Autoreload runs, it will not be able to find any filenames which are
# not absolute paths, because the current directory is not the same as when the
# module was first imported. Autoreload will then wrongly conclude the file
# has "changed", and initiate the shutdown/re-exec sequence.
# See ticket #917.
# For this workaround to have a decent probability of success, this module
# needs to be imported as early as possible, before the app has much chance
# to change the working directory.
_module__file__base = os.getcwd()
class SimplePlugin(object):
"""Plugin base class which auto-subscribes methods for known channels."""
bus = None
"""A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine.
"""
def __init__(self, bus):
self.bus = bus
def subscribe(self):
"""Register this object as a (multi-channel) listener on the bus."""
for channel in self.bus.listeners:
@@ -25,7 +47,7 @@ class SimplePlugin(object):
method = getattr(self, channel, None)
if method is not None:
self.bus.subscribe(channel, method)
def unsubscribe(self):
"""Unregister this object as a listener on the bus."""
for channel in self.bus.listeners:
@@ -35,25 +57,42 @@ class SimplePlugin(object):
self.bus.unsubscribe(channel, method)
class SignalHandler(object):
"""Register bus channels (and listeners) for system signals.
By default, instantiating this object subscribes the following signals
and listeners:
TERM: bus.exit
HUP : bus.restart
USR1: bus.graceful
You can modify what signals your application listens for, and what it does
when it receives signals, by modifying :attr:`SignalHandler.handlers`,
a dict of {signal name: callback} pairs. The default set is::
handlers = {'SIGTERM': self.bus.exit,
'SIGHUP': self.handle_SIGHUP,
'SIGUSR1': self.bus.graceful,
}
The :func:`SignalHandler.handle_SIGHUP`` method calls
:func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
if the process is daemonized, but
:func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
if the process is attached to a TTY. This is because Unix window
managers tend to send SIGHUP to terminal windows when the user closes them.
Feel free to add signals which are not available on every platform.
The :class:`SignalHandler` will ignore errors raised from attempting
to register handlers for unknown signals.
"""
# Map from signal numbers to names
handlers = {}
"""A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
signals = {}
"""A map from signal numbers to names."""
for k, v in vars(_signal).items():
if k.startswith('SIG') and not k.startswith('SIG_'):
signals[v] = k
del k, v
def __init__(self, bus):
self.bus = bus
# Set default handlers
@@ -61,138 +100,191 @@ class SignalHandler(object):
'SIGHUP': self.handle_SIGHUP,
'SIGUSR1': self.bus.graceful,
}
if sys.platform[:4] == 'java':
del self.handlers['SIGUSR1']
self.handlers['SIGUSR2'] = self.bus.graceful
self.bus.log('SIGUSR1 cannot be set on the JVM platform. '
'Using SIGUSR2 instead.')
self.handlers['SIGINT'] = self._jython_SIGINT_handler
self._previous_handlers = {}
# used to determine is the process is a daemon in `self._is_daemonized`
self._original_pid = os.getpid()
def _jython_SIGINT_handler(self, signum=None, frame=None):
# See http://bugs.jython.org/issue1313
self.bus.log('Keyboard Interrupt: shutting down bus')
self.bus.exit()
def _is_daemonized(self):
"""Return boolean indicating if the current process is
running as a daemon.
The criteria to determine the `daemon` condition is to verify
if the current pid is not the same as the one that got used on
the initial construction of the plugin *and* the stdin is not
connected to a terminal.
The sole validation of the tty is not enough when the plugin
is executing inside other process like in a CI tool
(Buildbot, Jenkins).
"""
if (self._original_pid != os.getpid() and
not os.isatty(sys.stdin.fileno())):
return True
else:
return False
def subscribe(self):
for sig, func in self.handlers.iteritems():
"""Subscribe self.handlers to signals."""
for sig, func in self.handlers.items():
try:
self.set_handler(sig, func)
except ValueError:
pass
def unsubscribe(self):
for signum, handler in self._previous_handlers.iteritems():
"""Unsubscribe self.handlers from signals."""
for signum, handler in self._previous_handlers.items():
signame = self.signals[signum]
if handler is None:
self.bus.log("Restoring %s handler to SIG_DFL." % signame)
self.bus.log('Restoring %s handler to SIG_DFL.' % signame)
handler = _signal.SIG_DFL
else:
self.bus.log("Restoring %s handler %r." % (signame, handler))
self.bus.log('Restoring %s handler %r.' % (signame, handler))
try:
our_handler = _signal.signal(signum, handler)
if our_handler is None:
self.bus.log("Restored old %s handler %r, but our "
"handler was not registered." %
self.bus.log('Restored old %s handler %r, but our '
'handler was not registered.' %
(signame, handler), level=30)
except ValueError:
self.bus.log("Unable to restore %s handler %r." %
self.bus.log('Unable to restore %s handler %r.' %
(signame, handler), level=40, traceback=True)
def set_handler(self, signal, listener=None):
"""Subscribe a handler for the given signal (number or name).
If the optional 'listener' argument is provided, it will be
subscribed as a listener for the given signal's channel.
If the given signal name or number is not available on the current
platform, ValueError is raised.
"""
if isinstance(signal, basestring):
if isinstance(signal, text_or_bytes):
signum = getattr(_signal, signal, None)
if signum is None:
raise ValueError("No such signal: %r" % signal)
raise ValueError('No such signal: %r' % signal)
signame = signal
else:
try:
signame = self.signals[signal]
except KeyError:
raise ValueError("No such signal: %r" % signal)
raise ValueError('No such signal: %r' % signal)
signum = signal
prev = _signal.signal(signum, self._handle_signal)
self._previous_handlers[signum] = prev
if listener is not None:
self.bus.log("Listening for %s." % signame)
self.bus.log('Listening for %s.' % signame)
self.bus.subscribe(signame, listener)
def _handle_signal(self, signum=None, frame=None):
"""Python signal handler (self.set_handler subscribes it for you)."""
signame = self.signals[signum]
self.bus.log("Caught signal %s." % signame)
self.bus.log('Caught signal %s.' % signame)
self.bus.publish(signame)
def handle_SIGHUP(self):
if os.isatty(sys.stdin.fileno()):
# not daemonized (may be foreground or background)
self.bus.log("SIGHUP caught but not daemonized. Exiting.")
self.bus.exit()
else:
self.bus.log("SIGHUP caught while daemonized. Restarting.")
"""Restart if daemonized, else exit."""
if self._is_daemonized():
self.bus.log('SIGHUP caught while daemonized. Restarting.')
self.bus.restart()
else:
# not daemonized (may be foreground or background)
self.bus.log('SIGHUP caught but not daemonized. Exiting.')
self.bus.exit()
try:
import pwd, grp
import pwd
import grp
except ImportError:
pwd, grp = None, None
class DropPrivileges(SimplePlugin):
"""Drop privileges. uid/gid arguments not available on Windows.
Special thanks to Gavin Baker: http://antonym.org/node/100.
Special thanks to `Gavin Baker <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
"""
def __init__(self, bus, umask=None, uid=None, gid=None):
SimplePlugin.__init__(self, bus)
self.finalized = False
self.uid = uid
self.gid = gid
self.umask = umask
def _get_uid(self):
return self._uid
def _set_uid(self, val):
if val is not None:
if pwd is None:
self.bus.log("pwd module not available; ignoring uid.",
self.bus.log('pwd module not available; ignoring uid.',
level=30)
val = None
elif isinstance(val, basestring):
elif isinstance(val, text_or_bytes):
val = pwd.getpwnam(val)[2]
self._uid = val
uid = property(_get_uid, _set_uid, doc="The uid under which to run.")
uid = property(_get_uid, _set_uid,
doc='The uid under which to run. Availability: Unix.')
def _get_gid(self):
return self._gid
def _set_gid(self, val):
if val is not None:
if grp is None:
self.bus.log("grp module not available; ignoring gid.",
self.bus.log('grp module not available; ignoring gid.',
level=30)
val = None
elif isinstance(val, basestring):
elif isinstance(val, text_or_bytes):
val = grp.getgrnam(val)[2]
self._gid = val
gid = property(_get_gid, _set_gid, doc="The gid under which to run.")
gid = property(_get_gid, _set_gid,
doc='The gid under which to run. Availability: Unix.')
def _get_umask(self):
return self._umask
def _set_umask(self, val):
if val is not None:
try:
os.umask
except AttributeError:
self.bus.log("umask function not available; ignoring umask.",
self.bus.log('umask function not available; ignoring umask.',
level=30)
val = None
self._umask = val
umask = property(_get_umask, _set_umask, doc="The umask under which to run.")
umask = property(
_get_umask,
_set_umask,
doc="""The default permission mode for newly created files and
directories.
Usually expressed in octal format, for example, ``0644``.
Availability: Unix, Windows.
""")
def start(self):
# uid/gid
def current_ids():
@@ -203,7 +295,7 @@ class DropPrivileges(SimplePlugin):
if grp:
group = grp.getgrgid(os.getgid())[0]
return name, group
if self.finalized:
if not (self.uid is None and self.gid is None):
self.bus.log('Already running as uid: %r gid: %r' %
@@ -216,10 +308,11 @@ class DropPrivileges(SimplePlugin):
self.bus.log('Started as uid: %r gid: %r' % current_ids())
if self.gid is not None:
os.setgid(self.gid)
os.setgroups([])
if self.uid is not None:
os.setuid(self.uid)
self.bus.log('Running as uid: %r gid: %r' % current_ids())
# umask
if self.finalized:
if self.umask is not None:
@@ -231,7 +324,7 @@ class DropPrivileges(SimplePlugin):
old_umask = os.umask(self.umask)
self.bus.log('umask old: %03o, new: %03o' %
(old_umask, self.umask))
self.finalized = True
# This is slightly higher than the priority for server.start
# in order to facilitate the most common use: starting on a low
@@ -240,12 +333,13 @@ class DropPrivileges(SimplePlugin):
class Daemonizer(SimplePlugin):
"""Daemonize the running script.
Use this with a Web Site Process Bus via:
Use this with a Web Site Process Bus via::
Daemonizer(bus).subscribe()
When this component finishes, the process is completely decoupled from
the parent environment. Please note that when this component is used,
the return code from the parent process will still be 0 if a startup
@@ -255,7 +349,7 @@ class Daemonizer(SimplePlugin):
of whether the process fully started. In fact, that return code only
indicates if the process succesfully finished the first fork.
"""
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
stderr='/dev/null'):
SimplePlugin.__init__(self, bus)
@@ -263,11 +357,11 @@ class Daemonizer(SimplePlugin):
self.stdout = stdout
self.stderr = stderr
self.finalized = False
def start(self):
if self.finalized:
self.bus.log('Already deamonized.')
# forking has issues with threads:
# http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
# "The general problem with making fork() work in a multi-threaded
@@ -277,15 +371,15 @@ class Daemonizer(SimplePlugin):
self.bus.log('There are %r active threads. '
'Daemonizing now may cause strange failures.' %
threading.enumerate(), level=30)
# See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
# (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
# Finish up with the current stdout/stderr
sys.stdout.flush()
sys.stderr.flush()
# Do first fork.
try:
pid = os.fork()
@@ -296,29 +390,31 @@ class Daemonizer(SimplePlugin):
# This is the first parent. Exit, now that we've forked.
self.bus.log('Forking once.')
os._exit(0)
except OSError, exc:
except OSError:
# Python raises OSError rather than returning negative numbers.
sys.exit("%s: fork #1 failed: (%d) %s\n"
exc = sys.exc_info()[1]
sys.exit('%s: fork #1 failed: (%d) %s\n'
% (sys.argv[0], exc.errno, exc.strerror))
os.setsid()
# Do second fork
try:
pid = os.fork()
if pid > 0:
self.bus.log('Forking twice.')
os._exit(0) # Exit second parent
except OSError, exc:
sys.exit("%s: fork #2 failed: (%d) %s\n"
os._exit(0) # Exit second parent
except OSError:
exc = sys.exc_info()[1]
sys.exit('%s: fork #2 failed: (%d) %s\n'
% (sys.argv[0], exc.errno, exc.strerror))
os.chdir("/")
os.chdir('/')
os.umask(0)
si = open(self.stdin, "r")
so = open(self.stdout, "a+")
se = open(self.stderr, "a+", 0)
si = open(self.stdin, 'r')
so = open(self.stdout, 'a+')
se = open(self.stderr, 'a+')
# os.dup2(fd, fd2) will close fd2 if necessary,
# so we don't explicitly close stdin/out/err.
@@ -326,30 +422,31 @@ class Daemonizer(SimplePlugin):
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
self.bus.log('Daemonized to PID: %s' % os.getpid())
self.finalized = True
start.priority = 65
class PIDFile(SimplePlugin):
"""Maintain a PID file via a WSPBus."""
def __init__(self, bus, pidfile):
SimplePlugin.__init__(self, bus)
self.pidfile = pidfile
self.finalized = False
def start(self):
pid = os.getpid()
if self.finalized:
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
else:
open(self.pidfile, "wb").write(str(pid))
open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8'))
self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
self.finalized = True
start.priority = 70
def exit(self):
try:
os.remove(self.pidfile)
@@ -360,131 +457,241 @@ class PIDFile(SimplePlugin):
pass
class PerpetualTimer(threading._Timer):
"""A subclass of threading._Timer whose run() method repeats."""
class PerpetualTimer(Timer):
"""A responsive subclass of threading.Timer whose run() method repeats.
Use this timer only when you really need a very interruptible timer;
this checks its 'finished' condition up to 20 times a second, which can
results in pretty high CPU usage
"""
def __init__(self, *args, **kwargs):
"Override parent constructor to allow 'bus' to be provided."
self.bus = kwargs.pop('bus', None)
super(PerpetualTimer, self).__init__(*args, **kwargs)
def run(self):
while True:
self.finished.wait(self.interval)
if self.finished.isSet():
return
self.function(*self.args, **self.kwargs)
try:
self.function(*self.args, **self.kwargs)
except Exception:
if self.bus:
self.bus.log(
'Error in perpetual timer thread function %r.' %
self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs.
raise
class BackgroundTask(threading.Thread):
"""A subclass of threading.Thread whose run() method repeats.
Use this class for most repeating tasks. It uses time.sleep() to wait
for each interval, which isn't very responsive; that is, even if you call
self.cancel(), you'll have to wait until the sleep() call finishes before
the thread stops. To compensate, it defaults to being daemonic, which means
it won't delay stopping the whole process.
"""
def __init__(self, interval, function, args=[], kwargs={}, bus=None):
super(BackgroundTask, self).__init__()
self.interval = interval
self.function = function
self.args = args
self.kwargs = kwargs
self.running = False
self.bus = bus
# default to daemonic
self.daemon = True
def cancel(self):
self.running = False
def run(self):
self.running = True
while self.running:
time.sleep(self.interval)
if not self.running:
return
try:
self.function(*self.args, **self.kwargs)
except Exception:
if self.bus:
self.bus.log('Error in background task thread function %r.'
% self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs.
raise
class Monitor(SimplePlugin):
"""WSPBus listener to periodically run a callback in its own thread.
bus: a Web Site Process Bus object.
callback: the function to call at intervals.
frequency: the time in seconds between callback runs.
"""
"""WSPBus listener to periodically run a callback in its own thread."""
callback = None
"""The function to call at intervals."""
frequency = 60
def __init__(self, bus, callback, frequency=60):
"""The time in seconds between callback runs."""
thread = None
"""A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>`
thread.
"""
def __init__(self, bus, callback, frequency=60, name=None):
SimplePlugin.__init__(self, bus)
self.callback = callback
self.frequency = frequency
self.thread = None
self.name = name
def start(self):
"""Start our callback in its own perpetual timer thread."""
"""Start our callback in its own background thread."""
if self.frequency > 0:
threadname = self.__class__.__name__
threadname = self.name or self.__class__.__name__
if self.thread is None:
self.thread = PerpetualTimer(self.frequency, self.callback)
self.thread = BackgroundTask(self.frequency, self.callback,
bus=self.bus)
self.thread.setName(threadname)
self.thread.start()
self.bus.log("Started monitor thread %r." % threadname)
self.bus.log('Started monitor thread %r.' % threadname)
else:
self.bus.log("Monitor thread %r already started." % threadname)
self.bus.log('Monitor thread %r already started.' % threadname)
start.priority = 70
def stop(self):
"""Stop our callback's perpetual timer thread."""
"""Stop our callback's background task thread."""
if self.thread is None:
self.bus.log("No thread running for %s." % self.__class__.__name__)
self.bus.log('No thread running for %s.' %
self.name or self.__class__.__name__)
else:
if self.thread is not threading.currentThread():
name = self.thread.getName()
self.thread.cancel()
self.thread.join()
self.bus.log("Stopped thread %r." % name)
if not self.thread.daemon:
self.bus.log('Joining %r' % name)
self.thread.join()
self.bus.log('Stopped thread %r.' % name)
self.thread = None
def graceful(self):
"""Stop the callback's perpetual timer thread and restart it."""
"""Stop the callback's background task thread and restart it."""
self.stop()
self.start()
class Autoreloader(Monitor):
"""Monitor which re-executes the process when files change."""
"""Monitor which re-executes the process when files change.
This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
if any of the files it monitors change (or is deleted). By default, the
autoreloader monitors all imported modules; you can add to the
set by adding to ``autoreload.files``::
cherrypy.engine.autoreload.files.add(myFile)
If there are imported files you do *not* wish to monitor, you can
adjust the ``match`` attribute, a regular expression. For example,
to stop monitoring cherrypy itself::
cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
the autoreload plugin takes a ``frequency`` argument. The default is
1 second; that is, the autoreloader will examine files once each second.
"""
files = None
"""The set of files to poll for modifications."""
frequency = 1
"""The interval in seconds at which to poll for modified files."""
match = '.*'
"""A regular expression by which to match filenames."""
def __init__(self, bus, frequency=1, match='.*'):
self.mtimes = {}
self.files = set()
self.match = match
Monitor.__init__(self, bus, self.run, frequency)
def start(self):
"""Start our own perpetual timer thread for self.run."""
"""Start our own background task thread for self.run."""
if self.thread is None:
self.mtimes = {}
Monitor.start(self)
start.priority = 70
start.priority = 70
def sysfiles(self):
"""Return a Set of sys.modules filenames to monitor."""
files = set()
for k, m in list(sys.modules.items()):
if re.match(self.match, k):
if (
hasattr(m, '__loader__') and
hasattr(m.__loader__, 'archive')
):
f = m.__loader__.archive
else:
f = getattr(m, '__file__', None)
if f is not None and not os.path.isabs(f):
# ensure absolute paths so a os.chdir() in the app
# doesn't break me
f = os.path.normpath(
os.path.join(_module__file__base, f))
files.add(f)
return files
def run(self):
"""Reload the process if registered files have been modified."""
sysfiles = set()
for k, m in sys.modules.items():
if re.match(self.match, k):
if hasattr(m, '__loader__'):
if hasattr(m.__loader__, 'archive'):
k = m.__loader__.archive
k = getattr(m, '__file__', None)
sysfiles.add(k)
for filename in sysfiles | self.files:
for filename in self.sysfiles() | self.files:
if filename:
if filename.endswith('.pyc'):
filename = filename[:-1]
oldtime = self.mtimes.get(filename, 0)
if oldtime is None:
# Module with no .py file. Skip it.
continue
try:
mtime = os.stat(filename).st_mtime
except OSError:
# Either a module with no .py file, or it's been deleted.
mtime = None
if filename not in self.mtimes:
# If a module has no .py file, this will be None.
self.mtimes[filename] = mtime
else:
if mtime is None or mtime > oldtime:
# The file has been deleted or modified.
self.bus.log("Restarting because %s changed." % filename)
self.bus.log('Restarting because %s changed.' %
filename)
self.thread.cancel()
self.bus.log("Stopped thread %r." % self.thread.getName())
self.bus.log('Stopped thread %r.' %
self.thread.getName())
self.bus.restart()
return
class ThreadManager(SimplePlugin):
"""Manager for HTTP request threads.
If you have control over thread creation and destruction, publish to
the 'acquire_thread' and 'release_thread' channels (for each thread).
This will register/unregister the current thread and publish to
'start_thread' and 'stop_thread' listeners in the bus as needed.
If threads are created and destroyed by code you do not control
(e.g., Apache), then, at the beginning of every HTTP request,
publish to 'acquire_thread' only. You should not publish to
@@ -492,38 +699,42 @@ class ThreadManager(SimplePlugin):
the thread will be re-used or not. The bus will call
'stop_thread' listeners for you when it stops.
"""
threads = None
"""A map of {thread ident: index number} pairs."""
def __init__(self, bus):
self.threads = {}
SimplePlugin.__init__(self, bus)
self.bus.listeners.setdefault('acquire_thread', set())
self.bus.listeners.setdefault('start_thread', set())
self.bus.listeners.setdefault('release_thread', set())
self.bus.listeners.setdefault('stop_thread', set())
def acquire_thread(self):
"""Run 'start_thread' listeners for the current thread.
If the current thread has already been seen, any 'start_thread'
listeners will not be run again.
"""
thread_ident = threading._get_ident()
thread_ident = get_thread_ident()
if thread_ident not in self.threads:
# We can't just use _get_ident as the thread ID
# We can't just use get_ident as the thread ID
# because some platforms reuse thread ID's.
i = len(self.threads) + 1
self.threads[thread_ident] = i
self.bus.publish('start_thread', i)
def release_thread(self):
"""Release the current thread and run 'stop_thread' listeners."""
thread_ident = threading._get_ident()
thread_ident = get_thread_ident()
i = self.threads.pop(thread_ident, None)
if i is not None:
self.bus.publish('stop_thread', i)
def stop(self):
"""Release all threads and run all 'stop_thread' listeners."""
for thread_ident, i in self.threads.iteritems():
for thread_ident, i in self.threads.items():
self.bus.publish('stop_thread', i)
self.threads.clear()
graceful = stop

View File

@@ -1,69 +1,201 @@
"""Adapt an HTTP server."""
"""
Starting in CherryPy 3.1, cherrypy.server is implemented as an
:ref:`Engine Plugin<plugins>`. It's an instance of
:class:`cherrypy._cpserver.Server`, which is a subclass of
:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class
is designed to control other servers, as well.
Multiple servers/ports
======================
If you need to start more than one HTTP server (to serve on multiple ports, or
protocols, etc.), you can manually register each one and then start them all
with engine.start::
s1 = ServerAdapter(cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80))
s2 = ServerAdapter(cherrypy.engine,
another.HTTPServer(host='127.0.0.1',
SSL=True))
s1.subscribe()
s2.subscribe()
cherrypy.engine.start()
.. index:: SCGI
FastCGI/SCGI
============
There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in
:mod:`cherrypy.process.servers`. To start an fcgi server, for example,
wrap an instance of it in a ServerAdapter::
addr = ('0.0.0.0', 4000)
f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr)
s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr)
s.subscribe()
The :doc:`cherryd</deployguide/cherryd>` startup script will do the above for
you via its `-f` flag.
Note that you need to download and install `flup <http://trac.saddi.com/flup>`_
yourself, whether you use ``cherryd`` or not.
.. _fastcgi:
.. index:: FastCGI
FastCGI
-------
A very simple setup lets your cherry run with FastCGI.
You just need the flup library,
plus a running Apache server (with ``mod_fastcgi``) or lighttpd server.
CherryPy code
^^^^^^^^^^^^^
hello.py::
#!/usr/bin/python
import cherrypy
class HelloWorld:
\"""Sample request handler class.\"""
@cherrypy.expose
def index(self):
return "Hello world!"
cherrypy.tree.mount(HelloWorld())
# CherryPy autoreload must be disabled for the flup server to work
cherrypy.config.update({'engine.autoreload.on':False})
Then run :doc:`/deployguide/cherryd` with the '-f' arg::
cherryd -c <myconfig> -d -f -i hello.py
Apache
^^^^^^
At the top level in httpd.conf::
FastCgiIpcDir /tmp
FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4
And inside the relevant VirtualHost section::
# FastCGI config
AddHandler fastcgi-script .fcgi
ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1
Lighttpd
^^^^^^^^
For `Lighttpd <http://www.lighttpd.net/>`_ you can follow these
instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is
active within ``server.modules``. Then, within your ``$HTTP["host"]``
directive, configure your fastcgi script like the following::
$HTTP["url"] =~ "" {
fastcgi.server = (
"/" => (
"script.fcgi" => (
"bin-path" => "/path/to/your/script.fcgi",
"socket" => "/tmp/script.sock",
"check-local" => "disable",
"disable-time" => 1,
"min-procs" => 1,
"max-procs" => 1, # adjust as needed
),
),
)
} # end of $HTTP["url"] =~ "^/"
Please see `Lighttpd FastCGI Docs
<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for
an explanation of the possible configuration options.
"""
import os
import sys
import time
import warnings
class ServerAdapter(object):
"""Adapter for an HTTP server.
If you need to start more than one HTTP server (to serve on multiple
ports, or protocols, etc.), you can manually register each one and then
start them all with bus.start:
start them all with bus.start::
s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
s1.subscribe()
s2.subscribe()
bus.start()
"""
def __init__(self, bus, httpserver=None, bind_addr=None):
self.bus = bus
self.httpserver = httpserver
self.bind_addr = bind_addr
self.interrupt = None
self.running = False
def subscribe(self):
self.bus.subscribe('start', self.start)
self.bus.subscribe('stop', self.stop)
def unsubscribe(self):
self.bus.unsubscribe('start', self.start)
self.bus.unsubscribe('stop', self.stop)
def start(self):
"""Start the HTTP server."""
if self.bind_addr is None:
on_what = "unknown interface (dynamic?)"
on_what = 'unknown interface (dynamic?)'
elif isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
on_what = "%s:%s" % (host, port)
on_what = self._get_base()
else:
on_what = "socket file: %s" % self.bind_addr
on_what = 'socket file: %s' % self.bind_addr
if self.running:
self.bus.log("Already serving on %s" % on_what)
self.bus.log('Already serving on %s' % on_what)
return
self.interrupt = None
if not self.httpserver:
raise ValueError("No HTTP server has been created.")
# Start the httpserver in a new thread.
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
raise ValueError('No HTTP server has been created.')
if not os.environ.get('LISTEN_PID', None):
# Start the httpserver in a new thread.
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
import threading
t = threading.Thread(target=self._start_http_thread)
t.setName("HTTPServer " + t.getName())
t.setName('HTTPServer ' + t.getName())
t.start()
self.wait()
self.running = True
self.bus.log("Serving on %s" % on_what)
self.bus.log('Serving on %s' % on_what)
start.priority = 75
def _get_base(self):
if not self.httpserver:
return ''
host, port = self.bind_addr
if getattr(self.httpserver, 'ssl_adapter', None):
scheme = 'https'
if port != 443:
host += ':%s' % port
else:
scheme = 'http'
if port != 80:
host += ':%s' % port
return '%s://%s' % (scheme, host)
def _start_http_thread(self):
"""HTTP servers MUST be running in new threads, so that the
main thread persists to receive KeyboardInterrupt's. If an
@@ -73,35 +205,37 @@ class ServerAdapter(object):
"""
try:
self.httpserver.start()
except KeyboardInterrupt, exc:
self.bus.log("<Ctrl-C> hit: shutting down HTTP server")
self.interrupt = exc
except KeyboardInterrupt:
self.bus.log('<Ctrl-C> hit: shutting down HTTP server')
self.interrupt = sys.exc_info()[1]
self.bus.exit()
except SystemExit, exc:
self.bus.log("SystemExit raised: shutting down HTTP server")
self.interrupt = exc
except SystemExit:
self.bus.log('SystemExit raised: shutting down HTTP server')
self.interrupt = sys.exc_info()[1]
self.bus.exit()
raise
except:
import sys
self.interrupt = sys.exc_info()[1]
self.bus.log("Error in HTTP server: shutting down",
self.bus.log('Error in HTTP server: shutting down',
traceback=True, level=40)
self.bus.exit()
raise
def wait(self):
"""Wait until the HTTP server is ready to receive requests."""
while not getattr(self.httpserver, "ready", False):
while not getattr(self.httpserver, 'ready', False):
if self.interrupt:
raise self.interrupt
time.sleep(.1)
# Wait for port to be occupied
if isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
wait_for_occupied_port(host, port)
if not os.environ.get('LISTEN_PID', None):
# Wait for port to be occupied if not running via socket-activation
# (for socket-activation the port will be managed by systemd )
if isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
wait_for_occupied_port(host, port)
def stop(self):
"""Stop the HTTP server."""
if self.running:
@@ -111,24 +245,49 @@ class ServerAdapter(object):
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
self.running = False
self.bus.log("HTTP Server %s shut down" % self.httpserver)
self.bus.log('HTTP Server %s shut down' % self.httpserver)
else:
self.bus.log("HTTP Server %s already shut down" % self.httpserver)
self.bus.log('HTTP Server %s already shut down' % self.httpserver)
stop.priority = 25
def restart(self):
"""Restart the HTTP server."""
self.stop()
self.start()
class FlupCGIServer(object):
"""Adapter for a flup.server.cgi.WSGIServer."""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.ready = False
def start(self):
"""Start the CGI server."""
# We have to instantiate the server class here because its __init__
# starts a threadpool. If we do it too early, daemonize won't work.
from flup.server.cgi import WSGIServer
self.cgiserver = WSGIServer(*self.args, **self.kwargs)
self.ready = True
self.cgiserver.run()
def stop(self):
"""Stop the HTTP server."""
self.ready = False
class FlupFCGIServer(object):
"""Adapter for a flup.server.fcgi.WSGIServer."""
def __init__(self, *args, **kwargs):
if kwargs.get('bindAddress', None) is None:
import socket
if not hasattr(socket.socket, 'fromfd'):
if not hasattr(socket, 'fromfd'):
raise ValueError(
'Dynamic FCGI server not available on this platform. '
'You must use a static or external one by providing a '
@@ -136,7 +295,7 @@ class FlupFCGIServer(object):
self.args = args
self.kwargs = kwargs
self.ready = False
def start(self):
"""Start the FCGI server."""
# We have to instantiate the server class here because its __init__
@@ -156,24 +315,26 @@ class FlupFCGIServer(object):
self.fcgiserver._oldSIGs = []
self.ready = True
self.fcgiserver.run()
def stop(self):
"""Stop the HTTP server."""
# Forcibly stop the fcgi server main event loop.
self.fcgiserver._keepGoing = False
# Force all worker threads to die off.
self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount
self.fcgiserver._threadPool.maxSpare = (
self.fcgiserver._threadPool._idleCount)
self.ready = False
class FlupSCGIServer(object):
"""Adapter for a flup.server.scgi.WSGIServer."""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.ready = False
def start(self):
"""Start the SCGI server."""
# We have to instantiate the server class here because its __init__
@@ -193,7 +354,7 @@ class FlupSCGIServer(object):
self.scgiserver._oldSIGs = []
self.ready = True
self.scgiserver.run()
def stop(self):
"""Stop the HTTP server."""
self.ready = False
@@ -208,24 +369,37 @@ def client_host(server_host):
if server_host == '0.0.0.0':
# 0.0.0.0 is INADDR_ANY, which should answer on localhost.
return '127.0.0.1'
if server_host == '::':
if server_host in ('::', '::0', '::0.0.0.0'):
# :: is IN6ADDR_ANY, which should answer on localhost.
# ::0 and ::0.0.0.0 are non-canonical but common
# ways to write IN6ADDR_ANY.
return '::1'
return server_host
def check_port(host, port, timeout=1.0):
"""Raise an error if the given port is not free on the given host."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
host = client_host(host)
port = int(port)
import socket
# AF_INET or AF_INET6 socket
# Get the correct address family for our host (allows IPv6 addresses)
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM):
try:
info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM)
except socket.gaierror:
if ':' in host:
info = [(
socket.AF_INET6, socket.SOCK_STREAM, 0, '', (host, port, 0, 0)
)]
else:
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, '', (host, port))]
for res in info:
af, socktype, proto, canonname, sa = res
s = None
try:
@@ -235,41 +409,62 @@ def check_port(host, port, timeout=1.0):
s.settimeout(timeout)
s.connect((host, port))
s.close()
raise IOError("Port %s is in use on %s; perhaps the previous "
"httpserver did not shut down properly." %
(repr(port), repr(host)))
except socket.error:
if s:
s.close()
else:
raise IOError('Port %s is in use on %s; perhaps the previous '
'httpserver did not shut down properly.' %
(repr(port), repr(host)))
def wait_for_free_port(host, port):
# Feel free to increase these defaults on slow systems:
free_port_timeout = 0.1
occupied_port_timeout = 1.0
def wait_for_free_port(host, port, timeout=None):
"""Wait for the specified port to become free (drop requests)."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
for trial in xrange(50):
if timeout is None:
timeout = free_port_timeout
for trial in range(50):
try:
# we are expecting a free port, so reduce the timeout
check_port(host, port, timeout=0.1)
check_port(host, port, timeout=timeout)
except IOError:
# Give the old server thread time to free the port.
time.sleep(0.1)
time.sleep(timeout)
else:
return
raise IOError("Port %r not free on %r" % (port, host))
def wait_for_occupied_port(host, port):
raise IOError('Port %r not free on %r' % (port, host))
def wait_for_occupied_port(host, port, timeout=None):
"""Wait for the specified port to become active (receive requests)."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
for trial in xrange(50):
if timeout is None:
timeout = occupied_port_timeout
for trial in range(50):
try:
check_port(host, port)
check_port(host, port, timeout=timeout)
except IOError:
# port is occupied
return
else:
time.sleep(.1)
raise IOError("Port %r not bound on %r" % (port, host))
time.sleep(timeout)
if host == client_host(host):
raise IOError('Port %r not bound on %r' % (port, host))
# On systems where a loopback interface is not available and the
# server is bound to all interfaces, it's difficult to determine
# whether the server is in fact occupying the port. In this case,
# just issue a warning and move on. See issue #1100.
msg = 'Unable to verify that the server is bound on %r' % port
warnings.warn(msg)

View File

@@ -1,7 +1,6 @@
"""Windows service. Requires pywin32."""
import os
import thread
import win32api
import win32con
import win32event
@@ -12,17 +11,18 @@ from cherrypy.process import wspbus, plugins
class ConsoleCtrlHandler(plugins.SimplePlugin):
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
def __init__(self, bus):
self.is_set = False
plugins.SimplePlugin.__init__(self, bus)
def start(self):
if self.is_set:
self.bus.log('Handler for console events already set.', level=40)
return
result = win32api.SetConsoleCtrlHandler(self.handle, 1)
if result == 0:
self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
@@ -30,38 +30,38 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
else:
self.bus.log('Set handler for console events.', level=40)
self.is_set = True
def stop(self):
if not self.is_set:
self.bus.log('Handler for console events already off.', level=40)
return
try:
result = win32api.SetConsoleCtrlHandler(self.handle, 0)
except ValueError:
# "ValueError: The object has not been registered"
result = 1
if result == 0:
self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
win32api.GetLastError(), level=40)
else:
self.bus.log('Removed handler for console events.', level=40)
self.is_set = False
def handle(self, event):
"""Handle console control events (like Ctrl-C)."""
if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
win32con.CTRL_CLOSE_EVENT):
self.bus.log('Console event %s: shutting down bus' % event)
# Remove self immediately so repeated Ctrl-C doesn't re-call it.
try:
self.stop()
except ValueError:
pass
self.bus.exit()
# 'First to return True stops the calls'
return 1
@@ -69,37 +69,39 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
class Win32Bus(wspbus.Bus):
"""A Web Site Process Bus implementation for Win32.
Instead of time.sleep, this bus blocks using native win32event objects.
"""
def __init__(self):
self.events = {}
wspbus.Bus.__init__(self)
def _get_state_event(self, state):
"""Return a win32event for the given state (creating it if needed)."""
try:
return self.events[state]
except KeyError:
event = win32event.CreateEvent(None, 0, 0,
u"WSPBus %s Event (pid=%r)" %
'WSPBus %s Event (pid=%r)' %
(state.name, os.getpid()))
self.events[state] = event
return event
def _get_state(self):
return self._state
def _set_state(self, value):
self._state = value
event = self._get_state_event(value)
win32event.PulseEvent(event)
state = property(_get_state, _set_state)
def wait(self, state, interval=0.1):
def wait(self, state, interval=0.1, channel=None):
"""Wait for the given state(s), KeyboardInterrupt or SystemExit.
Since this class uses native win32event objects, the interval
argument is ignored.
"""
@@ -107,7 +109,8 @@ class Win32Bus(wspbus.Bus):
# Don't wait for an event that beat us to the punch ;)
if self.state not in state:
events = tuple([self._get_state_event(s) for s in state])
win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE)
win32event.WaitForMultipleObjects(
events, 0, win32event.INFINITE)
else:
# Don't wait for an event that beat us to the punch ;)
if self.state != state:
@@ -116,22 +119,23 @@ class Win32Bus(wspbus.Bus):
class _ControlCodes(dict):
"""Control codes used to "signal" a service via ControlService.
User-defined control codes are in the range 128-255. We generally use
the standard Python value for the Linux signal and add 128. Example:
>>> signal.SIGUSR1
10
control_codes['graceful'] = 128 + 10
"""
def key_for(self, obj):
"""For the given value, return its corresponding key."""
for key, val in self.iteritems():
for key, val in self.items():
if val is obj:
return key
raise ValueError("The given object could not be found: %r" % obj)
raise ValueError('The given object could not be found: %r' % obj)
control_codes = _ControlCodes({'graceful': 138})
@@ -146,27 +150,28 @@ def signal_child(service, command):
class PyWebService(win32serviceutil.ServiceFramework):
"""Python Web Service."""
_svc_name_ = "Python Web Service"
_svc_display_name_ = "Python Web Service"
_svc_name_ = 'Python Web Service'
_svc_display_name_ = 'Python Web Service'
_svc_deps_ = None # sequence of service names on which this depends
_exe_name_ = "pywebsvc"
_exe_name_ = 'pywebsvc'
_exe_args_ = None # Default to no arguments
# Only exists on Windows 2000 or later, ignored on windows NT
_svc_description_ = "Python Web Service"
_svc_description_ = 'Python Web Service'
def SvcDoRun(self):
from cherrypy import process
process.bus.start()
process.bus.block()
def SvcStop(self):
from cherrypy import process
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
process.bus.exit()
def SvcOther(self, control):
process.bus.publish(control_codes.key_for(control))

View File

@@ -20,24 +20,24 @@ autoreload component.
Ideally, a Bus object will be flexible enough to be useful in a variety
of invocation scenarios:
1. The deployer starts a site from the command line via a framework-
neutral deployment script; applications from multiple frameworks
are mixed in a single site. Command-line arguments and configuration
files are used to define site-wide components such as the HTTP server,
WSGI component graph, autoreload behavior, signal handling, etc.
1. The deployer starts a site from the command line via a
framework-neutral deployment script; applications from multiple frameworks
are mixed in a single site. Command-line arguments and configuration
files are used to define site-wide components such as the HTTP server,
WSGI component graph, autoreload behavior, signal handling, etc.
2. The deployer starts a site via some other process, such as Apache;
applications from multiple frameworks are mixed in a single site.
Autoreload and signal handling (from Python at least) are disabled.
applications from multiple frameworks are mixed in a single site.
Autoreload and signal handling (from Python at least) are disabled.
3. The deployer starts a site via a framework-specific mechanism;
for example, when running tests, exploring tutorials, or deploying
single applications from a single framework. The framework controls
which site-wide components are enabled as it sees fit.
for example, when running tests, exploring tutorials, or deploying
single applications from a single framework. The framework controls
which site-wide components are enabled as it sees fit.
The Bus object in this package uses topic-based publish-subscribe
messaging to accomplish all this. A few topic channels are built in
('start', 'stop', 'exit', and 'graceful'). Frameworks and site containers
are free to define their own. If a message is sent to a channel that has
not been defined or has no listeners, there is no effect.
('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and
site containers are free to define their own. If a message is sent to a
channel that has not been defined or has no listeners, there is no effect.
In general, there should only ever be a single Bus object per process.
Frameworks and site containers share a single Bus object by publishing
@@ -46,7 +46,7 @@ messages and subscribing listeners.
The Bus object works as a finite state machine which models the current
state of the process. Bus methods move it from one state to another;
those methods then publish to subscribed listeners on the channel for
the new state.
the new state.::
O
|
@@ -61,25 +61,69 @@ the new state.
"""
import atexit
import ctypes
import operator
import os
try:
set
except NameError:
from sets import Set as set
import subprocess
import sys
import threading
import time
import traceback as _traceback
import warnings
import six
from cherrypy._cpcompat import _args_from_interpreter_flags
# Here I save the value of os.getcwd(), which, if I am imported early enough,
# will be the directory from which the startup script was run. This is needed
# by _do_execv(), to change back to the original directory before execv()ing a
# new process. This is a defense against the application having changed the
# current working directory (which could make sys.executable "not found" if
# sys.executable is a relative-path, and/or cause other problems).
_startup_cwd = os.getcwd()
class ChannelFailures(Exception):
"""Exception raised when errors occur in a listener during Bus.publish().
"""
delimiter = '\n'
def __init__(self, *args, **kwargs):
super(Exception, self).__init__(*args, **kwargs)
self._exceptions = list()
def handle_exception(self):
"""Append the current exception to self."""
self._exceptions.append(sys.exc_info()[1])
def get_instances(self):
"""Return a list of seen exception instances."""
return self._exceptions[:]
def __str__(self):
exception_strings = map(repr, self.get_instances())
return self.delimiter.join(exception_strings)
__repr__ = __str__
def __bool__(self):
return bool(self._exceptions)
__nonzero__ = __bool__
# Use a flag to indicate the state of the bus.
class _StateEnum(object):
class State(object):
name = None
def __repr__(self):
return "states.%s" % self.name
return 'states.%s' % self.name
def __setattr__(self, key, value):
if isinstance(value, self.State):
value.name = key
@@ -92,92 +136,109 @@ states.STOPPING = states.State()
states.EXITING = states.State()
try:
import fcntl
except ImportError:
max_files = 0
else:
try:
max_files = os.sysconf('SC_OPEN_MAX')
except AttributeError:
max_files = 1024
class Bus(object):
"""Process state-machine and messenger for HTTP site deployment.
All listeners for a given channel are guaranteed to be called even
if others at the same channel fail. Each failure is logged, but
execution proceeds on to the next listener. The only way to stop all
processing from inside a listener is to raise SystemExit and stop the
whole server.
"""
states = states
state = states.STOPPED
execv = False
max_cloexec_files = max_files
def __init__(self):
self.execv = False
self.state = states.STOPPED
channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main'
self.listeners = dict(
[(channel, set()) for channel
in ('start', 'stop', 'exit', 'graceful', 'log')])
(channel, set())
for channel in channels
)
self._priorities = {}
def subscribe(self, channel, callback, priority=None):
"""Add the given callback at the given channel (if not present)."""
if channel not in self.listeners:
self.listeners[channel] = set()
self.listeners[channel].add(callback)
ch_listeners = self.listeners.setdefault(channel, set())
ch_listeners.add(callback)
if priority is None:
priority = getattr(callback, 'priority', 50)
self._priorities[(channel, callback)] = priority
def unsubscribe(self, channel, callback):
"""Discard the given callback (if present)."""
listeners = self.listeners.get(channel)
if listeners and callback in listeners:
listeners.discard(callback)
del self._priorities[(channel, callback)]
def publish(self, channel, *args, **kwargs):
"""Return output of all subscribers for the given channel."""
if channel not in self.listeners:
return []
exc = None
exc = ChannelFailures()
output = []
items = [(self._priorities[(channel, listener)], listener)
for listener in self.listeners[channel]]
items.sort()
raw_items = (
(self._priorities[(channel, listener)], listener)
for listener in self.listeners[channel]
)
items = sorted(raw_items, key=operator.itemgetter(0))
for priority, listener in items:
try:
output.append(listener(*args, **kwargs))
except KeyboardInterrupt:
raise
except SystemExit, e:
except SystemExit:
e = sys.exc_info()[1]
# If we have previous errors ensure the exit code is non-zero
if exc and e.code == 0:
e.code = 1
raise
except:
exc = sys.exc_info()[1]
exc.handle_exception()
if channel == 'log':
# Assume any further messages to 'log' will fail.
pass
else:
self.log("Error in %r listener %r" % (channel, listener),
self.log('Error in %r listener %r' % (channel, listener),
level=40, traceback=True)
if exc:
raise
raise exc
return output
def _clean_exit(self):
"""An atexit handler which asserts the Bus is not running."""
if self.state != states.EXITING:
warnings.warn(
"The main thread is exiting, but the Bus is in the %r state; "
"shutting it down automatically now. You must either call "
"bus.block() after start(), or call bus.exit() before the "
"main thread exits." % self.state, RuntimeWarning)
'The main thread is exiting, but the Bus is in the %r state; '
'shutting it down automatically now. You must either call '
'bus.block() after start(), or call bus.exit() before the '
'main thread exits.' % self.state, RuntimeWarning)
self.exit()
def start(self):
"""Start all services."""
atexit.register(self._clean_exit)
self.state = states.STARTING
self.log('Bus STARTING')
try:
@@ -187,21 +248,23 @@ class Bus(object):
except (KeyboardInterrupt, SystemExit):
raise
except:
self.log("Shutting down due to error in start listener:",
self.log('Shutting down due to error in start listener:',
level=40, traceback=True)
e_info = sys.exc_info()
e_info = sys.exc_info()[1]
try:
self.exit()
except:
# Any stop/exit errors will be logged inside publish().
pass
raise e_info[0], e_info[1], e_info[2]
# Re-raise the original error
raise e_info
def exit(self):
"""Stop all services and prepare to exit the process."""
exitstate = self.state
try:
self.stop()
self.state = states.EXITING
self.log('Bus EXITING')
self.publish('exit')
@@ -213,25 +276,32 @@ class Bus(object):
# signal handler, console handler, or atexit handler), so we
# can't just let exceptions propagate out unhandled.
# Assume it's been logged and just die.
os._exit(70) # EX_SOFTWARE
os._exit(70) # EX_SOFTWARE
if exitstate == states.STARTING:
# exit() was called before start() finished, possibly due to
# Ctrl-C because a start listener got stuck. In this case,
# we could get stuck in a loop where Ctrl-C never exits the
# process, so we just call os.exit here.
os._exit(70) # EX_SOFTWARE
def restart(self):
"""Restart the process (may close connections).
This method does not restart the process from the calling thread;
instead, it stops the bus and asks the main thread to call execv.
"""
self.execv = True
self.exit()
def graceful(self):
"""Advise all services to reload."""
self.log('Bus graceful')
self.publish('graceful')
def block(self, interval=0.1):
"""Wait for the EXITING state, KeyboardInterrupt or SystemExit.
This function is intended to be called only by the main thread.
After waiting for the EXITING state, it also waits for all threads
to terminate, and then calls os.execv if self.execv is True. This
@@ -239,7 +309,7 @@ class Bus(object):
thread perform the actual execv call (required on some platforms).
"""
try:
self.wait(states.EXITING, interval=interval)
self.wait(states.EXITING, interval=interval, channel='main')
except (KeyboardInterrupt, IOError):
# The time.sleep call might raise
# "IOError: [Errno 4] Interrupted function call" on KBInt.
@@ -249,38 +319,48 @@ class Bus(object):
self.log('SystemExit raised: shutting down bus')
self.exit()
raise
# Waiting for ALL child threads to finish is necessary on OS X.
# See http://www.cherrypy.org/ticket/581.
# See https://github.com/cherrypy/cherrypy/issues/581.
# It's also good to let them all shut down before allowing
# the main thread to call atexit handlers.
# See http://www.cherrypy.org/ticket/751.
self.log("Waiting for child threads to terminate...")
# See https://github.com/cherrypy/cherrypy/issues/751.
self.log('Waiting for child threads to terminate...')
for t in threading.enumerate():
if t != threading.currentThread() and t.isAlive():
# Validate the we're not trying to join the MainThread
# that will cause a deadlock and the case exist when
# implemented as a windows service and in any other case
# that another thread executes cherrypy.engine.exit()
if (
t != threading.currentThread() and
t.isAlive() and
not isinstance(t, threading._MainThread)
):
# Note that any dummy (external) threads are always daemonic.
if hasattr(threading.Thread, "daemon"):
if hasattr(threading.Thread, 'daemon'):
# Python 2.6+
d = t.daemon
else:
d = t.isDaemon()
if not d:
self.log('Waiting for thread %s.' % t.getName())
t.join()
if self.execv:
self._do_execv()
def wait(self, state, interval=0.1):
"""Wait for the given state(s)."""
def wait(self, state, interval=0.1, channel=None):
"""Poll for the given state(s) at intervals; publish to channel."""
if isinstance(state, (tuple, list)):
states = state
else:
states = [state]
def _wait():
while self.state not in states:
time.sleep(interval)
self.publish(channel)
# From http://psyco.sourceforge.net/psycoguide/bugs.html:
# "The compiled machine code does not include the regular polling
# done by Python, meaning that a KeyboardInterrupt will not be
@@ -291,23 +371,112 @@ class Bus(object):
sys.modules['psyco'].cannotcompile(_wait)
except (KeyError, AttributeError):
pass
_wait()
def _do_execv(self):
"""Re-execute the current process.
This must be called from the main thread, because certain platforms
(OS X) don't allow execv to be called in a child thread very well.
"""
args = sys.argv[:]
try:
args = self._get_true_argv()
except NotImplementedError:
"""It's probably win32"""
# For the SABnzbd.exe binary we don't want interpreter flags
# https://github.com/cherrypy/cherrypy/issues/1526
if getattr(sys, 'frozen', False):
args = [sys.executable] + sys.argv
else:
args = [sys.executable] + _args_from_interpreter_flags() + sys.argv
self.log('Re-spawning %s' % ' '.join(args))
args.insert(0, sys.executable)
if sys.platform == 'win32':
args = ['"%s"' % arg for arg in args]
os.execv(sys.executable, args)
self._extend_pythonpath(os.environ)
if sys.platform[:4] == 'java':
from _systemrestart import SystemRestart
raise SystemRestart
else:
if sys.platform == 'win32':
args = ['"%s"' % arg for arg in args]
os.chdir(_startup_cwd)
if self.max_cloexec_files:
self._set_cloexec()
os.execv(sys.executable, args)
@staticmethod
def _get_true_argv():
"""Retrieves all real arguments of the python interpreter
...even those not listed in ``sys.argv``
:seealso: http://stackoverflow.com/a/28338254/595220
:seealso: http://stackoverflow.com/a/6683222/595220
:seealso: http://stackoverflow.com/a/28414807/595220
"""
try:
char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p
argv = ctypes.POINTER(char_p)()
argc = ctypes.c_int()
ctypes.pythonapi.Py_GetArgcArgv(ctypes.byref(argc), ctypes.byref(argv))
except AttributeError:
"""It looks Py_GetArgcArgv is completely absent in MS Windows
:seealso: https://github.com/cherrypy/cherrypy/issues/1506
:ref: https://chromium.googlesource.com/infra/infra/+/69eb0279c12bcede5937ce9298020dd4581e38dd%5E!/
"""
raise NotImplementedError
else:
return argv[:argc.value]
@staticmethod
def _extend_pythonpath(env):
"""
If sys.path[0] is an empty string, the interpreter was likely
invoked with -m and the effective path is about to change on
re-exec. Add the current directory to $PYTHONPATH to ensure
that the new process sees the same path.
This issue cannot be addressed in the general case because
Python cannot reliably reconstruct the
original command line (http://bugs.python.org/issue14208).
(This idea filched from tornado.autoreload)
"""
path_prefix = '.' + os.pathsep
existing_path = env.get('PYTHONPATH', '')
needs_patch = (
sys.path[0] == '' and
not existing_path.startswith(path_prefix)
)
if needs_patch:
env['PYTHONPATH'] = path_prefix + existing_path
def _set_cloexec(self):
"""Set the CLOEXEC flag on all open files (except stdin/out/err).
If self.max_cloexec_files is an integer (the default), then on
platforms which support it, it represents the max open files setting
for the operating system. This function will be called just before
the process is restarted via os.execv() to prevent open files
from persisting into the new process.
Set self.max_cloexec_files to 0 to disable this behavior.
"""
for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
try:
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
except IOError:
continue
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
def stop(self):
"""Stop all services."""
self.state = states.STOPPING
@@ -315,7 +484,7 @@ class Bus(object):
self.publish('stop')
self.state = states.STOPPED
self.log('Bus STOPPED')
def start_with_callback(self, func, args=None, kwargs=None):
"""Start 'func' in a new thread T, then start self (and return T)."""
if args is None:
@@ -323,23 +492,28 @@ class Bus(object):
if kwargs is None:
kwargs = {}
args = (func,) + args
def _callback(func, *a, **kw):
self.wait(states.STARTED)
func(*a, **kw)
t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
t.setName('Bus Callback ' + t.getName())
t.start()
self.start()
return t
def log(self, msg="", level=20, traceback=False):
def log(self, msg='', level=20, traceback=False):
"""Log the given message. Append the last traceback if requested."""
if traceback:
exc = sys.exc_info()
msg += "\n" + "".join(_traceback.format_exception(*exc))
# Work-around for bug in Python's traceback implementation
# which crashes when the error message contains %1, %2 etc.
errors = sys.exc_info()
if '%' in errors[1].message:
errors[1].message = errors[1].message.replace('%', '#')
errors[1].args = [item.replace('%', '#') for item in errors[1].args]
msg += "\n" + "".join(_traceback.format_exception(*errors))
self.publish('log', msg, level)
bus = Bus()

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,118 @@
"""A library for integrating Python's builtin ``ssl`` library with CherryPy.
The ssl module must be importable for SSL functionality.
To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of
``BuiltinSSLAdapter``.
"""
try:
import ssl
except ImportError:
ssl = None
try:
from _pyio import DEFAULT_BUFFER_SIZE
except ImportError:
try:
from io import DEFAULT_BUFFER_SIZE
except ImportError:
DEFAULT_BUFFER_SIZE = -1
import sys
from cherrypy import wsgiserver
class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
"""A wrapper for integrating Python's builtin ssl module with CherryPy."""
certificate = None
"""The filename of the server SSL certificate."""
private_key = None
"""The filename of the server's private key file."""
certificate_chain = None
"""The filename of the certificate chain file."""
"""The ssl.SSLContext that will be used to wrap sockets where available
(on Python > 2.7.9 / 3.3)
"""
context = None
def __init__(self, certificate, private_key, certificate_chain=None):
if ssl is None:
raise ImportError('You must install the ssl module to use HTTPS.')
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
if hasattr(ssl, 'create_default_context'):
self.context = ssl.create_default_context(
purpose=ssl.Purpose.CLIENT_AUTH,
cafile=certificate_chain
)
self.context.load_cert_chain(certificate, private_key)
def bind(self, sock):
"""Wrap and return the given socket."""
return sock
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
try:
if self.context is not None:
s = self.context.wrap_socket(sock,do_handshake_on_connect=True,
server_side=True)
else:
s = ssl.wrap_socket(sock, do_handshake_on_connect=True,
server_side=True, certfile=self.certificate,
keyfile=self.private_key,
ssl_version=ssl.PROTOCOL_SSLv23,
ca_certs=self.certificate_chain)
except ssl.SSLError:
e = sys.exc_info()[1]
if e.errno == ssl.SSL_ERROR_EOF:
# This is almost certainly due to the cherrypy engine
# 'pinging' the socket to assert it's connectable;
# the 'ping' isn't SSL.
return None, {}
elif e.errno == ssl.SSL_ERROR_SSL:
if 'http request' in e.args[1]:
# The client is speaking HTTP to an HTTPS server.
raise wsgiserver.NoSSLError
# Check if it's one of the known errors
# Errors that are caught by PyOpenSSL, but thrown by built-in ssl
_block_errors = ('unknown protocol', 'unknown ca', 'unknown_ca',
'inappropriate fallback', 'wrong version number',
'no shared cipher', 'certificate unknown', 'ccs received early')
for error_text in _block_errors:
if error_text in e.args[1].lower():
# Accepted error, let's pass
return None, {}
elif 'handshake operation timed out' in e.args[0]:
# This error is thrown by builtin SSL after a timeout
# when client is speaking HTTP to an HTTPS server.
# The connection can safely be dropped.
return None, {}
raise
return s, self.get_environ(s)
# TODO: fill this out more with mod ssl env
def get_environ(self, sock):
"""Create WSGI environ entries to be merged into each request."""
cipher = sock.cipher()
ssl_environ = {
'wsgi.url_scheme': 'https',
'HTTPS': 'on',
'SSL_PROTOCOL': cipher[1],
'SSL_CIPHER': cipher[0]
# SSL_VERSION_INTERFACE string The mod_ssl program version
# SSL_VERSION_LIBRARY string The OpenSSL program version
}
return ssl_environ
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
return wsgiserver.CP_makefile(sock, mode, bufsize)

View File

@@ -0,0 +1,253 @@
"""A library for integrating pyOpenSSL with CherryPy.
The OpenSSL module must be importable for SSL functionality.
You can obtain it from `here <https://launchpad.net/pyopenssl>`_.
To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of
SSLAdapter. There are two ways to use SSL:
Method One
----------
* ``ssl_adapter.context``: an instance of SSL.Context.
If this is not None, it is assumed to be an SSL.Context instance,
and will be passed to SSL.Connection on bind(). The developer is
responsible for forming a valid Context object. This approach is
to be preferred for more flexibility, e.g. if the cert and key are
streams instead of files, or need decryption, or SSL.SSLv3_METHOD
is desired instead of the default SSL.SSLv23_METHOD, etc. Consult
the pyOpenSSL documentation for complete options.
Method Two (shortcut)
---------------------
* ``ssl_adapter.certificate``: the filename of the server SSL certificate.
* ``ssl_adapter.private_key``: the filename of the server's private key file.
Both are None by default. If ssl_adapter.context is None, but .private_key
and .certificate are both given and valid, they will be read, and the
context will be automatically created from them.
"""
import socket
import threading
import time
from cherrypy import wsgiserver
try:
from OpenSSL import SSL
from OpenSSL import crypto
except ImportError:
SSL = None
class SSL_fileobject(wsgiserver.CP_makefile):
"""SSL file object attached to a socket object."""
ssl_timeout = 3
ssl_retry = .01
def _safe_call(self, is_reader, call, *args, **kwargs):
"""Wrap the given call with SSL error-trapping.
is_reader: if False EOF errors will be raised. If True, EOF errors
will return "" (to emulate normal sockets).
"""
start = time.time()
while True:
try:
return call(*args, **kwargs)
except SSL.WantReadError:
# Sleep and try again. This is dangerous, because it means
# the rest of the stack has no way of differentiating
# between a "new handshake" error and "client dropped".
# Note this isn't an endless loop: there's a timeout below.
time.sleep(self.ssl_retry)
except SSL.WantWriteError:
time.sleep(self.ssl_retry)
except SSL.SysCallError as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ''
errnum = e.args[0]
if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
return ''
raise socket.error(errnum)
except SSL.Error as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ''
thirdarg = None
try:
thirdarg = e.args[0][0][2]
except IndexError:
pass
if thirdarg == 'http request':
# The client is talking HTTP to an HTTPS server.
raise wsgiserver.NoSSLError()
raise wsgiserver.FatalSSLAlert(*e.args)
except:
raise
if time.time() - start > self.ssl_timeout:
raise socket.timeout('timed out')
def recv(self, size):
return self._safe_call(True, super(SSL_fileobject, self).recv, size)
def sendall(self, *args, **kwargs):
return self._safe_call(False, super(SSL_fileobject, self).sendall,
*args, **kwargs)
def send(self, *args, **kwargs):
return self._safe_call(False, super(SSL_fileobject, self).send,
*args, **kwargs)
class SSLConnection:
"""A thread-safe wrapper for an SSL.Connection.
``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
"""
def __init__(self, *args):
self._ssl_conn = SSL.Connection(*args)
self._lock = threading.RLock()
for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
'renegotiate', 'bind', 'listen', 'connect', 'accept',
'setblocking', 'fileno', 'close', 'get_cipher_list',
'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
'makefile', 'get_app_data', 'set_app_data', 'state_string',
'sock_shutdown', 'get_peer_certificate', 'want_read',
'want_write', 'set_connect_state', 'set_accept_state',
'connect_ex', 'sendall', 'settimeout', 'gettimeout'):
exec("""def %s(self, *args):
self._lock.acquire()
try:
return self._ssl_conn.%s(*args)
finally:
self._lock.release()
""" % (f, f))
def shutdown(self, *args):
self._lock.acquire()
try:
# pyOpenSSL.socket.shutdown takes no args
return self._ssl_conn.shutdown()
finally:
self._lock.release()
class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
"""A wrapper for integrating pyOpenSSL with CherryPy."""
context = None
"""An instance of SSL.Context."""
certificate = None
"""The filename of the server SSL certificate."""
private_key = None
"""The filename of the server's private key file."""
certificate_chain = None
"""Optional. The filename of CA's intermediate certificate bundle.
This is needed for cheaper "chained root" SSL certificates, and should be
left as None if not required."""
def __init__(self, certificate, private_key, certificate_chain=None):
if SSL is None:
raise ImportError('You must install pyOpenSSL to use HTTPS.')
self.context = None
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
self._environ = None
def bind(self, sock):
"""Wrap and return the given socket."""
if self.context is None:
self.context = self.get_context()
conn = SSLConnection(self.context, sock)
self._environ = self.get_environ()
return conn
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
return sock, self._environ.copy()
def get_context(self):
"""Return an SSL.Context from self attributes."""
# See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
c = SSL.Context(SSL.SSLv23_METHOD)
c.use_privatekey_file(self.private_key)
if self.certificate_chain:
c.load_verify_locations(self.certificate_chain)
c.use_certificate_file(self.certificate)
return c
def get_environ(self):
"""Return WSGI environ entries to be merged into each request."""
ssl_environ = {
'HTTPS': 'on',
# pyOpenSSL doesn't provide access to any of these AFAICT
# 'SSL_PROTOCOL': 'SSLv2',
# SSL_CIPHER string The cipher specification name
# SSL_VERSION_INTERFACE string The mod_ssl program version
# SSL_VERSION_LIBRARY string The OpenSSL program version
}
if self.certificate:
# Server certificate attributes
cert = open(self.certificate, 'rb').read()
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
ssl_environ.update({
'SSL_SERVER_M_VERSION': cert.get_version(),
'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
# 'SSL_SERVER_V_START':
# Validity of server's certificate (start time),
# 'SSL_SERVER_V_END':
# Validity of server's certificate (end time),
})
for prefix, dn in [('I', cert.get_issuer()),
('S', cert.get_subject())]:
# X509Name objects don't seem to have a way to get the
# complete DN string. Use str() and slice it instead,
# because str(dn) == "<X509Name object '/C=US/ST=...'>"
dnstr = str(dn)[18:-2]
wsgikey = 'SSL_SERVER_%s_DN' % prefix
ssl_environ[wsgikey] = dnstr
# The DN should be of the form: /k1=v1/k2=v2, but we must allow
# for any value to contain slashes itself (in a URL).
while dnstr:
pos = dnstr.rfind('=')
dnstr, value = dnstr[:pos], dnstr[pos + 1:]
pos = dnstr.rfind('/')
dnstr, key = dnstr[:pos], dnstr[pos + 1:]
if key and value:
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
ssl_environ[wsgikey] = value
return ssl_environ
def makefile(self, sock, mode='r', bufsize=-1):
if SSL and isinstance(sock, SSL.ConnectionType):
timeout = sock.gettimeout()
f = SSL_fileobject(sock, mode, bufsize)
f.ssl_timeout = timeout
return f
else:
return wsgiserver.CP_fileobject(sock, mode, bufsize)

View File

@@ -0,0 +1,16 @@
import six
import mock
from cherrypy import wsgiserver
class TestWSGIGateway_u0:
@mock.patch('cherrypy.wsgiserver.WSGIGateway_10.get_environ',
lambda self: {'foo': 'bar'})
def test_decodes_items(self):
req = mock.MagicMock(path=b'/', qs=b'')
gw = wsgiserver.WSGIGateway_u0(req=req)
env = gw.get_environ()
assert env['foo'] == 'bar'
assert isinstance(env['foo'], six.text_type)

View File

@@ -1,332 +1,402 @@
import re
import hashlib
import time
import platform
import StringIO
__version__ = '0.8'
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
GNTP_INFO_LINE = re.compile(
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
re.IGNORECASE
)
GNTP_INFO_LINE_SHORT = re.compile(
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
re.IGNORECASE
)
GNTP_HEADER = re.compile('([\w-]+):(.+)')
GNTP_EOL = '\r\n'
__version__ = '0.4'
class BaseError(Exception):
pass
def gntp_error(self):
error = GNTPError(self.errorcode, self.errordesc)
return error.encode()
class ParseError(BaseError):
def gntp_error(self):
error = GNTPError(errorcode=500,errordesc='Error parsing the message')
return error.encode()
errorcode = 500
errordesc = 'Error parsing the message'
class AuthError(BaseError):
def gntp_error(self):
error = GNTPError(errorcode=400,errordesc='Error with authorization')
return error.encode()
errorcode = 400
errordesc = 'Error with authorization'
class UnsupportedError(BaseError):
def gntp_error(self):
error = GNTPError(errorcode=500,errordesc='Currently unsupported by gntp.py')
return error.encode()
errorcode = 500
errordesc = 'Currently unsupported by gntp.py'
class _GNTPBuffer(StringIO.StringIO):
"""GNTP Buffer class"""
def writefmt(self, message="", *args):
"""Shortcut function for writing GNTP Headers"""
self.write((message % args).encode('utf8', 'replace'))
self.write(GNTP_EOL)
class _GNTPBase(object):
info = {
'version':'1.0',
'messagetype':None,
'encryptionAlgorithmID':None
}
_requiredHeaders = []
headers = {}
resources = {}
def add_origin_info(self):
self.add_header('Origin-Machine-Name',platform.node())
self.add_header('Origin-Software-Name','gntp.py')
self.add_header('Origin-Software-Version',__version__)
self.add_header('Origin-Platform-Name',platform.system())
self.add_header('Origin-Platform-Version',platform.platform())
"""Base initilization
:param string messagetype: GNTP Message type
:param string version: GNTP Protocol version
:param string encription: Encryption protocol
"""
def __init__(self, messagetype=None, version='1.0', encryption=None):
self.info = {
'version': version,
'messagetype': messagetype,
'encryptionAlgorithmID': encryption
}
self.headers = {}
self.resources = {}
def __str__(self):
return self.encode()
def _parse_info(self,data):
'''
Parse the first line of a GNTP message to get security and other info values
@param data: GNTP Message
@return: GNTP Message information in a dictionary
'''
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)'+
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?'+
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n', data,re.IGNORECASE)
def _parse_info(self, data):
"""Parse the first line of a GNTP message to get security and other info values
:param string data: GNTP Message
:return dict: Parsed GNTP Info line
"""
match = GNTP_INFO_LINE.match(data)
if not match:
raise ParseError('ERROR_PARSING_INFO_LINE')
info = match.groupdict()
if info['encryptionAlgorithmID'] == 'NONE':
info['encryptionAlgorithmID'] = None
return info
def set_password(self,password,encryptAlgo='MD5'):
'''
Set a password for a GNTP Message
@param password: Null to clear password
@param encryptAlgo: Supports MD5,SHA1,SHA256,SHA512
@todo: Support other hash functions
'''
def set_password(self, password, encryptAlgo='MD5'):
"""Set a password for a GNTP Message
:param string password: Null to clear password
:param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
"""
hash = {
'MD5': hashlib.md5,
'SHA1': hashlib.sha1,
'SHA256': hashlib.sha256,
'SHA512': hashlib.sha512,
}
self.password = password
self.encryptAlgo = encryptAlgo.upper()
if not password:
self.info['encryptionAlgorithmID'] = None
self.info['keyHashAlgorithm'] = None;
self.info['keyHashAlgorithm'] = None
return
if not self.encryptAlgo in hash.keys():
raise UnsupportedError('INVALID HASH "%s"'%self.encryptAlgo)
raise UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
hashfunction = hash.get(self.encryptAlgo)
password = password.encode('utf8')
seed = time.ctime()
salt = hashfunction(seed).hexdigest()
saltHash = hashfunction(seed).digest()
keyBasis = password+saltHash
keyBasis = password + saltHash
key = hashfunction(keyBasis).digest()
keyHash = hashfunction(key).hexdigest()
self.info['keyHashAlgorithmID'] = self.encryptAlgo
self.info['keyHash'] = keyHash.upper()
self.info['salt'] = salt.upper()
def _decode_hex(self,value):
'''
Helper function to decode hex string to `proper` hex string
@param value: Value to decode
@return: Hex string
'''
def _decode_hex(self, value):
"""Helper function to decode hex string to `proper` hex string
:param string value: Human readable hex string
:return string: Hex string
"""
result = ''
for i in range(0,len(value),2):
tmp = int(value[i:i+2],16)
for i in range(0, len(value), 2):
tmp = int(value[i:i + 2], 16)
result += chr(tmp)
return result
def _decode_binary(self,rawIdentifier,identifier):
def _decode_binary(self, rawIdentifier, identifier):
rawIdentifier += '\r\n\r\n'
dataLength = int(identifier['Length'])
pointerStart = self.raw.find(rawIdentifier)+len(rawIdentifier)
pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
pointerEnd = pointerStart + dataLength
data = self.raw[pointerStart:pointerEnd]
if not len(data) == dataLength:
raise ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s'%(dataLength,len(data)))
raise ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
return data
def _validate_password(self,password):
'''
Validate GNTP Message against stored password
'''
def _validate_password(self, password):
"""Validate GNTP Message against stored password"""
self.password = password
if password == None: raise Exception()
keyHash = self.info.get('keyHash',None)
if password == None:
raise AuthError('Missing password')
keyHash = self.info.get('keyHash', None)
if keyHash is None and self.password is None:
return True
if keyHash is None:
raise AuthError('Invalid keyHash')
if self.password is None:
raise AuthError('Missing password')
password = self.password.encode('utf8')
saltHash = self._decode_hex(self.info['salt'])
keyBasis = password+saltHash
keyBasis = password + saltHash
key = hashlib.md5(keyBasis).digest()
keyHash = hashlib.md5(key).hexdigest()
if not keyHash.upper() == self.info['keyHash'].upper():
raise AuthError('Invalid Hash')
return True
def validate(self):
'''
Verify required headers
'''
"""Verify required headers"""
for header in self._requiredHeaders:
if not self.headers.get(header,False):
raise ParseError('Missing Notification Header: '+header)
if not self.headers.get(header, False):
raise ParseError('Missing Notification Header: ' + header)
def _format_info(self):
'''
Generate info line for GNTP Message
@return: Info line string
'''
info = u'GNTP/%s %s'%(
"""Generate info line for GNTP Message
:return string:
"""
info = u'GNTP/%s %s' % (
self.info.get('version'),
self.info.get('messagetype'),
)
if self.info.get('encryptionAlgorithmID',None):
info += ' %s:%s'%(
if self.info.get('encryptionAlgorithmID', None):
info += ' %s:%s' % (
self.info.get('encryptionAlgorithmID'),
self.info.get('ivValue'),
)
else:
info+=' NONE'
if self.info.get('keyHashAlgorithmID',None):
info += ' %s:%s.%s'%(
info += ' NONE'
if self.info.get('keyHashAlgorithmID', None):
info += ' %s:%s.%s' % (
self.info.get('keyHashAlgorithmID'),
self.info.get('keyHash'),
self.info.get('salt')
)
return info
def _parse_dict(self,data):
'''
Helper function to parse blocks of GNTP headers into a dictionary
@param data:
@return: Dictionary of headers
'''
)
return info
def _parse_dict(self, data):
"""Helper function to parse blocks of GNTP headers into a dictionary
:param string data:
:return dict:
"""
dict = {}
for line in data.split('\r\n'):
match = re.match('([\w-]+):(.+)', line)
if not match: continue
key = match.group(1).strip()
val = match.group(2).strip()
match = GNTP_HEADER.match(line)
if not match:
continue
key = unicode(match.group(1).strip(), 'utf8', 'replace')
val = unicode(match.group(2).strip(), 'utf8', 'replace')
dict[key] = val
return dict
def add_header(self,key,value):
def add_header(self, key, value):
if isinstance(value, unicode):
self.headers[key] = value
else:
self.headers[key] = unicode('%s'%value,'utf8','replace')
def decode(self,data,password=None):
'''
Decode GNTP Message
@param data:
'''
self.headers[key] = unicode('%s' % value, 'utf8', 'replace')
def add_resource(self, data):
"""Add binary resource
:param string data: Binary Data
"""
identifier = hashlib.md5(data).hexdigest()
self.resources[identifier] = data
return 'x-growl-resource://%s' % identifier
def decode(self, data, password=None):
"""Decode GNTP Message
:param string data:
"""
self.password = password
self.raw = data
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(data)
self.headers = self._parse_dict(parts[0])
def encode(self):
'''
Encode a GNTP Message
@return: GNTP Message ready to be sent
'''
self.validate()
EOL = u'\r\n'
message = self._format_info() + EOL
"""Encode a generic GNTP Message
:return string: GNTP Message ready to be sent
"""
buffer = _GNTPBuffer()
buffer.writefmt(self._format_info())
#Headers
for k,v in self.headers.iteritems():
message += u'%s: %s%s'%(k,v,EOL)
message += EOL
return message
for k, v in self.headers.iteritems():
buffer.writefmt('%s: %s', k, v)
buffer.writefmt()
#Resources
for resource, data in self.resources.iteritems():
buffer.writefmt('Identifier: %s', resource)
buffer.writefmt('Length: %d', len(data))
buffer.writefmt()
buffer.write(data)
buffer.writefmt()
buffer.writefmt()
return buffer.getvalue()
class GNTPRegister(_GNTPBase):
"""Represents a GNTP Registration Command"""
notifications = []
"""Represents a GNTP Registration Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notifications-Count'
]
_requiredNotificationHeaders = ['Notification-Name']
def __init__(self,data=None,password=None):
'''
@param data: (Optional) See decode()
@param password: (Optional) Password to use while encoding/decoding messages
'''
self.info['messagetype'] = 'REGISTER'
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'REGISTER')
self.notifications = []
if data:
self.decode(data,password)
self.decode(data, password)
else:
self.set_password(password)
self.add_header('Application-Name', 'pygntp')
self.add_header('Notifications-Count', 0)
self.add_origin_info()
def validate(self):
'''
Validate required headers and validate notification headers
'''
'''Validate required headers and validate notification headers'''
for header in self._requiredHeaders:
if not self.headers.get(header,False):
raise ParseError('Missing Registration Header: '+header)
if not self.headers.get(header, False):
raise ParseError('Missing Registration Header: ' + header)
for notice in self.notifications:
for header in self._requiredNotificationHeaders:
if not notice.get(header,False):
raise ParseError('Missing Notification Header: '+header)
def decode(self,data,password):
'''
Decode existing GNTP Registration message
@param data: Message to decode.
'''
if not notice.get(header, False):
raise ParseError('Missing Notification Header: ' + header)
def decode(self, data, password):
"""Decode existing GNTP Registration message
:param string data: Message to decode
"""
self.raw = data
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(data)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i,part in enumerate(parts):
if i==0: continue #Skip Header
if part.strip()=='': continue
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Notification-Name',False):
if notice.get('Notification-Name', False):
self.notifications.append(notice)
elif notice.get('Identifier',False):
notice['Data'] = self._decode_binary(part,notice)
elif notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('register.png','wblol').write(notice['Data'])
self.resources[ notice.get('Identifier') ] = notice
def add_notification(self,name,enabled=True):
'''
Add new Notification to Registration message
@param name: Notification Name
@param enabled: Default Notification to Enabled
'''
self.resources[notice.get('Identifier')] = notice
def add_notification(self, name, enabled=True):
"""Add new Notification to Registration message
:param string name: Notification Name
:param boolean enabled: Enable this notification by default
"""
notice = {}
notice['Notification-Name'] = u'%s'%name
notice['Notification-Enabled'] = u'%s'%enabled
notice['Notification-Name'] = u'%s' % name
notice['Notification-Enabled'] = u'%s' % enabled
self.notifications.append(notice)
self.add_header('Notifications-Count', len(self.notifications))
def encode(self):
'''
Encode a GNTP Registration Message
@return: GNTP Registration Message ready to be sent
'''
self.validate()
EOL = u'\r\n'
message = self._format_info() + EOL
"""Encode a GNTP Registration Message
:return string: Encoded GNTP Registration message
"""
buffer = _GNTPBuffer()
buffer.writefmt(self._format_info())
#Headers
for k,v in self.headers.iteritems():
message += u'%s: %s%s'%(k,v,EOL)
for k, v in self.headers.iteritems():
buffer.writefmt('%s: %s', k, v)
buffer.writefmt()
#Notifications
if len(self.notifications)>0:
if len(self.notifications) > 0:
for notice in self.notifications:
message += EOL
for k,v in notice.iteritems():
message += u'%s: %s%s'%(k,v,EOL)
message += EOL
return message
for k, v in notice.iteritems():
buffer.writefmt('%s: %s', k, v)
buffer.writefmt()
#Resources
for resource, data in self.resources.iteritems():
buffer.writefmt('Identifier: %s', resource)
buffer.writefmt('Length: %d', len(data))
buffer.writefmt()
buffer.write(data)
buffer.writefmt()
buffer.writefmt()
return buffer.getvalue()
class GNTPNotice(_GNTPBase):
"""Represents a GNTP Notification Command"""
"""Represents a GNTP Notification Command
:param string data: (Optional) See decode()
:param string app: (Optional) Set Application-Name
:param string name: (Optional) Set Notification-Name
:param string title: (Optional) Set Notification Title
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notification-Name',
'Notification-Title'
]
def __init__(self,data=None,app=None,name=None,title=None,password=None):
'''
@param data: (Optional) See decode()
@param app: (Optional) Set Application-Name
@param name: (Optional) Set Notification-Name
@param title: (Optional) Set Notification Title
@param password: (Optional) Password to use while encoding/decoding messages
'''
self.info['messagetype'] = 'NOTIFY'
def __init__(self, data=None, app=None, name=None, title=None, password=None):
_GNTPBase.__init__(self, 'NOTIFY')
if data:
self.decode(data,password)
self.decode(data, password)
else:
self.set_password(password)
if app:
@@ -335,105 +405,103 @@ class GNTPNotice(_GNTPBase):
self.add_header('Notification-Name', name)
if title:
self.add_header('Notification-Title', title)
self.add_origin_info()
def decode(self,data,password):
'''
Decode existing GNTP Notification message
@param data: Message to decode.
'''
def decode(self, data, password):
"""Decode existing GNTP Notification message
:param string data: Message to decode.
"""
self.raw = data
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(data)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i,part in enumerate(parts):
if i==0: continue #Skip Header
if part.strip()=='': continue
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Identifier',False):
notice['Data'] = self._decode_binary(part,notice)
if notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('notice.png','wblol').write(notice['Data'])
self.resources[ notice.get('Identifier') ] = notice
def encode(self):
'''
Encode a GNTP Notification Message
@return: GNTP Notification Message ready to be sent
'''
self.validate()
EOL = u'\r\n'
message = self._format_info() + EOL
#Headers
for k,v in self.headers.iteritems():
message += u'%s: %s%s'%(k,v,EOL)
message += EOL
return message
self.resources[notice.get('Identifier')] = notice
class GNTPSubscribe(_GNTPBase):
"""Represents a GNTP Subscribe Command"""
def __init__(self,data=None,password=None):
self.info['messagetype'] = 'SUBSCRIBE'
self._requiredHeaders = [
'Subscriber-ID',
'Subscriber-Name',
]
"""Represents a GNTP Subscribe Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Subscriber-ID',
'Subscriber-Name',
]
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'SUBSCRIBE')
if data:
self.decode(data,password)
self.decode(data, password)
else:
self.set_password(password)
self.add_origin_info()
class GNTPOK(_GNTPBase):
"""Represents a GNTP OK Response"""
"""Represents a GNTP OK Response
:param string data: (Optional) See _GNTPResponse.decode()
:param string action: (Optional) Set type of action the OK Response is for
"""
_requiredHeaders = ['Response-Action']
def __init__(self,data=None,action=None):
'''
@param data: (Optional) See _GNTPResponse.decode()
@param action: (Optional) Set type of action the OK Response is for
'''
self.info['messagetype'] = '-OK'
def __init__(self, data=None, action=None):
_GNTPBase.__init__(self, '-OK')
if data:
self.decode(data)
if action:
self.add_header('Response-Action', action)
self.add_origin_info()
class GNTPError(_GNTPBase):
_requiredHeaders = ['Error-Code','Error-Description']
def __init__(self,data=None,errorcode=None,errordesc=None):
'''
@param data: (Optional) See _GNTPResponse.decode()
@param errorcode: (Optional) Error code
@param errordesc: (Optional) Error Description
'''
self.info['messagetype'] = '-ERROR'
"""Represents a GNTP Error response
:param string data: (Optional) See _GNTPResponse.decode()
:param string errorcode: (Optional) Error code
:param string errordesc: (Optional) Error Description
"""
_requiredHeaders = ['Error-Code', 'Error-Description']
def __init__(self, data=None, errorcode=None, errordesc=None):
_GNTPBase.__init__(self, '-ERROR')
if data:
self.decode(data)
if errorcode:
self.add_header('Error-Code', errorcode)
self.add_header('Error-Description', errordesc)
self.add_origin_info()
def error(self):
return self.headers['Error-Code'],self.headers['Error-Description']
def parse_gntp(data,password=None):
'''
Attempt to parse a message as a GNTP message
@param data: Message to be parsed
@param password: Optional password to be used to verify the message
'''
match = re.match('GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',data,re.IGNORECASE)
def error(self):
return (self.headers.get('Error-Code', None),
self.headers.get('Error-Description', None))
def parse_gntp(data, password=None):
"""Attempt to parse a message as a GNTP message
:param string data: Message to be parsed
:param string password: Optional password to be used to verify the message
"""
match = GNTP_INFO_LINE_SHORT.match(data)
if not match:
raise ParseError('INVALID_GNTP_INFO')
info = match.groupdict()
if info['messagetype'] == 'REGISTER':
return GNTPRegister(data,password=password)
return GNTPRegister(data, password=password)
elif info['messagetype'] == 'NOTIFY':
return GNTPNotice(data,password=password)
return GNTPNotice(data, password=password)
elif info['messagetype'] == 'SUBSCRIBE':
return GNTPSubscribe(data,password=password)
return GNTPSubscribe(data, password=password)
elif info['messagetype'] == '-OK':
return GNTPOK(data)
elif info['messagetype'] == '-ERROR':

View File

@@ -12,10 +12,55 @@ using GNTP
import gntp
import socket
import logging
import platform
__all__ = [
'mini',
'GrowlNotifier',
]
logger = logging.getLogger(__name__)
def mini(description, applicationName='PythonMini', noteType="Message",
title="Mini Message", applicationIcon=None, hostname='localhost',
password=None, port=23053, sticky=False, priority=None,
callback=None, notificationIcon=None, identifier=None):
"""Single notification function
Simple notification function in one line. Has only one required parameter
and attempts to use reasonable defaults for everything else
:param string description: Notification message
.. warning::
For now, only URL callbacks are supported. In the future, the
callback argument will also support a function
"""
growl = GrowlNotifier(
applicationName=applicationName,
notifications=[noteType],
defaultNotifications=[noteType],
applicationIcon=applicationIcon,
hostname=hostname,
password=password,
port=port,
)
result = growl.register()
if result is not True:
return result
return growl.notify(
noteType=noteType,
title=title,
description=description,
icon=notificationIcon,
sticky=sticky,
priority=priority,
callback=callback,
identifier=identifier,
)
class GrowlNotifier(object):
"""Helper class to simplfy sending Growl messages
@@ -27,55 +72,35 @@ class GrowlNotifier(object):
:param string hostname: Remote host
:param integer port: Remote port
"""
applicationName = 'Python GNTP'
notifications = []
defaultNotifications = []
applicationIcon = None
passwordHash = 'MD5'
socketTimeout = 3
#GNTP Specific
password = None
hostname = 'localhost'
port = 23053
def __init__(self, applicationName='Python GNTP', notifications=[],
defaultNotifications=None, applicationIcon=None, hostname='localhost',
password=None, port=23053):
def __init__(self, applicationName=None, notifications=None, defaultNotifications=None, applicationIcon=None, hostname=None, password=None, port=None):
if applicationName:
self.applicationName = applicationName
assert self.applicationName, 'An application name is required.'
if notifications:
self.notifications = list(notifications)
assert self.notifications, 'A sequence of one or more notification names is required.'
if defaultNotifications is not None:
self.applicationName = applicationName
self.notifications = list(notifications)
if defaultNotifications:
self.defaultNotifications = list(defaultNotifications)
elif not self.defaultNotifications:
self.defaultNotifications = list(self.notifications)
else:
self.defaultNotifications = self.notifications
self.applicationIcon = applicationIcon
if applicationIcon is not None:
self.applicationIcon = self._checkIcon(applicationIcon)
elif self.applicationIcon is not None:
self.applicationIcon = self._checkIcon(self.applicationIcon)
#GNTP Specific
if password:
self.password = password
if hostname:
self.hostname = hostname
assert self.hostname, 'Requires valid hostname'
if port:
self.port = int(port)
assert isinstance(self.port, int), 'Requires valid port'
self.password = password
self.hostname = hostname
self.port = int(port)
def _checkIcon(self, data):
'''
Check the icon to see if it's valid
@param data:
@todo Consider checking for a valid URL
If it's a simple URL icon, then we return True. If it's a data icon
then we return False
'''
return data
logger.debug('Checking icon')
return data.startswith('http')
def register(self):
"""Send GNTP Registration
@@ -84,23 +109,26 @@ class GrowlNotifier(object):
Before sending notifications to Growl, you need to have
sent a registration message at least once
"""
logger.info('Sending registration to %s:%s', self.hostname, self.port)
logger.debug('Sending registration to %s:%s', self.hostname, self.port)
register = gntp.GNTPRegister()
register.add_header('Application-Name', self.applicationName)
for notification in self.notifications:
enabled = notification in self.defaultNotifications
register.add_notification(notification, enabled)
if self.applicationIcon:
register.add_header('Application-Icon', self.applicationIcon)
if self._checkIcon(self.applicationIcon):
register.add_header('Application-Icon', self.applicationIcon)
else:
id = register.add_resource(self.applicationIcon)
register.add_header('Application-Icon', id)
if self.password:
register.set_password(self.password, self.passwordHash)
response = self._send('register', register.encode())
if isinstance(response, gntp.GNTPOK):
return True
logger.error('Invalid response %s', response.error())
return response.error()
self.add_origin_info(register)
self.register_hook(register)
return self._send('register', register)
def notify(self, noteType, title, description, icon=None, sticky=False, priority=None):
def notify(self, noteType, title, description, icon=None, sticky=False,
priority=None, callback=None, identifier=None):
"""Send a GNTP notifications
.. warning::
@@ -112,8 +140,13 @@ class GrowlNotifier(object):
:param string icon: Icon URL path
:param boolean sticky: Sticky notification
:param integer priority: Message priority level from -2 to 2
:param string callback: URL callback
.. warning::
For now, only URL callbacks are supported. In the future, the
callback argument will also support a function
"""
logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
logger.debug('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
assert noteType in self.notifications
notice = gntp.GNTPNotice()
notice.add_header('Application-Name', self.applicationName)
@@ -126,14 +159,23 @@ class GrowlNotifier(object):
if priority:
notice.add_header('Notification-Priority', priority)
if icon:
notice.add_header('Notification-Icon', self._checkIcon(icon))
if self._checkIcon(icon):
notice.add_header('Notification-Icon', icon)
else:
id = notice.add_resource(icon)
notice.add_header('Notification-Icon', id)
if description:
notice.add_header('Notification-Text', description)
response = self._send('notify', notice.encode())
if isinstance(response, gntp.GNTPOK):
return True
logger.error('Invalid response %s', response.error())
return response.error()
if callback:
notice.add_header('Notification-Callback-Target', callback)
if identifier:
notice.add_header('Notification-Coalescing-ID', identifier)
self.add_origin_info(notice)
self.notify_hook(notice)
return self._send('notify', notice)
def subscribe(self, id, name, port):
"""Send a Subscribe request to a remote machine"""
@@ -143,30 +185,63 @@ class GrowlNotifier(object):
sub.add_header('Subscriber-Port', port)
if self.password:
sub.set_password(self.password, self.passwordHash)
response = self._send('subscribe', sub.encode())
if isinstance(response, gntp.GNTPOK):
return True
logger.error('Invalid response %s', response.error())
return response.error()
def _send(self, type, data):
self.add_origin_info(sub)
self.subscribe_hook(sub)
return self._send('subscribe', sub)
def add_origin_info(self, packet):
"""Add optional Origin headers to message"""
packet.add_header('Origin-Machine-Name', platform.node())
packet.add_header('Origin-Software-Name', 'gntp.py')
packet.add_header('Origin-Software-Version', gntp.__version__)
packet.add_header('Origin-Platform-Name', platform.system())
packet.add_header('Origin-Platform-Version', platform.platform())
def register_hook(self, packet):
pass
def notify_hook(self, packet):
pass
def subscribe_hook(self, packet):
pass
def _send(self, messagetype, packet):
"""Send the GNTP Packet"""
#logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, type, data)
#Less verbose please
logger.debug('To : %s:%s <%s>', self.hostname, self.port, type)
packet.validate()
data = packet.encode()
#logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
#Less verbose
logger.debug('To : %s:%s <%s>', self.hostname, self.port, packet.__class__)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(self.socketTimeout)
s.connect((self.hostname, self.port))
s.send(data.encode('utf8', 'replace'))
try:
s.settimeout(10)
except:
pass
response = gntp.parse_gntp(s.recv(1024))
s.send(data)
recv_data = s.recv(1024)
while not recv_data.endswith("\r\n\r\n"):
recv_data += s.recv(1024)
response = gntp.parse_gntp(recv_data)
s.close()
#logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
#Less verbose please
#Less verbose
logger.debug('From : %s:%s <%s>', self.hostname, self.port, response.__class__)
return response
if type(response) == gntp.GNTPOK:
return True
if response.error()[0] == '404' and 'disabled' in response.error()[1]:
# Ignore message saying that user has disabled this class
return True
logger.error('Invalid response: %s', response.error())
return response.error()
if __name__ == '__main__':
# If we're running this module directly we're likely running it as a test
# so extra debugging is useful
logging.basicConfig(level=logging.DEBUG)
mini('Testing mini notification')

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/sabnzbd16_32.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
icons/sabnzbd16_32green.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,22 +0,0 @@
#
# Copyright 2008-2012 The SABnzbd-Team <team@sabnzbd.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
#
# This is the "Classic" web interface for SABnzbd
# Simple, but compatible with all popular browsers.
# We recommend use of the more advanced versions.
#

View File

@@ -1,66 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath=".."#-->
<!--#set global $helpsubject="Configure-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu=""#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2>$T('configuration')</h2>
<p>
<b>$T('confgFile'):</b> $configfn
</p>
$T('explain-Restart')<br/>
<form action="saveGeneral" method="post">
<input type="submit" onclick="this.form.action='restart?session=$session'; this.form.submit(); return false;" value="$T('button-restart')"/>
</form>
<br/><br/>
<hr/>
<!--#if $folders#-->
$T('explain-orphans')<br/>
<br/>
<table id="catTable">
<tr>
<th></th>
<th></th>
<th>$T('name')</th>
</tr>
<!--#set $odd = False#-->
<!--#for $folder in $folders#-->
<!--#set $odd = not $odd#-->
<tr class="<!--#if $odd then "odd" else "even"#-->">
<td>
<form action="delete" method="get">
<input type="hidden" value="$folder" name="name">
<input type="hidden" value="$session" name="session">
<input type="submit" value="$T('button-delCat')">
</form>
</td>
<td>
<form action="add" method="get">
<input type="hidden" value="$folder" name="name">
<input type="hidden" value="$session" name="session">
<input type="submit" value="$T('button-add')">
</form>
</td>
<td>
$folder
</td>
<!--#end for#-->
</table>
<hr/><br/>
<!--#end if#-->
$T('explain-Repair')<br/>
<form action="saveGeneral" method="post">
<input type="submit" onclick="this.form.action='repair?session=$session'; this.form.submit(); return false;" value="$T('button-repair')"/>
</form>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,104 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="configure-categories-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu="categories"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2>$T('configCat')</h2>
$T('explain-configCat')<br/>
$T('explain-catTags')<br/>
$T('explain-catTags2')<br/>
$T('explain-relFolder') $defdir<br/>
<br/>
<table id="catTable">
<tr>
<th></th>
<th>$T('category')</th>
<th>&nbsp;$T('mode')&nbsp;</th>
<th>&nbsp;$T('priority')&nbsp;</th>
<!--#if $script_list#--><th>$T('script')</th><!--#end if#-->
<th>$T('catFolderPath')</th>
<th>$T('catTags')</th>
<th></th>
</tr>
<!--#set $odd = False#-->
<!--#for $slot in $slotinfo#-->
<!--#set $odd = not $odd#-->
<tr class="<!--#if $odd then "odd" else "even"#-->">
<td><!--#if $slot.name and $slot.name != '*'#-->
<form action="delete" method="get">
<input type="hidden" value="$slot.name" name="name">
<input type="hidden" value="$session" name="session">
<input type="submit" value="$T('button-delCat')">
</form>
<!--#end if#-->
</td>
<td>
<form action="save" method="get">
<input type="hidden" value="$slot.name" name="name">
<input type="hidden" value="$session" name="session">
<!--#if $slot.name != '*'#-->
<input type="text" name="newname" value="$slot.name">
<!--#else#-->
$T('default')
<!--#end if#-->
</td>
<td>
<select name="pp">
<optgroup label="$T('pp')">
<!--#if $slot.name != '*'#-->
<option value="" <!--#if $slot.pp == "" then "selected" else ""#-->>$T('default')</option>
<!--#end if#-->
<option value="0" <!--#if $slot.pp == "0" then "selected" else ""#-->>$T('pp-none')</option>
<option value="1" <!--#if $slot.pp == "1" then "selected" else ""#-->>$T('pp-repair')</option>
<option value="2" <!--#if $slot.pp == "2" then "selected" else ""#-->>$T('pp-unpack')</option>
<option value="3" <!--#if $slot.pp == "3" then "selected" else ""#-->>$T('pp-delete')</option>
</optgroup>
</select>
</td>
<td>
<select name="priority">
<optgroup label="$T('priority')">
<!--#if $slot.name != '*'#-->
<option value="-100" <!--#if $slot.priority == -100 then 'selected' else ''#-->>$T('default')</option>
<!--#end if#-->
<option value="2" <!--#if $slot.priority == 2 then 'selected' else ''#-->>$T('pr-force')</option>
<option value="1" <!--#if $slot.priority == 1 then 'selected' else ''#-->>$T('pr-high')</option>
<option value="0" <!--#if $slot.priority == 0 then 'selected' else ''#-->>$T('pr-normal')</option>
<option value="-1" <!--#if $slot.priority == -1 then 'selected' else ''#-->>$T('pr-low')</option>
<option value="-2" <!--#if $slot.priority == -2 then 'selected' else ''#-->>$T('pr-paused')</option>
</optgroup>
</select>
</td>
<!--#if $script_list#-->
<td>
<select name="script">
<optgroup label="$T('script')">
<!--#for $sc in $script_list#-->
<!--#if not ($sc == 'Default' and $slot.name == '*')#-->
<option value="$sc" <!--#if $slot.script.lower() == $sc.lower() then "selected" else ""#-->>$Tspec($sc)</option>
<!--#end if#-->
<!--#end for#-->
</optgroup>
</select>
</td>
<!--#end if#-->
<td><input type="text" size=30 name="dir" value="$slot.dir"></td>
<td><input type="text" size=30 name="newzbin" value="$slot.newzbin"></td>
<td><input type="submit" value="$T('button-save')"></td>
</form>
<!--#end for#-->
</table>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,90 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+Folders-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu="directories"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2>$T('folderConfig')</h2>
<p><strong>
$T('explain-folderConfig')<br />
</strong></p>
<form action="saveDirectories" method="post">
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('userFolders')</legend>
<emp>$T('in') "$my_home"</emp><br><br>
<strong>$T('opt-download_dir'):</strong><br>
$T('explain-download_dir')<br/>
<input type="text" size="40" name="download_dir" value="$download_dir">
<br>
<br>
<strong>$T('opt-download_free'):</strong><br>
$T('explain-download_free')<br>
<input type="text" size="10" name="download_free" value="$download_free">
<br>
<br>
<strong>$T('opt-complete_dir'):</strong><br>
$T('explain-complete_dir')<br>
<input type="text" size="40" id="complete_dir" name="complete_dir" value="$complete_dir">
<!--#if not $nt#-->
<br>
<br>
<strong>$T('opt-permissions'):</strong><br>
$T('explain-permissions')<br>
<input type="text" size="10" name="permissions" value="$permissions">
<!--#end if#-->
<br>
<br>
<strong>$T('opt-dirscan_dir'):</strong><br>
$T('explain-dirscan_dir')<br>
<input type="text" size="40" name="dirscan_dir" value="$dirscan_dir">
<br>
<br>
<strong>$T('opt-dirscan_speed'):</strong><br>
$T('explain-dirscan_speed')<br>
<input type="text" size="10" name="dirscan_speed" value="$dirscan_speed">
<br>
<br>
<strong>$T('opt-script_dir'):</strong><br>
$T('explain-script_dir')<br>
<input type="text" size="40" name="script_dir" value="$script_dir">
<br>
<br>
<strong>$T('opt-email_dir'):</strong><br>
$T('explain-email_dir')<br>
<input type="text" size="40" name="email_dir" value="$email_dir">
<br>
<br>
<strong>$T('opt-password_file'):</strong><br>
$T('explain-password_file')<br>
<input type="text" size="40" name="password_file" value="$password_file">
</fieldset>
<fieldset class="EntryFieldSet">
<legend>$T('systemFolders')</legend>
<emp>$T('in') "$my_lcldata"</emp><br><br>
<strong>$T('opt-admin_dir'):</strong><br>
$T('explain-admin_dir1')<br/>$T('explain-admin_dir2')<br/>
<input type="text" size="40" name="admin_dir" value="$admin_dir">
<br>
<br>
<strong>$T('opt-log_dir'):</strong><br>
$T('explain-log_dir')<br/>
<input type="text" size="40" name="log_dir" value="$log_dir">
<br>
<br>
<strong>$T('opt-nzb_backup_dir'):</strong><br>
$T('explain-nzb_backup_dir')<br>
<input type="text" size="40" name="nzb_backup_dir" value="$nzb_backup_dir">
<input type="hidden" name="session" value="$session">
</fieldset>
</div><br>
<input type="submit" size="40" value="$T('button-saveChanges')">
<!--#if $restart_req#-->
<input type="submit" onclick="this.form.action='../restart'; this.form.submit(); return false;" value="$T('button-restart')"/>
<!--#end if#-->
</form>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,145 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+General-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu="general"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2>$T('generalConfig')</h2>
<form action="saveGeneral" method="post" autocomplete="off">
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('webServer')</legend>
<i>$T('restartRequired')</i><br/><br/>
<strong>$T('opt-host'):</strong><br>
$T('explain-host')<br>
<input type="text" name="host" value="$host">
<br>
<br>
<strong>$T('opt-port'):</strong><br>
$T('explain-port')<br>
<input type="text" name="port" value="$port">
<br>
<br>
<strong>$T('opt-web_dir'):</strong><br>
$T('explain-web_dir')<br>
<select name="web_dir">
<!--#for $webline in $web_list#-->
<!--#if $webline.lower() == $web_dir.lower()#-->
<option value="$webline" selected>$webline</option>
<!--#else#-->
<option value="$webline">$webline</option>
<!--#end if#-->
<!--#end for#-->
</select>
<br/><br/>
<strong>$T('opt-web_dir2'):</strong><br>
$T('explain-web_dir2')<br>
<select name="web_dir2">
<!--#for $webline in $web_list2#-->
<!--#if $webline.lower() == $web_dir2.lower()#-->
<option value="$webline" selected>$webline</option>
<!--#else#-->
<option value="$webline">$webline</option>
<!--#end if#-->
<!--#end for#-->
</select>
<br /><br /><strong>$T('opt-apikey'):</strong><br />
$T('explain-apikey')<br />
<input type="text" style="width:250px;border:none;" onclick="this.select()" id="apikey" value="$session">
<a href="generateAPIKey?session=$session">$T('button-apikey')</a>
<br /><br /><strong>$T('opt-nzbkey'):</strong><br />
$T('explain-nzbkey')<br />
<input type="text" style="width:250px;border:none;" onclick="this.select()" id="nzbkey" value="$nzb_key">
<a href="generateNzbKey?session=$session">$T('button-apikey')</a>
<br /><br />
<label><input type="checkbox" name="disable_api_key" value="1" <!--#if $disable_api_key > 0 then "checked=1" else ""#--> /> <strong>$T('opt-disableApikey')</strong></label><br>
$T('explain-disableApikey') <a href="${helpuri}cross-site-vulnerability/" target="_blank">$T('explain-disableApikeyWarn')</a>
<!--#if $lang_list#-->
<br/><br/>
<strong>$T('opt-language'):</strong><br/>
$T('explain-language')<br/>
<select name="language">
<!--#for $webline in $lang_list#-->
<!--#if $webline[0].lower() == $language.lower()#-->
<option value="$webline[0]" selected>$webline[1]</option>
<!--#else#-->
<option value="$webline[0]">$webline[1]</option>
<!--#end if#-->
<!--#end for#-->
</select>
<!--#end if#-->
</fieldset>
</div>
<fieldset class="EntryFieldSet">
<legend>$T('webAuth')</legend>
<strong>$T('opt-web_username'):</strong><br>
$T('explain-web_username')<br>
<input type="text" name="username" value="$username">
<br>
<br>
<strong>$T('opt-web_password')</strong><br>
$T('explain-web_password')<br>
<input type="password" name="password" value="$password">
</fieldset>
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('httpsSupport')</legend>
<i>$T('restartRequired')</i><br/><br/>
<label><input type="checkbox" name="enable_https" value="1" <!--#if $enable_https > 0 then 'checked="1"' else ""#--> <!--#if int($have_ssl) == 0 then "disabled" else ""#--> />
<strong>$T('opt-enable_https')<!--#if int($have_ssl) == 0 then " "+$T('opt-notInstalled') else ""#--></strong></label><br/>
$T('explain-enable_https')<br>
<br/>
<strong>$T('opt-https_port'):</strong><br>
$T('explain-https_port')<br>
<input type="text" name="https_port" value="$https_port">
<br/>
<br/>
<strong>$T('opt-https_cert'):</strong><br/>
$T('explain-https_cert')<br/>
<input type="text" name="https_cert" value="$https_cert">
<br/>
<br/>
<strong>$T('opt-https_key'):</strong><br/>
$T('explain-https_key')<br/>
<input type="text" name="https_key" value="$https_key">
</fieldset>
</div>
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('tuning')</legend>
<strong>$T('opt-refresh_rate'):</strong><br>
$T('explain-refresh_rate')<br>
<input type="text" name="refresh_rate" value="$refresh_rate">
<br>
<br>
<strong>$T('opt-bandwidth_limit'):</strong><br>
$T('explain-bandwidth_limit')<br>
<input type="text" name="bandwidth_limit" value="$bandwidth_limit">
<br>
<br>
<strong>$T('opt-cache_limitstr'):</strong><br>
$T('explain-cache_limitstr')<br>
<input type="text" name="cache_limit" value="$cache_limit">
<br>
<br>
<strong>$T('opt-cleanup_list'):</strong><br>
$T('explain-cleanup_list')<br><br>
<input type="text" name="cleanup_list" value="$cleanup_list">
<input type="hidden" name="session" value="$session">
</fieldset>
</div>
<p>
<input type="submit" value="$T('button-saveChanges')">
<!--#if $restart_req#-->
<input type="submit" onclick="this.form.action='../restart'; this.form.submit(); return false;" value="$T('button-restart')"/>
<!--#end if#-->
</p>
</form>
</table>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,87 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+Indexers-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu="newzbin"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2>Newzbin</h2>
$T('explain-newzbin')<br/><br/>
<form action="saveNewzbin" method="post" autocomplete="off">
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('accountInfo')</legend>
<strong>$T('opt-username_newzbin'):</strong><br>
$T('explain-username_newzbin')<br>
<input type="text" name="username_newzbin" value="$username_newzbin">
<br>
<br>
<strong>$T('opt-password_newzbin'):</strong><br>
$T('explain-password_newzbin')<br>
<input type="password" name="password_newzbin" value="$password_newzbin">
</fieldset>
</div>
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('newzbinBookmarks')</legend>
<label><input type="checkbox" name="newzbin_bookmarks" value="1" <!--#if $newzbin_bookmarks > 0 then "checked=1" else ""#--> <strong>$T('opt-newzbin_bookmarks'):</strong></label><br>
$T('explain-newzbin_bookmarks')<br>
<a href="getBookmarks?session=$session">$T('link-getBookmarks')</a>
<br>
<!--#if $bookmarks_list#-->
<a href="hideBookmarks?session=$session">$T('link-HideBM')</a>
<!--#else#-->
<a href="showBookmarks?session=$session">$T('link-ShowBM')</a>
<!--#end if#-->
<br/>
<br/>
<label><input type="checkbox" name="newzbin_unbookmark" value="1" <!--#if $newzbin_unbookmark > 0 then "checked=1" else ""#--> /> <strong>$T('opt-newzbin_unbookmark'):</strong></label><br>
$T('explain-newzbin_unbookmark')<br>
<br/>
<strong>$T('opt-bookmark_rate'):</strong><br>
$T('explain-bookmark_rate')<br>
<input type="text" name="bookmark_rate" value="$bookmark_rate">
</fieldset>
</div>
<!--#if $bookmarks_list#-->
<fieldset class="EntryFieldSet">
<legend>$T('processedBM')</legend>
<!--#for $msgid in $bookmarks_list#-->
<a href="https://$newzbin_url/browse/post/$msgid/" target="_blank">$msgid</a>&nbsp;
<!--#end for#-->
</fieldset>
<!--#end if#-->
<input type="hidden" name="session" value="$session">
<p><input type="submit" value="$T('button-saveChanges')"></p>
</form>
<hr/>
<h2>NzbMatrix</h2>
$T('explain-nzbmatrix')<br/><br/>
<form action="saveMatrix" method="post" autocomplete="off">
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('accountInfo')</legend>
<strong>$T('opt-username_matrix'):</strong><br>
$T('explain-username_matrix')<br>
<input type="text" name="matrix_username" value="$matrix_username">
<br>
<br>
<strong>$T('opt-apikey_matrix'):</strong><br>
$T('explain-apikey_matrix')<br>
<input type="text" name="matrix_apikey" value="$matrix_apikey">
<br/><br/>
<input type="checkbox" name="matrix_del_bookmark" value="1" <!--#if $matrix_del_bookmark > 0 then "checked=1" else ""#--> /> <strong>$T('opt-newzbin_unbookmark'):</strong><br>
$T('explain-newzbin_unbookmark')<br>
</fieldset>
</div>
<input type="hidden" name="session" value="$session">
<p><input type="submit" value="$T('button-saveChanges')"></p>
</form>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,94 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+Notifications-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl" #-->
<!--#set global $submenu="email"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2>$T('configEmail')</h2>
<form action="saveEmail" method="post" autocomplete="off">
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('emailOptions')</legend>
<strong>$T('opt-email_endjob')</strong><br/>
<input type="radio" name="email_endjob" value="0" <!--#if $email_endjob == "0" then "checked=1" else ""#--> /> $T('email-never')
<input type="radio" name="email_endjob" value="1" <!--#if $email_endjob == "1" then "checked=1" else ""#--> /> $T('email-always')
<input type="radio" name="email_endjob" value="2" <!--#if $email_endjob == "2" then "checked=1" else ""#--> /> $T('email-errorOnly')
<br/><br/>
<label><input type="checkbox" name="email_full" value="1" <!--#if $email_full != "0" then "checked=1" else ""#--> /> <strong>$T('opt-email_full'):</strong></label><br>
$T('explain-email_full')<br/>
<br/>
<label><input type="checkbox" name="email_rss" value="1" <!--#if $email_rss != "0" then "checked=1" else ""#--> /> <strong>$T('opt-email_rss'):</strong></label><br>
$T('explain-email_rss')<br/>
<strong>$T('opt-email_dir'):</strong><br/>
$T('explain-email_dir')<br/>
<input type="text" size="40" name="email_dir" value="$email_dir">
</fieldset>
</div>
<fieldset class="EntryFieldSet">
<legend>$T('emailAccount')</legend>
<strong>$T('opt-email_server'):</strong><br>
$T('explain-email_server').<br>
<input type="text" size="35" name="email_server" value="$email_server">
<br>
<br>
<strong>$T('opt-email_to'):</strong><br>
$T('explain-email_to')<br>
<input type="text" size="35" name="email_to" value="$email_to">
<br>
<br>
<strong>$T('opt-email_from'):</strong><br>
$T('explain-email_from')<br>
<input type="text" size="35" name="email_from" value="$email_from">
<br>
<br>
<strong>$T('opt-email_account'):</strong><br>
$T('explain-email_account')<br>
<input type="text" size="35" name="email_account" value="$email_account">
<br>
<br>
<strong>$T('opt-email_pwd'):</strong><br>
$T('explain-email_pwd')<br>
<input type="password" size="35" name="email_pwd" value="$email_pwd">
</fieldset>
<!--#if $have_growl or $have_ntfosd#-->
<fieldset class="EntryFieldSet">
<legend>$T('growlSettings')</legend>
<!--#if $have_ntfosd#-->
<label><input type="checkbox" name="ntfosd_enable" value="1" <!--#if $ntfosd_enable != "0" then "checked=1" else ""#--> /> <strong>$T('opt-ntfosd_enable'):</strong></label><br>
$T('explain-ntfosd_enable')
<br/>
<br/>
<!--#end if#-->
<!--#if $have_growl#-->
<label><input type="checkbox" name="growl_enable" value="1" <!--#if $growl_enable != "0" then "checked=1" else ""#--> /> <strong>$T('opt-growl_enable'):</strong></label><br>
$T('explain-growl_enable')
<br/>
<br/>
<strong>$T('opt-growl_server'):</strong><br>
$T('explain-growl_server')<br>
<input type="text" size="35" name="growl_server" value="$growl_server">
<br>
<br>
<strong>$T('opt-growl_password'):</strong><br>
$T('explain-growl_password')<br>
<input type="password" size="35" name="growl_password" value="$growl_password">
</fieldset>
<!--#end if#-->
<!--#end if#-->
</div>
<input type="hidden" name="session" value="$session">
<p><input type="submit" value="$T('button-saveChanges')">&nbsp;&nbsp;
<input type="button" onclick="if (confirm('$T('askTestEmail').replace("'","`") ')) { this.form.action='testmail?session=$session&'; this.form.submit(); return false;}" value="$T('link-testEmail')"/>
<input type="button" onclick="this.form.action='testnotification?session=$session&'; this.form.submit(); return false;"value="$T('testNotify')"/>
</p>
</form>
<!--#if $lastmail#-->
$T('emailResult') = <b>$lastmail</b>
<!--#end if#-->
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,375 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+RSS-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu="rss"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2><a href="../rss">$T('configRSS')</a></h2>
<!--#if $active_feed#-->
<!--#set $feed = $active_feed#-->
<div class="EntryBlock">
<form action="upd_rss_feed" method="post">
<fieldset class="EntryFieldSet">
<legend <!--#if $rss[$feed]['enable'] then 'class="feedEnabled"' else 'class="feedDisabled"'#-->><input type="checkbox" onclick="this.form.action='toggle_rss_feed?session=$session'; this.form.submit(); return false;" name="enable" <!--#if $rss[$feed]['enable'] then "CHECKED" else "" #-->/>
$T('feed') $feed</legend>
<input type="text" size="105" name="uri" value="$rss[$feed]['uri']"/>
<input type="button" onclick="if (confirm('$T('confirm').replace("'","`") ')) { this.form.action='del_rss_feed?session=$session&'; this.form.submit(); return false;}" value="$T('button-delFeed')"/>
<input type="button" onclick="this.form.action='test_rss_feed?session=$session&'; this.form.submit(); return false;" value="$T('button-preFeed')"/>
<input type="button" onclick="this.form.action='download_rss_feed?session=$session&'; this.form.submit(); return false;" value="$T('button-forceFeed')"/>
<br/><br/>
<!--#if $rss[$feed]['pick_cat']#-->
<select name="cat">
<optgroup label="$T('category')">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $ct == $rss[$feed]['cat'] then "selected" else ""#-->>$Tspec($ct)</option>
<!--#end for#-->
</optgroup>
</select>
<!--#end if#-->
<select name="pp">
<optgroup label="$T('pp')">
<option value="" <!--#if $rss[$feed]['pp'] == "" then 'selected' else ''#-->>$T('default')</option>
<option value="0" <!--#if $rss[$feed]['pp'] == "0" then 'selected' else ''#-->>$T('pp-none')</option>
<option value="1" <!--#if $rss[$feed]['pp'] == "1" then 'selected' else ''#-->>$T('pp-repair')</option>
<option value="2" <!--#if $rss[$feed]['pp'] == "2" then 'selected' else ''#-->>$T('pp-unpack')</option>
<option value="3" <!--#if $rss[$feed]['pp'] == "3" then 'selected' else ''#-->>$T('pp-delete')</option>
</optgroup>
</select>
<!--#if $rss[$feed]['pick_script']#-->
<select name="script">
<optgroup label="$T('script')">
<!--#for $sc in $script_list#-->
<option value="$sc" <!--#if $sc == $rss[$feed]['script'] then "selected" else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</optgroup>
</select>
<!--#end if#-->
<select name="priority">
<optgroup label="$T('priority')">
<option value="-100" <!--#if $rss[$feed]['priority'] == -100 then 'selected' else ''#-->>$T('default')</option>
<option value="2" <!--#if $rss[$feed]['priority'] == 2 then 'selected' else ''#-->>$T('pr-force')</option>
<option value="1" <!--#if $rss[$feed]['priority'] == 1 then 'selected' else ''#-->>$T('pr-high')</option>
<option value="0" <!--#if $rss[$feed]['priority'] == 0 then 'selected' else ''#-->>$T('pr-normal')</option>
<option value="-1" <!--#if $rss[$feed]['priority'] == -1 then 'selected' else ''#-->>$T('pr-low')</option>
<option value="-2" <!--#if $rss[$feed]['priority'] == -2 then 'selected' else ''#-->>$T('pr-paused')</option>
</optgroup>
</select>
<input type="hidden" name="feed" value="$feed"/>
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-save')"/>
<br />
</form>
<br/><br/>
<table>
<tr>
<th>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</th>
<th>$T('rss-order')</th>
<th>$T('rss-type')</th>
<th>$T('rss-filter')</th>
<!--#if $rss[$feed]['pick_cat']#--><th>$T('category')</th><!--#end if#-->
<th>Mode</th>
<!--#if $rss[$feed]['pick_script']#--><th>$T('script')</th><!--#end if#-->
<th>$T('priority')</th>
<th></th>
</tr>
<form action="upd_rss_filter" method="get">
<tr class="odd">
<td></td>
<td></td>
<td>
<select name="filter_type">
<option value="A" selected /> $T('rss-accept')</option>
<option value="M" /> $T('rss-must')</option>
<option value="R" /> $T('rss-reject')</option>
<option value="C" /> $T('rss-mustcat')</option>
</select>
</td>
<td><input type="text" size="60" name="filter_text" value=""></td>
<!--#if $rss[$feed]['pick_cat']#-->
<td>
<select name="cat">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $ct == "Default" then "selected" else ""#-->>$Tspec($ct)</option>
<!--#end for#-->
</select>
</td>
<!--#end if#-->
<td>
<select name="pp">
<option value="" selected>$T('default')</option>
<option value="0">$T('pp-none')</option>
<option value="1">$T('pp-repair')</option>
<option value="2">$T('pp-unpack')</option>
<option value="3">$T('pp-delete')</option>
</select>
</td>
<!--#if $rss[$feed]['pick_script']#-->
<td>
<select name="script">
<!--#for $sc in $script_list#-->
<option value="$sc" <!--#if $sc == "Default" then "selected" else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</select>
</td>
<td>
<select name="priority">
<option value="-100" selected>$T('default')</option>
<option value="2">$T('pr-force')</option>
<option value="1">$T('pr-high')</option>
<option value="0">$T('pr-normal')</option>
<option value="-1">$T('pr-low')</option>
<option value="-2">$T('pr-paused')</option>
</select>
</td>
<!--#end if#-->
<input type="hidden" value="$rss[$feed]['filtercount']" name="index">
<input type="hidden" value="$feed" name="feed">
<input type="hidden" name="session" value="$session">
<td><input type="submit" value="$T('button-save')"></td>
</tr>
</form>
<!--#set $fnum = 0#-->
<!--#for $filter in $rss[$feed].filters#-->
<tr class="odd">
<td>
<form action="del_rss_filter" method="post">
<input type="hidden" value="$fnum" name="index">
<input type="hidden" value="$feed" name="feed">
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('rss-delFilter')"></form>
</td>
<form action="upd_rss_filter" method="get">
<td>
<input type="text" size="3" name="new_index" value=$fnum>
</td>
<td>
<select name="filter_type">
<option value="A" <!--#if $filter[3] == "A" then "selected" else ""#--> /> $T('rss-accept')</option>
<option value="M" <!--#if $filter[3] == "M" then "selected" else ""#--> /> $T('rss-must')</option>
<option value="R" <!--#if $filter[3] == "R" then "selected" else ""#--> /> $T('rss-reject')</option>
<option value="C" <!--#if $filter[3] == "C" then "selected" else ""#--> /> $T('rss-mustcat')</option>
</select>
</td>
<td><input type="text" size="60" name="filter_text" value="$filter[4]"/></td>
<!--#if $rss[$feed]['pick_cat']#-->
<td>
<select name="cat">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $ct == $filter[0] then "selected" else ""#-->>$Tspec($ct)</option>
<!--#end for#-->
</select>
</td>
<!--#end if#-->
<td>
<select name="pp">
<option value="" <!--#if $filter[1] == "0" then 'selected' else ''#-->>$T('default')</option>
<option value="0" <!--#if $filter[1] == "0" then 'selected' else ''#-->>$T('pp-none')</option>
<option value="1" <!--#if $filter[1] == "1" then 'selected' else ''#-->>$T('pp-repair')</option>
<option value="2" <!--#if $filter[1] == "2" then 'selected' else ''#-->>$T('pp-unpack')</option>
<option value="3" <!--#if $filter[1] == "3" then 'selected' else ''#-->>$T('pp-delete')</option>
</select>
</td>
<!--#if $rss[$feed]['pick_script']#-->
<td>
<select name="script">
<!--#for $sc in $script_list#-->
<option value="$sc" <!--#if $sc == $filter[2] then "selected" else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</select>
</td>
<!--#end if#-->
<td>
<select name="priority">
<option value="-100" <!--#if $filter[5] == "-100" or $filter[4] == "" then 'selected' else ''#-->>$T('default')</option>
<option value="2" <!--#if $filter[5] == "2" then "selected" else ""#-->>$T('pr-force')</option>
<option value="1" <!--#if $filter[5] == "1" then "selected" else ""#-->>$T('pr-high')</option>
<option value="0" <!--#if $filter[5] == "0" then "selected" else ""#-->>$T('pr-normal')</option>
<option value="-1" <!--#if $filter[5] == "-1" then "selected" else ""#-->>$T('pr-low')</option>
<option value="-2" <!--#if $filter[5] == "-2" then "selected" else ""#-->>$T('pr-paused')</option>
</select>
</td>
<td>
<input type="hidden" name="index" value="$fnum"/>
<input type="hidden" name="feed" value="$feed"/>
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-save')"/>
<!--#if not $rss[$feed].filter_states[$fnum]#-->&nbsp;&nbsp;$T('Incorrect filter')<!--#end if#-->
</td>
</form>
</tr>
<!--#set $fnum = $fnum+1#-->
<!--#end for#-->
</table>
</div>
</fieldset>
<!--#if $error#-->
<br/><br/><b>$error</b><br/><br/>
<!--#end if#-->
<h3>$T('rss-matched')</h3>
<table id="catTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;$T('rss-skip')&nbsp;</th>
<th>&nbsp;$T('rss-filter')&nbsp;</th>
<th>$T('sort-title')</th>
</tr>
<!--#set $odd = False#-->
<!--#for $job in $matched#-->
<!--#set $odd = not $odd#-->
<tr class="<!--#if $odd then "odd" else "even"#-->">
<td><form action="download" method="get">
<input type="hidden" name="url" value="$job[0]"/>
<input type="hidden" name="nzbname" value="$job[4]"/>
<input type="hidden" value="$feed" name="feed"/>
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('link-download')">
</form>
</td>
<td>$job[2]</td>
<td>$job[3]</td>
<td>$job[1]</td>
</tr>
<!--#end for#-->
</table>
<h3>$T('rss-notMatched')</h3>
<table id="catTable">
<tr>
<th>&nbsp;</th>
<th>&nbsp;$T('rss-skip')&nbsp;</th>
<th>&nbsp;$T('rss-filter')&nbsp;</th>
<th>$T('sort-title')</th>
</tr>
<!--#set $odd = False#-->
<!--#for $job in $unmatched#-->
<!--#set $odd = not $odd#-->
<tr class="<!--#if $odd then "odd" else "even"#-->">
<td><form action="download" method="get">
<input type="hidden" name="url" value="$job[0]"/>
<input type="hidden" name="nzbname" value="$job[4]"/>
<input type="hidden" value="$feed" name="feed"/>
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('link-download')">
</form>
</td>
<td>$job[2]</td>
<td>$job[3]</td>
<td>$job[1]</td>
</tr>
<!--#end for#-->
</table>
<h3>$T('rss-done')</h3>
<!--#if $downloaded#-->
<form action="clean_rss_jobs" method="get">
<input type="hidden" value="$feed" name="feed"/>
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-clear')">
</form><br/>
<!--#end if#-->
<table id="catTable">
<tr>
<th>$T('sort-title')</th>
</tr>
<!--#set $odd = False#-->
<!--#for $job in $downloaded#-->
<!--#set $odd = not $odd#-->
<tr class="<!--#if $odd then "odd" else "even"#-->">
<td>$job</td>
</tr>
<!--#end for#-->
</table>
<!--#else#-->
<div class="EntryBlock">
<form action="add_rss_feed" method="post">
<fieldset class="EntryFieldSet">
<legend>$T('newFeedURI')</legend>
<input type="text" size="10" name="feed" value="$feed"/>&nbsp;
<input type="text" size="104" name="uri" value=""/><br/><br/>
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-add')"/>
</fieldset>
</form>
</div>
<p>$T('explain-RSS')</p>
<div class="EntryBlock">
<form action="save_rss_rate" method="post">
<fieldset class="EntryFieldSet">
<legend>$T('opt-rss_rate')</legend>
<input type="text" name="rss_rate" value="$rss_rate">
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-save')"/>
<br/>
$T('explain-rss_rate')
</fieldset>
</form>
</div>
<div class="EntryBlock">
<form action="rss_now" method="post">
<fieldset class="EntryFieldSet">
<legend>RSS</legend>
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-rssNow')"/>
<br/>
&nbsp;
</fieldset>
</form>
</div>
<table id="catTable">
<tr>
<th></th>
<th>$T('enabled')</th>
<th>$T('feed')</th>
<th>URL</th>
</tr>
<!--#set $odd = False#-->
<!--#for $feed in sorted($rss.keys(), cmp=lambda x,y: cmp(x.lower(), y.lower()))#-->
<!--#set $odd = not $odd#-->
<tr class="<!--#if $odd then "odd" else "even"#-->">
<td><form action="del_rss_feed" method="get">
<input type="hidden" name="session" value="$session">
<input type="hidden" value="$feed" name="feed">
<input type="button" onclick="if (confirm('$T('confirm').replace("'","`") ')) { this.form.action='del_rss_feed?session=$session&'; this.form.submit(); return false;}" value="$T('button-del')"/>
</form>
</td>
<td><form action="upd_rss_feed" method="post">
<input type="hidden" name="session" value="$session">
<input type="hidden" value="$feed" name="feed">
<input type="hidden" value="1" name="table">
<input type="checkbox" onclick="this.form.action='toggle_rss_feed?session=$session'; this.form.submit(); return false;" name="enable" <!--#if $rss[$feed]['enable'] then "CHECKED" else "" #-->/>
</form>
</td>
<td><a href="?feed=$rss[$feed]['link']">$feed</a></td>
<td>$rss[$feed]['uri']</td>
</tr>
<!--#end for#-->
</table>
<!--#end if#-->
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,73 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+Scheduling-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu="scheduling"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2>$T('configSchedule')</h2>
<div class="EntryBlock">
<form action="addSchedule" method="post">
<fieldset class="EntryFieldSet">
<legend>$T('addSchedule')</legend>
<%import time
t = time.localtime()
hour = t[3]
if hour != 23:
hour += 1
else:
hour = 0 %>
$T('hour'):<br>
<select name="hour">
<!--#for $i in range(24)#-->
<option value="$i" <!--#if hour == i then "selected=1" else ""#-->> $i</option>
<!--#end for#-->
</select>
:
<select name="minute">
<!--#for $i in range(60)#-->
<option value="$i">$i
<!--#end for#-->
</select>
<br>$T('sch-frequency'): <br>
<select name="dayofweek">
<option value="*" selected>$T('daily')
<option value="1">$T('monday')
<option value="2">$T('tuesday')
<option value="3">$T('wednesday')
<option value="4">$T('thursday')
<option value="5">$T('friday')
<option value="6">$T('saturday')
<option value="7">$T('sunday')
</select>
<br>$T('sch-action'):<br>
<select name="action">
<!--#for $action in $actions#-->
<option value="$action">$actions_lng[$action]
<!--#end for#-->
</select>
<br>$T('sch-arguments'):<br>
<input type="text" size="20" name="arguments" value="">
<input type="hidden" name="session" value="$session">
<p><input type="submit" value="$T('button-addSchedule')"></p>
</fieldset>
</form>
</div>
<h3>$T('currentSchedules'):</h3>
<div class="EntryBlock">
<!--#set $schednum = 0#-->
<!--#for $line in $schedlines#-->
<form action="delSchedule" method="post">
<fieldset class="EntryFieldSet">
$T('sch-task') $taskinfo[$schednum][0]: <strong>$taskinfo[$schednum][1]:$taskinfo[$schednum][2]</strong> - $taskinfo[$schednum][3] - $taskinfo[$schednum][4]
<!--#set $schednum += 1#-->
<input type="hidden" name="line" value="$line">
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-delSchedule')">
</fieldset>
</form><br />
<!--#end for#-->
</div>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,71 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+Servers-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu="servers"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2>$T('configServer')</h2>
<div class="EntryBlock">
<form action="addServer" method="post" autocomplete="off">
<fieldset class="EntryFieldSet">
<legend>$T('addServer')</legend>
$T('srv-host'):<br><input type="text" size="25" name="host"><br>
$T('srv-port'):<br><input type="text" size="25" name="port"><br>
$T('srv-username'):<br><input type="text" size="25" name="username"><br>
$T('srv-password'):<br><input type="password" size="25" name="password"><br>
$T('srv-timeout'):<br><input type="text" size="25" name="timeout" value="120"><br>
$T('srv-connections'):<br><input type="text" size="25" name="connections"><br>
$T('srv-retention') ($T('days')):<br><input type="text" size="25" name="retention"><br>
<!--#if int($have_ssl) == 0#-->
$T('srv-ssl') $T('opt-notInstalled')
<!--#else#-->
<input type="checkbox" name="ssl" value="1" <!--#if int($have_ssl) == 0 then "disabled" else ""#-->>&nbsp;$T('srv-ssl')<br/>
<!--#end if#-->
<input type="checkbox" name="fillserver" value="1">&nbsp;$T('srv-fillserver')<br>
<input type="checkbox" name="optional" value="1">&nbsp;$T('srv-optional')<br>
<input type="checkbox" name="enable" value="1" checked="1">&nbsp;$T('srv-enable')<br>
<input type="hidden" name="session" value="$session">
<p><input type="submit" value="$T('button-addServer')"></p>
</fieldset>
</form>
<!--#set $slist = $servers.keys()#-->
<!--#$slist.sort()#-->
<!--#for $server in $slist#-->
<form action="saveServer" method="post" autocomplete="off">
<fieldset class="EntryFieldSet">
<legend>$server</legend>
$T('srv-host'):<br><input type="text" size="25" name="host" value="$servers[$server]['host']"><br>
$T('srv-port'):<br><input type="text" size="25" name="port" value="$servers[$server]['port']"><br>
$T('srv-username'):<br><input type="text" size="25" name="username" value="$servers[$server]['username']"><br>
$T('srv-password'):<br><input type="password" size="25" name="password" value="$servers[$server]['password']"><br>
$T('srv-timeout'):<br><input type="text" size="25" name="timeout" value="$servers[$server]['timeout']"><br>
$T('srv-connections'):<br><input type="text" size="25" name="connections" value="$servers[$server]['connections']"><br>
$T('srv-retention'):<br><input type="text" size="25" name="retention" value="$servers[$server]['retention']"><br>
<!--#if int($have_ssl) == 0#-->
$T('srv-ssl') $T('opt-notInstalled')
<!--#else#-->
<input type="checkbox" name="ssl" value="1" <!--#if int($servers[$server]['ssl']) != 0 then "checked=1" else ""#-->/>&nbsp;$T('srv-ssl')<br/>
<!--#end if#-->
<input type="checkbox" name="fillserver" value="1" <!--#if int($servers[$server]['fillserver']) != 0 then "checked=1" else ""#--> />&nbsp;$T('srv-fillserver')<br/>
<input type="checkbox" name="optional" value="1" <!--#if int($servers[$server]['optional']) != 0 then "checked=1" else ""#--> />&nbsp;$T('srv-optional')<br/>
<input type="checkbox" name="enable" value="1" <!--#if int($servers[$server]['enable']) != 0 then "checked=1" else ""#--> />&nbsp;$T('srv-enable')<br/>
<input type="hidden" name="server" value="$server">
<input type="hidden" name="session" value="$session">
<p><input type="submit" value="$T('button-saveChanges')"></p>
<p><input type="submit" onclick="this.form.action='delServer'; this.form.submit(); return false;" value="$T('button-delServer')"></p>
<!--#if 'amounts' in $servers[$server]#-->
<table border="1">
<tr><td>$T('total')</td><td>$servers[$server]['amounts'][0]</td>
<td>$T('thisMonth')</td><td>$servers[$server]['amounts'][1]</td></tr>
<tr><td>$T('today')</td><td>$servers[$server]['amounts'][3]</td>
<td>$T('thisWeek')</td><td>$servers[$server]['amounts'][2]</td></tr>
</table>
<!--#end if#-->
</fieldset>
</form>
<!--#end for#-->
</div>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,450 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+Sorting-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu="sorting"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<script type="text/javascript">
function tvAdd(val){
var tv = document.getElementById('tvfoldername');
tv.value = tv.value + val;
previewtv();
}
function tvSet(val){
var tv = document.getElementById('tvfoldername');
tv.value = val;
previewtv();
}
function tvClear(){
var tv = document.getElementById('tvfoldername');
tv.value = '';
previewtv();
}
function previewtv(){
var input = document.getElementById('tvfoldername').value;
var slash = "\\";
input.toLowerCase()
input = input.replace(/%ext/g,'avi');
input = input.replace(/%sn/g,'$T("show-sp-name")');
input = input.replace(/%s\.n/g,'$T("show-dot-name")');
input = input.replace(/%s\_n/g,'$T("show-us-name")');
input = input.replace(/%0s/g,'01');
input = input.replace(/%s/g,'1');
input = input.replace(/%en/g,'$T("ep-sp-name")');
input = input.replace(/%e\.n/g,'$T("ep-dot-name")');
input = input.replace(/%e\_n/g,'$T("ep-us-name")');
input = input.replace(/%0e/g,'05');
input = input.replace(/%e/g,'5');
input = input.replace(/%fn/g,'$T("sort-File")');
input = input.replace(/%dn/g,'$T("sort-Folder")');
document.getElementById('previewtv').innerHTML = getOutput(input);
}
function getOutput(input){
var slash = "\\";
var com = document.getElementById('complete_dir').value;
// For some reason a simply replace() doesn't replace multiple / characters
// Work out whether we are using / or \
for (i=0; i<com.length; i++){
if (com[i] == "/") slash = "/";
}
// Replace / or \ with whatever complete_dir uses
for (i=0; i<input.length; i++){
if (input[i] == "/") {
input = input.replace("/",slash, "gi");
} else if (input[i] == "\\") {
input = input.replace("\\",slash, "gi");
}
}
// If the complete_dir ends in / or \, then strip that character
len = com.length
if (com.substring(len-1, len) == slash)
{
com = com.substring(0, len-1)
}
// If the pattern starts with absolute path, don't prepend default folder
if (input[0] == "/" || input[0] == slash || input[1] == ':') {
output = input;
} else {
output = com+slash+input;
}
return output
}
function movieAdd(val){
var tv = document.getElementById('moviefoldername');
tv.value = tv.value + val;
previewmovie();
}
function movieSet(val, val2){
var tv = document.getElementById('moviefoldername');
tv.value = val;
var tv2 = document.getElementById('movieextra');
tv2.value = val2;
previewmovie();
}
function movieClear(){
var tv = document.getElementById('moviefoldername');
tv.value = '';
previewmovie();
}
function movieClearExtra(){
var tv = document.getElementById('movieextra');
tv.value = '';
previewmovie();
}
function movieExtraFolder(value)
{
document.getElementById('movie_extra_folder').checked = value;
}
function previewmovie()
{
var input = document.getElementById('moviefoldername').value;
var input2 = document.getElementById('movieextra').value;
extra = input2.replace(/%1/g,'1');
var slash = "\\";
ext = extra + '.avi'
input = input.replace(/\.%ext/g,ext);
input = input.replace(/%ext/g,'avi');
input = input.replace(/%title/g,'$T("movie-sp-name")');
input = input.replace(/%\.title/g,'$T("movie-dot-name")');
input = input.replace(/%\_title/g,'$T("movie-us-name")');
input = input.replace(/%y/g,'2000');
input = input.replace(/%decade/g,'00');
input = input.replace(/%0decade/g,'2000');
input = input.replace(/%fn/g,'$T("sort-File")');
var regex = /\{([^\{]*)\}/g;
var str = input;
var result;
while ((result = regex.exec(str)) != null)
{
var lower = result[0].toLowerCase();
input = input.replace(result[0],lower);
}
input = input.replace(/{/g,'');
input = input.replace(/}/g,'');
document.getElementById('previewmovie').innerHTML = getOutput(input);
}
function dateSet(val){
var tv = document.getElementById('datefoldername');
tv.value = val;
previewdate();
}
function dateClear(){
var tv = document.getElementById('datefoldername');
tv.value = '';
previewdate();
}
function previewdate(){
var input = document.getElementById('datefoldername').value;
var slash = "\\";
input.toLowerCase()
input = input.replace(/%ext/g,'avi');
input = input.replace(/%t/g,'$T("show-sp-name")');
input = input.replace(/%\.t/g,'$T("show-dot-name")');
input = input.replace(/%\_t/g,'$T("show-us-name")');
input = input.replace(/%decade/g,'00');
input = input.replace(/%0decade/g,'2000');
input = input.replace(/%fn/g,'$T("sort-File")');
input = input.replace(/%desc/g,'$T("ep-sp-name")');
input = input.replace(/%\.desc/g,'$T("ep-dot-name")');
input = input.replace(/%\_desc/g,'$T("ep-us-name")');
input = input.replace(/%0d/g,'02');
input = input.replace(/%d/g,'2');
input = input.replace(/%0m/g,'01');
input = input.replace(/%m/g,'1');
input = input.replace(/%y/g,'2009');
document.getElementById('previewdate').innerHTML = getOutput(input);
}
function showDiv(id)
{
disp = (document.getElementById(id).style.display == "block") ? "none" : "block";
document.getElementById(id).style.display = disp;
}
</script>
<h2>$T('configSort')</h2>
<form action="saveSorting" method="post">
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('seriesSorting')</legend>
<input class="radio" type="checkbox" name="enable_tv_sorting" value="1" <!--#if $enable_tv_sorting > 0 then "checked=1" else ""#--> /> <strong>$T('opt-tvsort')</strong><br class="clear" />
<br class="clear" />
<strong>$T('affectedCat'):</strong>
<br class="clear" />
<select name="tv_cat" multiple="multiple" size=4 class="multiple_cats">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $ct in $tv_categories then 'selected' else ''#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
<br class="clear" /><br class="clear" />
<a href="#toggle" onclick="javascript:showDiv('Keytable')">$T('sort-legenda')</a>
<div id="Keytable">
<div class="row">
<span class="heading1">$T('sort-meaning')</span>
<span class="heading2">$T('sort-pattern')</span>
<span class="heading3">$T('sort-result')</span>
</div>
<div class="row">
<span class="tripleheight">$T('show-name')</span>
<span class="pattern">%sn</span>
<span class="result">$T('show-sp-name')</span>
<br />
<span class="pattern">%s.n</span>
<span class="result">$T('show-dot-name')</span>
<br />
<span class="pattern">%s_n</span>
<span class="result">$T('show-us-name')</span>
</div>
<div class="row">
<span class="doubleheight">$T('show-seasonNum')</span>
<span class="pattern">%s</span>
<span class="result">1</span>
<br />
<span class="pattern">%0s</span>
<span class="result">01</span>
</div>
<div class="row">
<span class="doubleheight">$T('show-epNum')</span>
<span class="pattern">%e</span>
<span class="result">5</span>
<br />
<span class="pattern">%0e</span>
<span class="result">05</span>
</div>
<div class="row">
<span class="tripleheight">$T('ep-name')</span>
<span class="pattern">%en</span>
<span class="result">$T('ep-sp-name')</span>
<br />
<span class="pattern">%e.n</span>
<span class="result">$T('ep-dot-name')</span>
<br />
<span class="pattern">%e_n</span>
<span class="result">$T('ep-us-name')</span>
</div>
<div class="row">
<span class="normalheight">$T('fileExt')</span>
<span class="pattern">%ext</span>
<span class="result">avi</span>
</div>
<div class="row">
<span class="normalheight">$T('orgFilename')</span>
<span class="pattern">%fn</span>
<span class="result">$T("sort-File")</span>
</div>
<div class="row">
<span class="normalheight">$T('orgDirname')</span>
<span class="pattern">%dn</span>
<span class="result">$T("sort-Folder")</span>
</div>
<div class="row">
<span class="normalheight">$T('lowercase')</span>
<span class="pattern">{$T('TEXT')}</span>
<span class="result">$T('text')</span>
</div>
<!--
<span class="">$T('orgFilename')</span>
<span class="">%fn</span>
<span class="">$T('ep-name')</span>
<br />-->
</div><br class="clear" />
<strong>$T('sortString'):</strong><br class="clear" />
<input type="text" style="width:400px" size="80" id="tvfoldername" name="tv_sort_string" value="$tv_sort_string" onkeyup="javascript:previewtv()"><input type="button" style="width:100px;" onclick="tvClear()" value="$T('button-clear')"/><br class="clear" />
<strong>$T('presetSort'):</strong><br class="clear" />
<input type="button" onclick="tvSet('%sn/Season %s/%sn - %sx%0e - %en.%ext')" value="$T('button-Season1x05')"/>
<input type="button" onclick="tvSet('%sn/Season %s/%sn - S%0sE%0e - %en.%ext')" value="$T('button-SeasonS01E05')"/>
<input type="button" onclick="tvSet('%sn/%sx%0e - %en/%sn - %sx%0e - %en.%ext')" value="$T('button-Ep1x05')"/>
<input type="button" onclick="tvSet('%sn/S%0sE%0e - %en/%sn - S%0sE%0e - %en.%ext')" value="$T('button-EpS01E05')"/><br class="clear" />
<br class="clear" />
$T('example'): <span id="previewtv"></span>
<br class="clear" /><br class="clear" />
</fieldset>
<br />
<fieldset class="EntryFieldSet">
<legend>$T('genericSort')</legend>
<input id="complete_dir" type="hidden" value="$complete_dir" />
<input class="radio" type="checkbox" name="enable_movie_sorting" value="1" <!--#if $enable_movie_sorting > 0 then "checked=1" else ""#--> /> <strong>$T('opt-movieSort')</strong>
<br class="clear" /><br class="clear" />
<input class="radio" type="checkbox" name="movie_extra_folder" value="1" <!--#if $movie_extra_folder > 0 then "checked=1" else ""#--> /> <strong>$T('opt-movieExtra')</strong>
<br class="clear" /><br class="clear" />
<strong>$T('affectedCat'):</strong>
<br class="clear" />
<select name="movie_cat" multiple="multiple" size=4 class="multiple_cats">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $ct in $movie_categories then 'selected' else ''#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
<br class="clear" /><br class="clear" />
<a href="#toggle" onclick="javascript:showDiv('Keytable2')">$T('sort-legenda')</a>
<div id="Keytable2">
<div class="row">
<span class="heading1">$T('sort-meaning')</span>
<span class="heading2">$T('sort-pattern')</span>
<span class="heading3">$T('sort-result')</span>
</div>
<div class="row">
<span class="tripleheight">$T('sort-title')</span>
<span class="pattern">%title</span>
<span class="result">$T('movie-sp-name')</span>
<br />
<span class="pattern">%.title</span>
<span class="result">$T('movie-dot-name')</span>
<br />
<span class="pattern">%_title</span>
<span class="result">$T('movie-us-name')</span>
</div>
<div class="row">
<span class="doubleheight">$T('year')</span>
<span class="pattern">%y</span>
<span class="result">2000</span>
</div>
<div class="row">
<span class="doubleheight">$T('extension')</span>
<span class="pattern">%ext</span>
<span class="result">avi</span>
</div>
<div class="row">
<span class="doubleheight">$T('partNumber')</span>
<span class="pattern">%1</span>
<span class="result">1</span>
</div>
<div class="row">
<span class="normalheight">$T('decade')</span>
<span class="pattern">%decade</span>
<span class="result">00</span>
</div>
<div class="row">
<span class="normalheight">$T('decade')</span>
<span class="pattern">%0decade</span>
<span class="result">2000</span>
</div>
<div class="row">
<span class="normalheight">$T('orgFilename')</span>
<span class="pattern">%fn</span>
<span class="result">$T('sort-File')</span>
</div>
<div class="row">
<span class="normalheight">$T('lowercase')</span>
<span class="pattern">{$T('TEXT')}</span>
<span class="result">$T('text')</span>
</div>
</div><br class="clear" /><br class="clear" />
<strong>$T('sortString'):</strong><br class="clear" />
<input type="text" style="width:400px" size="80" id="moviefoldername" name="movie_sort_string" value="$movie_sort_string" onkeyup="javascript:previewmovie()"><input type="button" onclick="movieClear()" style="width:100px;" value="$T('button-clear')"/><br class="clear" />
<strong>$T('multiPartLabel'):</strong><br class="clear" />
<input type="text" style="width:400px" size="80" id="movieextra" name="movie_sort_extra" value="$movie_sort_extra" onkeyup="javascript:previewmovie()"><input type="button" onclick="movieClearExtra()" style="width:100px;" value="$T('button-clear')"/><br class="clear" />
<strong>$T('presetSort'):</strong><br class="clear" />
<input type="button" onclick="movieSet('%title (%y)/%title (%y).%ext',' CD%1');movieExtraFolder(false)" value="$T('button-inFolders')"/>
<input type="button" onclick="movieSet('%title (%y).%ext',' CD%1');movieExtraFolder(true)" value="$T('button-noFolders')"/><br class="clear" />
<br class="clear" />
$T('example'): <span id="previewmovie"></span>
<br class="clear" /><br class="clear" />
</fieldset>
<br />
<fieldset class="EntryFieldSet">
<legend>$T('dateSorting')</legend>
<input class="radio" type="checkbox" name="enable_date_sorting" value="1" <!--#if $enable_date_sorting > 0 then "checked=1" else ""#--> /> <strong>$T('opt-dateSort')</strong><br class="clear" />
<br class="clear" />
<strong>$T('affectedCat'):</strong>
<br class="clear" />
<select name="date_cat" multiple="multiple" size=4 class="multiple_cats">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $ct in $date_categories then 'selected' else ''#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
<br class="clear" /><br class="clear" />
<a href="#toggle" onclick="javascript:showDiv('Keytable3')">$T('sort-legenda')</a>
<div id="Keytable3">
<div class="row">
<span class="heading1">$T('sort-meaning')</span>
<span class="heading2">$T('sort-pattern')</span>
<span class="heading3">$T('sort-result')</span>
</div>
<div class="row">
<span class="tripleheight">$T('show-name')</span>
<span class="pattern">%t</span>
<span class="result">$T('show-sp-name')</span>
<br />
<span class="pattern">%.t</span>
<span class="result">$T('show-dot-name')</span>
<br />
<span class="pattern">%_t</span>
<span class="result">$T('show-us-name')</span>
</div>
<div class="row">
<span class="doubleheight">$T('year')</span>
<span class="pattern">%y</span>
<span class="result">2009</span>
</div>
<div class="row">
<span class="doubleheight">$T('month')</span>
<span class="pattern">%m</span>
<span class="result">1</span>
<br />
<span class="pattern">%0m</span>
<span class="result">01</span>
</div>
<div class="row">
<span class="tripleheight">$T('day-of-month')</span>
<span class="pattern">%d</span>
<span class="result">2</span>
<br />
<span class="pattern">%0d</span>
<span class="result">02</span>
<br />
</div>
<div class="row">
<span class="normalheight">$T('decade')</span>
<span class="pattern">%decade</span>
<span class="result">00</span>
</div>
<div class="row">
<span class="normalheight">$T('decade')</span>
<span class="pattern">%0decade</span>
<span class="result">2000</span>
</div>
<div class="row">
<span class="normalheight">$T('orgFilename')</span>
<span class="pattern">%fn</span>
<span class="result">$T('sort-File')</span>
</div>
<div class="row">
<span class="normalheight">$T('lowercase')</span>
<span class="pattern">{$T('TEXT')}</span>
<span class="result">$T('text')</span>
</div>
</div><br class="clear" />
<strong>$T('sortString'):</strong><br class="clear" />
<input type="text" style="width:400px" size="80" id="datefoldername" name="date_sort_string" value="$date_sort_string" onkeyup="javascript:previewdate()"><input type="button" style="width:100px;" onclick="dateClear()" value="$T('button-clear')"/><br class="clear" />
<strong>$T('presetSort'):</strong><br class="clear" />
<input type="button" onclick="dateSet('%t/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-ShowNameF')"/>
<input type="button" onclick="dateSet('%y-%0m/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-YMF')"/>
<input type="button" onclick="dateSet('%y-%0m-%0d/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-DailyF')"/><br class="clear" />
<br class="clear" />
$T('example'): <span id="previewdate"></span>
</fieldset>
</div><br>
<input type="hidden" name="session" value="$session">
<input type="submit" size="40" value="$T('button-saveChanges')">
</form>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,189 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+Switches-0-7"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<!--#set global $submenu="switches"#-->
<!--#include $webdir + "/inc_cmenu.tmpl"#-->
<h2>$T('switchesConfig')</h2>
<form action="saveSwitches" method="post">
<div class="EntryBlock">
<fieldset class="EntryFieldSet">
<legend>$T('processingSwitches')</legend>
<label><input type="checkbox" name="quick_check" value="1" <!--#if $quick_check > 0 then "checked=1" else ""#--> /> <strong>$T('opt-quick_check')</strong></label><br/>
$T('explain-quick_check')<br>
<br/>
<label><input type="checkbox" name="pre_check" value="1" <!--#if $pre_check > 0 then "checked=1" else ""#--> /> <strong>$T('opt-pre_check')</strong></label><br/>
$T('explain-pre_check')<br>
<br/>
<label><input type="checkbox" name="enable_unrar" value="1" <!--#if $enable_unrar > 0 then "checked=1" else ""#--> /> <strong>$T('opt-enable_unrar')</strong></label><br>
$T('explain-enable_unrar')<br>
<br>
<label><input type="checkbox" name="enable_unzip" value="1" <!--#if $enable_unzip > 0 then "checked=1" else ""#--> /> <strong>$T('opt-enable_unzip')</strong></label><br>
$T('explain-enable_unzip')<br>
<br>
<label><input type="checkbox" name="enable_filejoin" value="1" <!--#if $enable_filejoin > 0 then "checked=1" else ""#--> /> <strong>$T('opt-enable_filejoin')</strong></label><br>
$T('explain-enable_filejoin')<br>
<br>
<label><input type="checkbox" name="enable_tsjoin" value="1" <!--#if $enable_tsjoin > 0 then "checked=1" else ""#--> /> <strong>$T('opt-enable_tsjoin')</strong></label><br>
$T('explain-ts_join')<br>
<br>
<label><input type="checkbox" name="enable_par_cleanup" value="1" <!--#if $enable_par_cleanup > 0 then "checked=1" else ""#--> /> <strong>$T('opt-enable_par_cleanup')</strong></label><br>
$T('explain-enable_par_cleanup')<br>
<br>
<label><input type="checkbox" name="fail_on_crc" value="1" <!--#if $fail_on_crc > 0 then "checked=1" else ""#--> /> <strong>$T('opt-fail_on_crc')</strong></label><br>
$T('explain-fail_on_crc')<br>
<br>
<label><input type="checkbox" name="top_only" value="1" <!--#if $top_only > 0 then "checked=1" else ""#--> /> <strong>$T('opt-top_only')</strong></label><br>
$T('explain-top_only')<br/>
<br>
<label><input type="checkbox" name="safe_postproc" value="1" <!--#if $safe_postproc > 0 then "checked=1" else ""#--> /> <strong>$T('opt-safe_postproc')</strong></label><br>
$T('explain-safe_postproc')<br>
<br>
<label><input type="checkbox" name="pause_on_pwrar" value="1" <!--#if $pause_on_pwrar > 0 then "checked=1" else ""#--> /> <strong>$T('opt-pause_on_pwrar')</strong></label><br>
$T('explain-pause_on_pwrar')<br>
<br>
<label><input type="checkbox" name="unpack_check" value="1" <!--#if $unpack_check > 0 then "checked=1" else ""#--> /> <strong>$T('opt-unpack_check')</strong></label><br>
$T('explain-unpack_check')<br>
<br>
<strong>$T('opt-no_dupes'):</strong><br>
$T('explain-no_dupes')<br>
<input class="radio" type="radio" name="no_dupes" value="0" <!--#if $no_dupes == 0 then 'checked="1"' else ""#--> /> $T('nodupes-off')
<input class="radio" type="radio" name="no_dupes" value="1" <!--#if $no_dupes == 1 then 'checked="1"' else ""#--> /> $T('nodupes-ignore')
<input class="radio" type="radio" name="no_dupes" value="2" <!--#if $no_dupes == 2 then 'checked="1"' else ""#--> /> $T('nodupes-pause')
<br/><br/>
<label><input type="checkbox" name="sfv_check" value="1" <!--#if $sfv_check > 0 then "checked=1" else ""#--> /> <strong>$T('opt-sfv_check')</strong></label><br>
$T('explain-sfv_check')<br>
<br>
<label><input type="checkbox" name="folder_rename" value="1" <!--#if $folder_rename > 0 then "checked=1" else ""#--> /> <strong>$T('opt-folder_rename')</strong></label><br>
$T('explain-folder_rename')<br>
<br>
<br>
<strong>$T('opt-pre_script'):</strong><br>
$T('explain-pre_script')<br>
<select name="pre_script">
<!--#for $sc in $script_list#-->
<!--#if $sc.lower() == $pre_script.lower()#-->
<option value="$sc" selected>$Tspec($sc)</option>
<!--#else#-->
<option value="$sc">$Tspec($sc)</option>
<!--#end if#-->
<!--#end for#-->
</select>
<br>
<br>
<!--#if $nt#-->
<label><input type="checkbox" name="par2_multicore" value="1" <!--#if $par2_multicore > 0 then "checked=1" else ""#--> /> <strong>$T('opt-par2_multicore')</strong></label><br>
$T('explain-par2_multicore')<br>
<br>
<!--#end if#-->
<strong>$T('opt-par_option'):</strong><br/>
$T('explain-par_option')<br/>
<input type="text" name="par_option" value="$par_option"/>
<br>
<br>
<!--#if $have_nice#-->
<strong>$T('opt-nice'):</strong><br/>
$T('explain-nice')<br/>
<input type="text" name="nice" value="$nice"/>
<br>
<br>
<!--#end if#-->
<!--#if $have_ionice#-->
<strong>$T('opt-ionice'):</strong><br/>
$T('explain-ionice')<br/>
<input type="text" name="ionice" value="$ionice"/>
<br>
<br>
<!--#end if#-->
</fieldset>
<fieldset class="EntryFieldSet">
<legend>$T('otherSwitches')</legend>
<label><strong>$T('opt-max_art_tries')</strong></label><br>
$T('explain-max_art_tries')<br>
<input type="text" size=5" name="max_art_tries" value="$max_art_tries" />
<br><br>
<label><input type="checkbox" name="max_art_opt" value="1" <!--#if $max_art_opt > 0 then "checked=1" else ""#--> /> <strong>$T('opt-max_art_opt')</strong></label><br>
$T('explain-max_art_opt')<br>
<br>
<label><input type="checkbox" name="auto_disconnect" value="1" <!--#if $auto_disconnect > 0 then "checked=1" else ""#--> /> <strong>$T('opt-auto_disconnect')</strong></label><br>
$T('explain-auto_disconnect')<br>
<br>
<label><input type="checkbox" name="send_group" value="1" <!--#if $send_group > 0 then "checked=1" else ""#--> /> <strong>$T('opt-send_group')</strong></label><br>
$T('explain-send_group')<br>
<br>
<label><input type="checkbox" name="auto_sort" value="1" <!--#if $auto_sort > 0 then "checked=1" else ""#--> /> <strong>$T('opt-auto_sort')</strong></label><br>
$T('explain-auto_sort')<br>
<br>
<label><input type="checkbox" name="check_new_rel" value="1" <!--#if $check_new_rel > 0 then "checked=1" else ""#--> /> <strong>$T('opt-check_new_rel')</strong></label><br>
$T('explain-check_new_rel')<br>
<br>
<label><input type="checkbox" name="replace_spaces" value="1" <!--#if $replace_spaces > 0 then "checked=1" else ""#--> /> <strong>$T('opt-replace_spaces')</strong></label><br>
$T('explain-replace_spaces')<br>
<br>
<label><input type="checkbox" name="replace_dots" value="1" <!--#if $replace_dots > 0 then "checked=1" else ""#--> /> <strong>$T('opt-replace_dots')</strong></label><br>
$T('explain-replace_dots')<br>
<br>
<label><input type="checkbox" name="replace_illegal" value="1" <!--#if $replace_illegal > 0 then "checked=1" else ""#--> /> <strong>$T('opt-replace_illegal')</strong></label><br>
$T('explain-replace_illegal')<br>
<br>
<label><input type="checkbox" name="auto_browser" value="1" <!--#if $auto_browser > 0 then "checked=1" else ""#--> /> <strong>$T('opt-auto_browser')</strong></label><br>
$T('explain-auto_browser')<br>
<br>
<label><input type="checkbox" name="pause_on_post_processing" value="1" <!--#if $pause_on_post_processing > 0 then "checked=1" else ""#--> /> <strong>$T('opt-pause_on_post_processing')</strong></label><br>
$T('explain-pause_on_post_processing')<br>
<br/>
<!--#if $have_ampm#-->
<label><input type="checkbox" name="ampm" value="1" <!--#if $ampm > 0 then "checked=1" else ""#--> /> <strong>$T('opt-ampm')</strong></label><br>
$T('explain-ampm')<br>
<br/>
<!--#end if#-->
<strong>$T('opt-ignore_samples'):</strong><br>
$T('explain-ignore_samples')<br>
<input class="radio" type="radio" name="ignore_samples" value="0" <!--#if $ignore_samples == 0 then 'checked="1"' else ""#--> /> $T('igsam-off')
<input class="radio" type="radio" name="ignore_samples" value="1" <!--#if $ignore_samples == 1 then 'checked="1"' else ""#--> /> $T('igsam-del')
<input class="radio" type="radio" name="ignore_samples" value="2" <!--#if $ignore_samples == 2 then 'checked="1"' else ""#--> /> $T('igsam-not')
<br/>
<br/>
<input type="hidden" name="session" value="$session">
<strong>$T('opt-ssl_type'):</strong><br>
$T('explain-ssl_type')<br>
<select name="ssl_type">
<option value="v23" <!--#if $ssl_type == 'v23' then 'selected' else ''#--> >V23</option>
<option value="v2" <!--#if $ssl_type == 'v2' then 'selected' else ''#--> >V2</option>
<option value="v3" <!--#if $ssl_type == 'v3' then 'selected' else ''#--> >V3</option>
</select>
<br/>
</fieldset>
<fieldset class="EntryFieldSet">
<strong>$T('opt-quota_size'):</strong><br/>
$T('explain-quota_size')<br/>
<input type="text" name="quota_size" value="$quota_size"/>
<br>
<br>
<strong>$T('opt-quota_period'):</strong><br>
$T('explain-quota_period')<br>
<select name="ssl_type">
<option value="m" <!--#if $quota_period == 'm' then 'selected' else ''#--> >$T('month')</option>
<option value="w" <!--#if $quota_period == 'w' then 'selected' else ''#--> >$T('week')</option>
<option value="d" <!--#if $quota_period == 'd' then 'selected' else ''#--> >$T('day')</option>
<option value="x" <!--#if $quota_period == 'x' then 'selected' else ''#--> >$T('manual')</option>
</select>
<br/><br/>
<strong>$T('opt-quota_day'):</strong><br/>
$T('explain-quota_day')<br/>
<input type="text" name="quota_day" value="$quota_day"/>
<br>
<br>
<legend>$T('swtag-quota')</legend>
<label><input type="checkbox" name="quota_resume" value="1" <!--#if $quota_resume > 0 then "checked=1" else ""#--> /> <strong>$T('opt-quota_resume')</strong></label><br>
$T('explain-quota_resume')<br>
<br/>
</fieldset>
</div>
<p><input type="submit" value="$T('button-saveChanges')"></p>
</form>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,100 +0,0 @@
<!--#set global $topmenu="history"#-->
<!--#set global $statpath=".."#-->
<!--#set global $helpsubject="GUI+History"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<span class="SubMenu">
<a href="./purge?session=$session" onclick="return confirm('$T('purgeHistConf').replace("'","`") ');">$T('purgeHist')</a> |
<a href="./purge_failed?session=$session" onclick="return confirm('$T('purgeHistFailedConf').replace("'","`") ');">$T('purgeHistFailed')</a> |
<a href="./purge_failed?session=$session&del_files=1" onclick="return confirm('$T('purgeFailed-Files').replace("'","`") ');">$T('purgeFailed-Files')</a> |
<!--#if $isverbose#-->
<a href="./tog_verbose?session=$session">$T('hideDetails')</a> |
<!--#else#-->
<a href="./tog_verbose?session=$session">$T('showDetails')</a> |
<!--#end if#-->
<!--#if $failed_only#-->
<a href="./tog_failed_only?session=$session">$T('showAllHis')</a>
<!--#else#-->
<a href="./tog_failed_only?session=$session">$T('showFailedHis')</a>
<!--#end if#-->
</span>
<br>
<p>
<strong>$T('sizeHist'): $total_size&nbsp;&nbsp;|&nbsp;&nbsp;$T('today'): $day_size&nbsp;&nbsp;|&nbsp;&nbsp;
$T('thisWeek'): $week_size&nbsp;&nbsp;|&nbsp;&nbsp;$T('thisMonth'): $month_size
</strong>
</p>
<% import datetime %>
<% from sabnzbd.misc import time_format %>
<!--#if $lines#-->
<table id="historyTable">
<tr><th></th><th>$T('completed')</th><th>$T('name')</th><th>$T('size')</th><th>$T('status')</th><th></th></tr>
<!--#set $odd = False#-->
<!--#for $line in $lines #-->
<%
compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_format('%Y-%m-%d %H:%M:%S'))
%>
<!--#set $odd = not $odd#-->
<tr class="<!--#if $odd then "odd" else "even"#-->">
<td><a class="verbosity_link" href="./tog_verbose?session=$session&jobs=$line.nzo_id">
<!--#if $line.show_details then '-' else '+'#-->
</a></td>
<td>$compl</td>
<td>$line.name<!--#if $line.action_line#--> - $line.action_line<!--#else if $line.fail_message#--> - <span class="fail_message">$line.fail_message</span><!--#end if#--></td>
<td>$line.size</td><td>$Tx('post-'+$line.status)</td>
<td>
<!--#if not $line.loaded#-->
<!--#if $line.retry#-->
<input type="submit" onclick="if(confirm('$T('confirm').replace("'","`") ')){ if (confirm('$T('delFiles').replace("'","`") ')) window.location='delete?job=$line.nzo_id&del_files=1&session=$session'; else window.location='delete?job=$line.nzo_id&del_files=0&session=$session'; return false;}" value="$T('button-del')">
<!--#else#-->
<form action="delete" method="get">
<input type="hidden" value="$line.nzo_id" name="job">
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-del')"></form>
<!--#end if#-->
<!--#end if#-->
<br/>
<!--#if $line.retry#-->
<form action="retry_pp" method="post" enctype="multipart/form-data">
<input type="file" name="nzbfile">
<input type="hidden" value="$line.nzo_id" name="job">
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('button-retry')"></form>
<!--#end if#-->
</td>
</tr>
<!--#if $line.show_details#-->
<!--#set $oddLine = not False#-->
<tr class="<!--#if $oddLine then "oddLine" else "evenLine"#-->"><td></td><td></td>
<td colspan="3"><dl>
<!--#for $stage in $line.stage_log#-->
<!--#set $oddLine = not $oddLine#-->
<dt><b>$Tx('stage-'+$stage.name.title.lower())</b></dt>
<!--#for $action in $stage.actions#-->
<dd>$action</dd>
<!--#end for#-->
<br />
<!--#end for#-->
<!--#if $line.category#-->
<dt><b>$T('category')</b></dt>
<dd>$line.category</dd>
<br />
<!--#end if#-->
<!--#if $line.storage#-->
<dt><b>$T('msg-path')</b></dt>
<dd>$line.storage</dd>
<br />
<!--#end if#-->
</dl>
</td>
<td></td>
</tr>
<!--#end if#-->
<!--#end for#-->
</table>
<!--#end if#-->
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,35 +0,0 @@
<div class="footer">
<b>$T('ft-download'):</b> $diskspace1 GB - <b>$T('ft-complete'):</b> $diskspace2 GB - <b>$T('ft-speed'):</b> ${speed}B/s - <b>$T('ft-queued'):</b> $sizeleft / $size
<!--#if $loadavg#-->
- <b>$T('ft-sysload')</b> $loadavg
<!--#end if#-->
<!--#if $paused#-->
- <b>$T('ft-paused')</b><!--#if $pause_int != "0"#-->($pause_int)<!--#end if#--><!--#if $paused_all#--><b>!</b><!--#end if#-->
<!--#end if#-->
<br>
<!--#if int($cache_max)#-->
<!--#set $msg=$T('ft-buffer@2')%($cache_art, $cache_size)#-->
$msg&nbsp;&nbsp;
<!--#end if#-->
<!--#if $have_quota#-->
| &nbsp;&nbsp;<strong>$T('quota-left'):</strong>&nbsp;&nbsp;$left_quota&nbsp;&frasl;&nbsp;$quota</strong>&nbsp;&nbsp;&nbsp;
<!--#end if#-->
<!--#if $new_release#-->
<!--#set $msg=$T('ft-newRelease@1')%($new_release)#-->
<b>$msg <a href="$new_rel_url/" target="_blank">SF.net</a></b><br>
<!--#end if#-->
<!--#if $have_warnings != "0"#-->
<strong><a href="$statpath/status/">$T('ft-warning')($have_warnings)!</a></strong>
<!--#end if#-->
</div>
<!--#set $mbleftrnd = str(int(float($mbleft)))#-->
<!--#set $mbrnd = str(int(float($mb)))#-->
<div id='SABData' style='visibility: hidden'>
<b>&nbsp;SAB:</b> <!--#if $paused then "<blink><b>$T('ft-paused')</b></blink>" else "${speed}B/s"#--> <b>Q:</b> $mbleftrnd/$mbrnd MB<b>&nbsp; Comp:</b> $diskspace2 GB
</div>
<!--#if self.varExists('warning') and $warning#-->
<h2>$T('ft-warning')</h2>
<b>$warning</b>
<!--#end if#-->
</body>
</html>

View File

@@ -1,68 +0,0 @@
<!--#if $submenu == ""#-->
<!--#set $cpath="."#-->
<!--#else#-->
<!--#set $cpath=".."#-->
<!--#end if#-->
<span class="SubMenu">
<!--#if $submenu=="general"#-->
<a class="current" href="./">$T('cmenu-general')</a> |
<!--#else#-->
<a href="$cpath/general/">$T('cmenu-general')</a> |
<!--#end if#-->
<!--#if $submenu=="folders"#-->
<a class="current" href="./">$T('cmenu-folders')</a> |
<!--#else#-->
<a href="$cpath/folders/">$T('cmenu-folders')</a> |
<!--#end if#-->
<!--#if $submenu=="switches"#-->
<a class="current" href="./">$T('cmenu-switches')</a> |
<!--#else#-->
<a href="$cpath/switches/">$T('cmenu-switches')</a> |
<!--#end if#-->
<!--#if $submenu=="servers"#-->
<a class="current" href="./">$T('cmenu-servers')</a> |
<!--#else#-->
<a href="$cpath/server/">$T('cmenu-servers')</a> |
<!--#end if#-->
<!--#if $submenu=="scheduling"#-->
<a class="current" href="./">$T('cmenu-scheduling')</a> |
<!--#else#-->
<a href="$cpath/scheduling/">$T('cmenu-scheduling')</a> |
<!--#end if#-->
<!--#if $submenu=="rss"#-->
<a class="current" href="./">$T('cmenu-rss')</a> |
<!--#else#-->
<a href="$cpath/rss/">$T('cmenu-rss')</a> |
<!--#end if#-->
<!--#if $submenu=="notify"#-->
<a class="current" href="./">$T('cmenu-notif')</a> |
<!--#else#-->
<a href="$cpath/notify/">$T('cmenu-notif')</a> |
<!--#end if#-->
<!--#if $submenu=="indexers"#-->
<a class="current" href="./">$T('cmenu-newzbin')</a> |
<!--#else#-->
<a href="$cpath/indexers/">$T('cmenu-newzbin')</a> |
<!--#end if#-->
<!--#if $submenu=="categories"#-->
<a class="current" href="./">$T('cmenu-cat')</a> |
<!--#else#-->
<a href="$cpath/categories/">$T('cmenu-cat')</a> |
<!--#end if#-->
<!--#if $submenu=="sorting"#-->
<a class="current" href="./">$T('cmenu-sorting')</a>
<!--#else#-->
<a href="$cpath/sorting/">$T('cmenu-sorting')</a>
<!--#end if#-->
</span>

View File

@@ -1,67 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<html>
<head>
<title>$mbleft MB $T('queued') - SABnzbd $version</title>
<link rel="stylesheet" type="text/css" href="$statpath/static/stylesheets/default.css"/>
<link rel="stylesheet" type="text/css" href="$statpath/static/stylesheets/defaultcolors.css"/>
<!--#if $color_scheme#-->
<link rel="stylesheet" type="text/css" href="$statpath/static/stylesheets/colorschemes/${color_scheme}.css"/>
<!--#end if#-->
<!--#if $varExists('have_refresh') and $have_refresh#-->
<meta http-equiv="refresh" content="$have_refresh">
<!--#end if#-->
<link rel="shortcut icon" href="$statpath/static/images/favicon.ico" />
<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="rss?mode=history&apikey=$session"/>
</head>
<body>
<h1><span id="first">SAB</span><span id="second">nzbd+</span><span id="third"> $version</span><span id="catchfrase">$T('signOn')</span></h1>
<!--#set $mypath=$statpath+"/"+$topmenu#-->
<span class="MainMenu">
<!--#if $topmenu==''#-->
<a class="current" href="$mypath">$T('menu-home')</a> |
<!--#else#-->
<a href="$statpath">$T('menu-home')</a> |
<!--#end if#-->
<!--#if $topmenu=='queue'#-->
<a class="current" href="$mypath/">$T('menu-queue')</a> |
<!--#else#-->
<a href="$statpath/queue/">$T('menu-queue')</a> |
<!--#end if#-->
<!--#if $topmenu=='history'#-->
<a class="current" href="$mypath/">$T('menu-history')</a> |
<!--#else#-->
<a href="$statpath/history/">$T('menu-history')</a> |
<!--#end if#-->
<!--#if $topmenu=='config'#-->
<a class="current" href="$mypath/">$T('menu-config')</a> |
<!--#else#-->
<a href="$statpath/config/">$T('menu-config')</a> |
<!--#end if#-->
<!--#if $topmenu=='connections'#-->
<a class="current" href="$mypath/">$T('menu-cons')</a> |
<!--#else#-->
<a href="$statpath/status/">$T('menu-cons')</a> |
<!--#end if#-->
<!--[if IE]>
<a href="$helpuri$helpsubject/" target="_blank">$T('menu-help')</a>
<![endif]-->
<comment>
<ul id="nav">
<li><a href="$helpuri$helpsubject/" target="_blank">$T('menu-help')</a>
<ul>
<li><a href="$helpuri$helpsubject/" target="_blank">$T('menu-wiki')</a></li>
<li><a href="http://forums.sabnzbd.org/" target="_blank">$T('menu-forums')</a></li>
<li><a href="http://www.sabnzbd.org/live-chat/" target="_blank">$T('menu-irc')</a></li>
</ul>
</li>
</ul>
</comment>
</span>

View File

@@ -1,113 +0,0 @@
<!--#set global $topmenu=""#-->
<!--#set global $statpath="."#-->
<!--#set global $helpsubject="GUI+Main"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<span class="SubMenu">
<!--#if $paused#-->
<a href="./resume?session=$session">$T('link-resume')</a>
<!--#else#-->
<a href="./pause?session=$session">$T('link-pause')</a>
<!--#end if#-->|
<!--#set $shutMsg = "'" + $T('shutdownOK?') + "'" #-->
<a href="./shutdown?session=$session" onclick="javascript:return confirm($shutMsg)">$T('link-shutdown')</a>
</span>
<h2>$T("addNewJobs")</h2>
<div class="EntryBlock">
<form action="addID" method="get">
<fieldset class="EntryFieldSet">
<legend>$T('add')
<!--#if $varExists('newzbinDetails')#--> $T('reportId') / <!--#end if#-->URL</legend>
<input type="text" name="id">
<!--#if $cat_list#-->
<select name="cat" >
<optgroup label="$T('category')">
<!--#for $ct in $cat_list#-->
<option value="$ct">$Tspec($ct)</option>
<!--#end for#-->
</optgroup>
</select>
<!--#end if#-->
<select name="pp">
<optgroup label="$T('pp')">
<option value="-1" <!--#if $cat_list then "selected" else ""#-->>$T('default')</option>
<option value="0">$T('none')</option>
<option value="1">$T('pp-repair')</option>
<option value="2">$T('pp-unpack')</option>
<option value="3" <!--#if $cat_list then "" else "selected"#-->>$T('pp-delete')</option>
</optgroup>
</select>
<!--#if $script_list#-->
<select name="script">
<optgroup label="$T('script')">
<!--#for $sc in $script_list#-->
<option value="$sc">$Tspec($sc)</option>
<!--#end for#-->
</optgroup>
</select>
<!--#end if#-->
<select name="priority">
<optgroup label="$T('priority')">
<option value="-100" selected>$T('default')</option>
<option value="2">$T('pr-force')</option>
<option value="1">$T('pr-high')</option>
<option value="0">$T('pr-normal')</option>
<option value="-1">$T('pr-low')</option>
</optgroup>
</select>
<br>
<input type="hidden" name="session" value="$session">
<input type="submit" value=$T("button-add")>
</fieldset>
</form>
</div>
<div class="EntryBlock">
<form action="addFile" method="post" enctype="multipart/form-data">
<fieldset class="EntryFieldSet">
<legend>$T('addFile')</legend>
<input type="file" name="nzbfile">
<!--#if $cat_list#-->
<select name="cat" >
<optgroup label="Category">
<!--#for $ct in $cat_list#-->
<option value="$ct">$Tspec($ct)</option>
<!--#end for#-->
</optgroup>
</select>
<!--#end if#-->
<select name="pp">
<optgroup label="$T('pp')">
<option value="-1" <!--#if $cat_list then "selected" else ""#-->>$T('default')</option>
<option value="0">$T('none')</option>
<option value="1">$T('pp-repair')</option>
<option value="2">$T('pp-unpack')</option>
<option value="3" <!--#if $cat_list then "" else "selected"#-->>$T('pp-delete')</option>
</optgroup>
</select>
<!--#if $script_list#-->
<select name="script">
<optgroup label="Script">
<!--#for $sc in $script_list#-->
<option value="$sc">$Tspec($sc)</option>
<!--#end for#-->
</optgroup>
</select>
<!--#end if#-->
<select name="priority">
<optgroup label="$T('priority')">
<option value="-100" selected>$T('default')</option>
<option value="2">$T('pr-force')</option>
<option value="1">$T('pr-high')</option>
<option value="0">$T('pr-normal')</option>
<option value="-1">$T('pr-low')</option>
</optgroup>
</select>
<br>
<input type="hidden" name="session" value="$session">
<input type="submit" value=$T("button-add")>
</fieldset>
</form>
</div>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

Some files were not shown because too many files have changed in this diff Show More