Compare commits

..

2486 Commits
0.6.2 ... 1.2.1

Author SHA1 Message Date
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
7170325df5 Update text files for 0.7.2 Final 2012-07-17 21:46:17 +02:00
ShyPike
c44d98da66 When localhost resolves to ['127.0.0.1', '::1'] instead of ['::1', '127.0.0.1'], SABnzbd would try to register twice on '::1'. 2012-07-17 21:29:33 +02:00
ShyPike
018410afb0 Validate the values read from totals9.sab 2012-07-17 19:38:49 +02:00
ShyPike
fc47238a7a Temporary fix for missing NZB icon when associating NZB files on a system that had a 0.7.0 release installed. 2012-07-16 18:51:39 +02:00
ShyPike
9561b8a64e Update text files for 0.7.2 RC2 2012-07-14 17:01:07 +02:00
ShyPike
7b0e56b55f Fix little anomaly in Wizard-Four. 2012-07-14 15:16:30 +02:00
ShyPike
c6d5a79776 Improve web-host address selection on systems where the hostname does not resolve to an IP address. 2012-07-14 14:27:18 +02:00
ShyPike
faa4cacd3e Improve detection of bad articles.
For now, don't let precheck use "HEAD" until method is improved.
2012-07-14 13:12:22 +02:00
ShyPike
56e417eea1 Change nzbsrus support to use their API response codes. 2012-07-14 09:40:46 +02:00
ShyPike
5f02ec00f9 Don't show nonsense NZB age when still trying to get it from an indexer. 2012-07-13 21:29:37 +02:00
ShyPike
5ea35db922 Add limited API support for nzbsrus.com 2012-07-13 21:19:09 +02:00
ShyPike
5dcf26a56c Update text files for 0.7.2RC1 2012-07-12 21:45:13 +02:00
ShyPike
35b598d10e Improve detection (and ignoring) of invalid articles.
This also requires reading headers during pre-check instead of just asking for article presence.
2012-07-12 21:24:38 +02:00
ShyPike
5e7b27c4ef Prevent potential crash in NzbQueue.repair_job() 2012-07-12 18:28:17 +02:00
ShyPike
9ed408d35b Windows Installer: don't uninstall settings by default. 2012-07-12 18:19:19 +02:00
ShyPike
6c782fe255 Fix nzbrus.com fatal error (when <nzb is not within the first 100 characters of the file). 2012-07-10 23:01:11 +02:00
ShyPike
1689323dc3 Fix sorting of files in a job so that .rar comes before .r00 again. 2012-07-10 22:12:04 +02:00
ShyPike
a3c50a907a Fix wrapping problem of queue titles in Plush skin by inserting zero-width spaces in titles. 2012-07-10 19:59:59 +02:00
ShyPike
36a3792846 After setting quota for the first time, the initial "quota left" was set to the already consumed amount of the current period, instead of the actual still available amount (which is quota-consumed). 2012-07-08 11:45:09 +02:00
ShyPike
4cd0c0691a Windows installer: fix NZB association so that a Windows reboot is not required to register the NZB icon. 2012-07-08 11:33:57 +02:00
ShyPike
6ac98dcacd Handle incorrect regular expression in RSS filters.
Ignore during scanning and signal in user interface.
2012-07-08 10:45:57 +02:00
ShyPike
cadb4afe7c Merge from master (release 0.7.1 Final). 2012-07-06 21:29:33 +02:00
ShyPike
0a0d00930a Update text files for 0.7.1 Final. 2012-07-06 16:50:00 +02:00
ShyPike
28a0d041f9 Correction on fix 7258e56a20.
nzf.completed is a @property attribute and should not be set directly.
2012-07-06 16:40:36 +02:00
ShyPike
85bb91a7ea Disable VC90 check in Windows Installer as long as we're still on Python 2.5 2012-07-04 21:10:49 +02:00
ShyPike
6561e0abfa Make Windows path joining of base and category paths more robust to work around different behaviors in Python and Windows versions combinations. 2012-07-04 20:36:52 +02:00
ShyPike
6715e61a68 Try to check for "maintenance" mode of nzbsrus.com 2012-07-04 19:34:00 +02:00
ShyPike
a886b284b6 Prevent "not committed" flag in baseline variable for official builds. 2012-07-04 19:09:55 +02:00
ShyPike
a349c82b6f Update text files for 0.7.1RC5 2012-07-03 21:38:20 +02:00
ShyPike
1f4df0ebf4 Only send "bad fetch" emails when emails are enabled. 2012-07-03 21:28:59 +02:00
ShyPike
0221e7bf93 Add some support for nzbsrus.com's awkward non-VIP limiting. 2012-07-03 21:14:30 +02:00
ShyPike
f9cf14e7d8 Update text files for 0.7.1 Final. 2012-07-03 19:59:36 +02:00
ShyPike
7258e56a20 When retrying a job, existing files were not flagged properly as "completed", leading to an endless loop in par2 fetching. 2012-07-03 19:58:02 +02:00
ShyPike
90bd495d44 Update text files fr 0.7.1 Final. 2012-07-03 17:54:18 +02:00
shypike
6c216d6dfe Update translations 2012-07-03 17:49:10 +02:00
ShyPike
e1f3fae6c7 Windows installer: detect incompatible older version by looking for python27.dll. 2012-07-02 22:31:28 +02:00
ShyPike
29f126ca47 Make warning about Python3 easier to read. 2012-07-02 22:13:01 +02:00
shypike
8b4b742466 Remove remaining .py files from OSX DMG image, to prevent invalid signatures after first run. 2012-07-02 18:44:36 +02:00
ShyPike
57a9d362bc Update text files for 0.7.1RC4 2012-07-01 17:26:00 +02:00
ShyPike
b7d54c2bea Fix failure to grab NZBs from indexers that send compressed files. 2012-07-01 17:23:16 +02:00
ShyPike
59f9833076 Update text files for 0.7.1 RC3 2012-07-01 14:03:59 +02:00
ShyPike
8e360fe53e Improve retry handling of URL fetches.
Add detection of error messages from nzbsrus.com and handle retries accordingly.
2012-07-01 13:49:24 +02:00
ShyPike
afc5005382 Make sure that badly formatted or otherwise incorrect articles are retried on other servers (if fail_on_crc option is on).
Make default of fail_on_crc True.
2012-07-01 13:47:55 +02:00
ShyPike
3a531c6d2b API calls "addurl" and "addid" can use the same code so that either will take newzbin IDs or regular URLs. 2012-07-01 12:53:54 +02:00
ShyPike
f056ad6347 Solve problem of stalling par2 fetching.
The attributes of the vol-par files were analyzed to late,
leading to a race condition.
2012-07-01 10:01:56 +02:00
ShyPike
5c1342a663 Tell user that Python 3.0 won't work. 2012-07-01 09:12:25 +02:00
ShyPike
dfe8a47a2a Verification/repair would not be executed properly when one more RAR files missed their first article. 2012-06-26 23:26:38 +02:00
ShyPike
e293a439dd Don't set __verified__ flag file when more par2 files need to be fetched. 2012-06-26 22:24:01 +02:00
ShyPike
7e0027922a Prevent QuickCheck crash when expected par2 file wasn't downloaded (due to missing articles). 2012-06-26 19:12:56 +02:00
shypike
00b5302ba9 Update text files for 0.7.1 Final. 2012-06-25 22:40:39 +02:00
shypike
79488c4785 Add option to suppress listening on web host address ::1 2012-06-25 22:34:07 +02:00
ShyPike
c3d0438250 Update texts for 0.7.1 RC2 2012-06-23 12:31:14 +02:00
ShyPike
2909d4636b Fix parameter bug in Swedish translation. 2012-06-23 12:28:57 +02:00
shypike
a1ee8b6af4 Update translations 2012-06-23 12:20:05 +02:00
shypike
e52326f4ac Update translations 2012-06-23 12:15:54 +02:00
ShyPike
cfe3b58f7f On Windows, the Python runtime starts the wrong browser when using http://::1:8080/sabnzbd. Convert URL to http://[::1]:8080/sabnzbd. 2012-06-23 12:05:38 +02:00
ShyPike
ff6b87ef5b Improve the INI file handling.
Simplify and make the save more robust.
Use the INI.BAK file when the INI file is missing or corrupt.
2012-06-21 21:46:14 +02:00
ShyPike
8fbcfd0d5a Update text files for 0.7.1RC1 2012-06-20 20:54:53 +02:00
shypike
347ba999b4 Reduce amount of info requested when updating Windows Tray icon and OSX top menu.
The Windows Tray icon queried for free diskspace, while it is never shown.
The OSX top menu should query for no more queue entries than it will show (10).
The OSX top menu queue didn't show accented characters properly.
2012-06-20 20:48:55 +02:00
ShyPike
503bcf64c9 Prevent problems on systems that don't bother to resolve "localhost". 2012-06-19 22:31:47 +02:00
ShyPike
2a667470a1 Add retries when creating "final folder" to allow for slow activation of mounted volumes. 2012-06-19 19:05:33 +02:00
ShyPike
c61165b840 Plush: fix Speedlimit, Pause and Options pulldown menus for Mobile Safari. 2012-06-18 22:17:09 +02:00
ShyPike
3672189bc8 Remove another unwanted Windows DLL when building on Windows 7. 2012-06-16 13:41:44 +02:00
ShyPike
5b38c772fb When the "uname -n" name of a system doesn't resolve to an IP, only use 0.0.0.0 as hostname when the user hadn't set "localhost", "127.0.0.1" or "::1" as the hostname.
Otherwise any such system will still listen on the external IP despite the fact that user didn't want this.
2012-06-16 12:18:12 +02:00
ShyPike
f28bc4dd9c Windows: the installer did not set an icon for NZB files (association). 2012-06-16 10:45:04 +02:00
ShyPike
e16cc49a17 Config-skin: "server" field in Config->Servers should have html5 tag "text" instead of "url". 2012-06-16 10:44:48 +02:00
ShyPike
f686cc94fd Modify server test to please very critical Usenet server. 2012-06-16 10:44:32 +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
ShyPike
d88b5a3b3e Restore proper support for Python 2.5 on Windows by including curl tool again. 2012-06-10 15:35:56 +02:00
shypike
bccc5665f5 Prevent Growl crash at shutdown, due to accented characters in message. 2012-06-10 15:03:56 +02:00
ShyPike
69ac9d39ad Update text files for release 0.7.0Final. 2012-06-09 10:36:31 +02:00
ShyPike
b5b6999bc9 Default value of cache_limit should remain empty. 2012-06-09 10:25:09 +02:00
shypike
7ddb3d2752 Update translations 2012-06-08 18:49:31 +02:00
ShyPike
9febaf919c Update translations. 2012-06-06 17:34:12 +02:00
shypike
2225383485 Removed gntp and gnutext as required modules, because they are included in the distribution. 2012-06-06 17:36:32 +03:00
shypike
e3e500326c Update translations 2012-06-05 19:16:06 +02:00
ShyPike
b73570fe2a Update text files for 0.7.0 RC2 2012-06-02 21:24:08 +02:00
shypike
d1357875c9 Update translations 2012-06-02 21:23:33 +02:00
ShyPike
037c7661ea Suppress permission errors on paths containing ".AppleDouble" or ".DS_Store".
Common on NAS systems that support OSX with AFP shares.
2012-06-02 21:17:13 +02:00
ShyPike
5337ade3fb Set article cache to 200M when not already set. 2012-06-02 15:57:08 +02:00
ShyPike
504ce5458f Pre-check: lower default minimum completion rate to 100.2% 2012-06-01 19:43:07 +02:00
ShyPike
d8d3b60cbc Update text files for 0.7.0 RC1 2012-05-30 21:32:02 +02:00
ShyPike
0b0e7d5531 Pre-check: the required ratio for NZBs without par2 files should be 100% and not the "safe" ratio. 2012-05-30 21:28:24 +02:00
ShyPike
18cd9ab7ca Update text files for 0.7.0 RC1 2012-05-30 20:32:17 +02:00
shypike
1392b3b1eb Update translations 2012-05-30 20:29:56 +02:00
ShyPike
cd93abfab1 Fix for rare crash in par2 fetching. 2012-05-30 20:09:16 +02:00
ShyPike
9fede00949 Fix failing nomedia marker file (again). 2012-05-27 21:10:26 +02:00
ShyPike
6ddc3fec96 Windows SystemTray menu code sometimes times out when coming out of standby/hibernate. Catch exception and hope for the best... 2012-05-27 20:53:04 +02:00
ShyPike
f1030f9b6f Quota reset wasn't done when quota-reset-time was passed while SABnzbd wasn't running.
Due to not setting self.have_quota before reading the totals9.sab file.
2012-05-27 12:46:45 +02:00
shypike
6a8ff22f96 Fix error message. 2012-05-26 09:06:31 +02:00
ShyPike
1e82b79c16 Update text files for 0.7.0 Beta8. 2012-05-25 20:26:58 +02:00
shypike
d85bc90cbb Update translations 2012-05-25 20:22:25 +02:00
ShyPike
fdce48376a Add completion rate info to email notifying about failed pre-check run. 2012-05-25 20:11:55 +02:00
ShyPike
58cc6b2e7d Rename special option "marker_file" to "nomedia_marker" and disable it by default. 2012-05-25 19:48:10 +02:00
ShyPike
5ebad3cb70 Fix signing of OSX Leopard app. 2012-05-22 20:21:47 +02:00
ShyPike
d4757a0a74 Update text files for 0.7.0 Beta7 2012-05-22 19:57:15 +02:00
shypike
0406a4b901 Update translations 2012-05-22 19:53:34 +02:00
ShyPike
95684fbe3a Do removal of ".nomedia" marker file earlier, just after unpacking. So before it can be moved by the Sorting functions.
This also solves the problem of the NZB-chaining being disabled.
2012-05-22 19:39:23 +02:00
ShyPike
a4f8155138 Prevent crash when Wizard tries to resolve the system's name to an IP (and name cannot resolve). 2012-05-22 19:09:00 +02:00
ShyPike
cddb1ba7f3 Remove junk code. 2012-05-21 18:41:39 +02:00
ShyPike
73b2930a0b Remove potential exe.log file in Windows uninstaller. 2012-05-21 18:40:48 +02:00
ShyPike
e74519ca43 When the Sort functions look to rename auxiliary files after a main file, they should ignore casing of the original names. 2012-05-20 14:09:07 +02:00
ShyPike
f75fad9c6a Add Polish to NSIS script. 2012-05-18 14:12:48 +02:00
ShyPike
2d8805a49f Spend more effort to track down and delete the .nomedia marker file at the end of post-processing. 2012-05-18 12:45:21 +02:00
ShyPike
fc3a19a816 Translatable hover-over texts for Windows Tray icon. 2012-05-18 11:31:18 +02:00
ShyPike
e1b2d49341 Update POT files. 2012-05-17 22:37:18 +02:00
ShyPike
2cb6890dac Add two Wizard texts for later Unicode release. 2012-05-17 22:36:07 +02:00
ShyPike
e6bef729ed Add some type safety to database.py (preparing for Unicode). 2012-05-17 14:26:15 +02:00
ShyPike
1545f7a4ef Handle newline idiosyncrasies of gettext for NSIS template. 2012-05-17 14:24:59 +02:00
ShyPike
3f9446a336 Update text files for 0.7.0 Beta6 2012-05-17 10:44:46 +02:00
shypike
7b3ab90a95 Update translations 2012-05-16 22:51:58 +02:00
shypike
636b264432 OSX: Make "10.1 MB/s" fit in menu bar status by omitting the space. 2012-05-16 22:30:33 +02:00
ShyPike
59fd6bf23d Lower default threshold for pre-check to 100.5% 2012-05-16 20:33:31 +02:00
ShyPike
59b4bbcdaa Don't retry bookmark removals for nzbmatrix.com.
Currently it causes a crash, but otherwise the site would be hammered.
2012-05-16 17:50:36 +02:00
ShyPike
a82bdd9f74 Always use URL when complaining about unsuccessful NZB fetches. 2012-05-15 20:10:49 +02:00
ShyPike
a4f10e7577 Prevent crash in rare case of a timeout on a connection that has just been cleared by the "unreliable-server" detection. 2012-05-15 19:32:19 +02:00
shypike
71f3231487 Update translations 2012-05-14 18:05:30 +02:00
ShyPike
2133092402 Add Polish translation (with reduced character set). 2012-05-14 17:58:08 +02:00
ShyPike
20e47ab099 Use readable substitutions for quote and apostrophe. 2012-05-13 14:27:40 +02:00
ShyPike
3e9c8c3ff8 Update INSTALL.txt 2012-05-12 23:09:55 +02:00
ShyPike
731f331502 Upgrade unzip for Windows to 6.0 2012-05-12 23:05:22 +02:00
ShyPike
b15efb4d38 Clean up package.py. 2012-05-12 23:04:08 +02:00
shypike
ccfb0df819 Remove .nomedia marker file also when it has been moved by Sorting. 2012-05-12 19:57:12 +02:00
shypike
620ef9fc64 Extension-based cleanup list should also remove extension-only files like ".sfv". 2012-05-12 19:48:59 +02:00
shypike
f799fc08f7 Remove some debugging lines. 2012-05-12 19:47:51 +02:00
ShyPike
11a1ce4a6c Set UI-ready flag sooner. 2012-05-12 16:04:04 +02:00
shypike
9610a5a0dd Do HTML quoting for " and ' characters in skin texts.
Improve skin text translation cache implementation.
2012-05-12 15:54:44 +02:00
ShyPike
0b39afcd18 Fix incorrect README.rtf. 2012-05-09 21:08:45 +02:00
ShyPike
abe53b8a8a Fix error in OSX signing. 2012-05-09 20:59:42 +02:00
ShyPike
bffdf77d9a Fix crash of Sorting preview when using accented characters in example. 2012-05-09 20:37:19 +02:00
shypike
31a7016c15 Merge pull request #37 from thezoggy/uniConfig
Integrate Config skin (previously a submodule)
2012-05-09 11:15:50 -07:00
ShyPike
b92bd6aa0d Remove submodule Config. 2012-05-09 20:12:25 +02:00
ShyPike
ed4a6567c3 Update text files for 0.7.0 Beta5 2012-05-09 20:07:15 +02:00
Jonathon Saine
1988977d7c Remove submodule and just use clone of uniConfig master branch. 2012-05-08 16:43:08 -05:00
ShyPike
9f33bb4bb3 Improve rounding of ratio in pre-check so that you don't get messages like "101.0% is less than required 101.0%". 2012-05-08 22:46:17 +02:00
shypike
02a8141de2 Create unified DMG file for all OSX versions. 2012-05-08 22:29:51 +02:00
ShyPike
c852b44dcd Update translatable texts. 2012-05-07 22:11:29 +02:00
ShyPike
1a44b38308 Add special option required completeness percentage when doing pre-check.
Set default to 101.
2012-05-07 22:11:00 +02:00
ShyPike
743bf88d28 Enable randomization of multiple Usenet server IPs by default. 2012-05-07 15:14:43 +02:00
ShyPike
c0250c2c29 Fix incorrect formatting of source URL in history. 2012-05-07 15:12:40 +02:00
shypike
50990a8a01 Fix crash in newswrapper.py when dealing with single line server responses. 2012-05-05 23:51:09 +02:00
shypike
03c12a65ac Update translations 2012-05-04 14:54:14 +02:00
ShyPike
3f7967326b Make Usenet server provider IP randomization an option instead of always on. 2012-05-04 12:08:55 +02:00
ShyPike
6d47ba30b9 Prevent exception messages being logged when evaluating incomplete Sort expressions in Config->Sorting's preview mode. 2012-05-04 10:43:12 +02:00
ShyPike
c4459e2238 Add support for ".nomedia" marker in "final" folder in order to disable scanning by media players during unpack. 2012-05-03 19:02:30 +02:00
ShyPike
cba0d3f890 Extend the Windows SysTray menu. 2012-05-01 22:15:42 +02:00
ShyPike
95182b8fe5 Update text files for 0.7.0 Beta4 2012-05-01 21:18:56 +02:00
ShyPike
0436247269 Update translations. 2012-05-01 21:18:32 +02:00
ShyPike
b41f4f0cb2 Uniform favicons for all skins. 2012-05-01 20:33:03 +02:00
ShyPike
840ec9cb05 Update translatable texts. 2012-05-01 20:26:45 +02:00
ShyPike
774ebf4cd5 Fix RSS filter enabling logic. 2012-05-01 20:14:37 +02:00
ShyPike
13b7d75ee8 More cleanup. 2012-04-30 15:22:55 +02:00
ShyPike
6171caa0e9 Cleanup NSIS_Installer.nsi and package.py. 2012-04-30 15:01:47 +02:00
ShyPike
04c74be144 Don't patch translations into the NSIS_Installer.nsi file, but create a temporary file to be used by the packager. 2012-04-30 14:14:20 +02:00
shypike
84ff098527 Fix error in displaying current language (side-effect of previous change). 2012-04-29 17:19:40 +02:00
ShyPike
3afb1e37cb Some more fixes for Brazilian Portuguese support. 2012-04-29 14:31:18 +02:00
ShyPike
5c4cd2c343 Remove program start option from the Windows installer.
This is troublesome because SABnzbd would be started as admin instead of actual user.
2012-04-29 14:29:32 +02:00
ShyPike
4949782afd Add some translatable texts for Config skin. 2012-04-29 14:27:16 +02:00
ShyPike
9dbb43b4e7 Replace current icon files with smaller ones (thanks, inpheaux). 2012-04-29 12:29:53 +02:00
ShyPike
3667e7e596 Add Portuguese (Brasil) to translations. 2012-04-29 12:04:02 +02:00
ShyPike
eb7dc19732 Fix error in package.py which incorrectly still added "curl" to Windows distro when using Python 2.6+ 2012-04-28 15:31:46 +02:00
ShyPike
5daa7487fa Update main POT file. 2012-04-28 15:15:52 +02:00
ShyPike
2a9ba72fcd Merge branch 'opencomplete' of https://github.com/lordp/sabnzbd into develop.
Change "directory" to "folder". Make new entry the second one.
2012-04-28 14:24:14 +02:00
ShyPike
20507eacc8 Update Config skin. 2012-04-28 14:18:06 +02:00
shypike
f0897fd046 Update main POT file. 2012-04-28 13:55:53 +02:00
shypike
6f546d608b Update translations 2012-04-28 13:51:01 +02:00
shypike
41cf240cb4 Add support for OSX code-signing. 2012-04-28 13:08:23 +02:00
Darryl Hamilton
3ec13769fa Add option to tray to open the complete download directory 2012-04-27 21:38:50 +12:00
ShyPike
0153141394 Correct provisional extraction of filename from the subject. It went wrong when more than one quoted string is present (e.g. 'This post "file1.rar" "1of10" yEnc'. 2012-04-21 20:31:14 +02:00
ShyPike
b8359cc7de Quota wasn't initialized properly when deriving initial download amount from History database. 2012-04-20 19:44:42 +02:00
ShyPike
6becfafb6d Merge branch 'develop' of github.com:sabnzbd/sabnzbd into develop 2012-04-20 19:05:56 +02:00
ShyPike
e20f2cde02 Prepare RSS for enable/disable per filter.
This is the 7th parameter of the filter line.
"0" is disabled, anything else is enabled.
2012-04-20 19:05:24 +02:00
ShyPike
88c1ef8d93 Never show pre-checked jobs that have insufficient data as 100.0% (due to rounding), force to 99.9% 2012-04-19 17:57:18 +02:00
shypike
52329b7372 Merge pull request #30 from thezoggy/develop-fix_smpl
smpl update
2012-04-18 13:16:45 -07:00
ShyPike
fbdf71c0e5 Provisional fix for Growl icon.
For remote Growl client send an sf.net URL for the icon.
The problem is that the current GNTP library doesn't support icon embedding.
2012-04-18 21:49:23 +02:00
ShyPike
fdd14125ff Fix response from test_notify. 2012-04-18 20:36:01 +02:00
Jonathon Saine
1629676330 Cosmetic change, onclick should be all lower. 2012-04-17 19:16:00 -05:00
Jonathon Saine
9a56a3238d Fix the edit nzb details 'All / None / Invert' selection. 2012-04-17 18:53:05 -05:00
ShyPike
a02fc91a1e Add "newshost.za.net" to list of bad gzipping indexers. 2012-04-17 21:02:04 +02:00
ShyPike
f5f501a6c2 In Classic skin, the Status page still called itself "Connections". 2012-04-17 19:07:40 +02:00
ShyPike
b4232369fe Add "xcat" parameter to api-calls "addfile" and "addlocalfile" allowing an indexer category to be sent. This category will be mapped to a user-defined category. 2012-04-17 18:59:14 +02:00
shypike
a9eb107503 Add missing "B/s" to speed indicator in OSX menu. 2012-04-15 18:54:42 +02:00
ShyPike
9f7b5412da Update text files for 0.7.0 Beta3 2012-04-15 15:02:13 +02:00
ShyPike
130c996bc2 Update Config.
Remove dummy option.
2012-04-15 14:23:00 +02:00
shypike
5f5905138c Apply file/folder permissions also to the "temporary download folder".
Do so for root folder, job folders and downloaded files (so not for __admin__).
This will make streaming during download more useful.
2012-04-15 13:31:31 +02:00
shypike
69d4859ec5 Fix crash in api-call "switch" when using ascii output. 2012-04-15 10:09:35 +02:00
ShyPike
b11710a32a Rename option "max_opt_only" to "max_art_opt" to give it a sensible default (False). 2012-04-14 16:24:06 +02:00
ShyPike
f5673b190a Stop distributing CURL with 0.7.x (due to typo in package.py). 2012-04-14 16:00:19 +02:00
ShyPike
ff21d6d4d1 Disable gzip HTTP compression when getting files from nzbsa.co.za,
because that site uses compression in a way that Python's HTTP library doesn't understand.
2012-04-11 22:49:10 +02:00
ShyPike
951d5e6797 Fix crash in urlgrabber.py when an invalid file received. 2012-04-11 22:40:29 +02:00
ShyPike
52a8e7a7e8 Log all external API-calls at debug level. 2012-04-11 20:34:04 +02:00
ShyPike
19be644888 Fix backward compatibility of API-calls "addurl" and "addid" by returning "ok" in ASCII mode (instead of the more sensible status/nzo_ids).
Fix unjustified error message when sending a single item with API-calls "addfile" and "addlocalfile".
2012-04-11 19:47:48 +02:00
ShyPike
460f510ce1 Accept multiple items in API-calls "addurl" and "addid".
The "name" and "nzbname" keywords can be repeated.
2012-04-10 21:48:53 +02:00
ShyPike
10a564c02e Fix growler.py fix, because it has side-effects.
Now pynotify will simply be disabled when using Python 2.5
2012-04-10 20:59:22 +02:00
ShyPike
dc70f5e3cd Restore Python2.5 compatibility in growler.py. 2012-04-10 19:52:35 +02:00
ShyPike
760eb5b2be After a language change, Growl needs to be registered again. 2012-04-09 23:09:24 +02:00
ShyPike
5fca90a8e2 Sync Config skin. 2012-04-09 21:04:54 +02:00
ShyPike
444b65f003 Windows: fix problem with latin1 encoded par2 files when retrying from history. 2012-04-09 21:03:24 +02:00
ShyPike
ad0eb94e63 Fix detection of retry-able history entries for case-insensitive file systems. 2012-04-09 20:24:43 +02:00
ShyPike
567d86b715 Fix the api-call "auth". 2012-04-07 14:12:42 +02:00
ShyPike
9394bc0f0b API-calls "addfile" and "addlocalfile" returned an incorrect status value. 2012-04-07 13:38:19 +02:00
ShyPike
7c4e3b20cc Add support for the peculiar Usenet provider "free.xsusenet.com". 2012-04-06 20:31:36 +02:00
ShyPike
55118154d3 OSX menu should use the same code for formatting the speed as the skins.
(And so show a single decimal digit for M and G speeds.)
2012-04-06 19:18:58 +02:00
ShyPike
595fbc4313 Replace "Spanish Castilian" with "Spanish" in make_mo.py, because NSIS can only handle "Spanish". 2012-04-04 21:02:54 +02:00
ShyPike
868df5fb46 Update text files for 0.7.0 Beta2 2012-04-04 20:33:01 +02:00
ShyPike
11bae22827 Fix Wiki link in the installer (temporary). 2012-04-04 20:03:23 +02:00
shypike
5bbf8e9d89 Update translations 2012-04-04 19:57:40 +02:00
shypike
e544c7e9c4 Update Spanish translation. 2012-04-03 23:20:05 +02:00
shypike
41c2b6ff62 Add Spanish translation. 2012-04-03 22:54:54 +02:00
shypike
fca9d0b89e Ensure proper handling of web host address 0.0.0.0 when localhost does not resolve as expected. 2012-04-03 22:28:01 +02:00
ShyPike
af274c0f1c Sync with Config 2012-04-02 21:13:58 +02:00
shypike
7929cabb87 Update translations 2012-04-02 21:08:55 +02:00
ShyPike
e2de82979e Update text files for 0.7.0Beta1 2012-04-02 21:05:46 +02:00
ShyPike
0c89a69229 Add missing queue status (Status.*) replacements. 2012-03-31 16:41:35 +02:00
shypike
a1a45debdc Merge pull request #28 from alexfu/develop
Define queue status strings in a class.
2012-03-31 06:59:31 -07:00
Alex Fu
6c3f22573a Merged status.py with constants.py and fixed imports 2012-03-31 09:40:05 -04:00
ShyPike
b934b64468 Don't allow the "complete" folder to be a subfolder of "incomplete". 2012-03-31 11:49:18 +02:00
Alex Fu
c2998590fb Fixed NameError error 2012-03-30 21:37:06 -04:00
Alex Fu
01bd6fb979 Externalized all status strings using status.py 2012-03-30 16:22:38 -04:00
Alex Fu
4e8a384004 New file to isolate status strings. 2012-03-30 16:22:17 -04:00
ShyPike
8c515da6c0 Add "special" option to allow incomplete NZB files (in paused mode). 2012-03-29 22:17:32 +02:00
shypike
c18a1bba10 Merge pull request #27 from thezoggy/nzbmatrix-cat
NZBmatrix category update
2012-03-29 12:09:46 -07:00
Jonathon Saine
be002f8281 Updated nzbmatrix category mapping to current values and names. 2012-03-28 11:19:06 -05:00
ShyPike
b3b067c8fa Fix RSS link in the smpl skin so that it is compatible with an Apache proxy. 2012-03-27 19:54:08 +02:00
shypike
08c3d0f511 Set X bit of unrar-leopard. 2012-03-26 22:42:24 +02:00
ShyPike
c39ea87ef8 Update text files for 0.7.0 Alpha 3. 2012-03-26 22:29:01 +02:00
ShyPike
ba1693a853 OSX: Use unrar 4.10 for Leopard and older to keep PPC support.
(SL and Lion will get unrar 4.11)
2012-03-26 22:21:50 +02:00
ShyPike
9e36c64bb8 Sync Config skin. 2012-03-26 22:08:32 +02:00
ShyPike
40f08f04e4 Sync Config skin. 2012-03-26 20:32:57 +02:00
ShyPike
b9efcaa5a1 Update copyright stuff. 2012-03-26 19:29:29 +02:00
shypike
79cba85aba Update translations 2012-03-25 14:13:37 +02:00
ShyPike
cf105e144b When addressing a non-existent NZB detail page, return to queue page instead of showing a CherryPy crash message. 2012-03-25 13:59:59 +02:00
ShyPike
3a35402f1b Prevent crashes in the API when trying to access non-existing history elements. 2012-03-25 13:21:37 +02:00
ShyPike
f7f40d0c75 When determining set names, the setname was stored in lowercase (regression from 0.6.x).
This leads to later problems when matching files to sets in post-processing.
2012-03-24 10:54:15 +01:00
ShyPike
fa6b8199cb Update text files for 0.7.0Alpha2 2012-03-23 19:34:13 +01:00
shypike
f2fffd8c0a Update translations 2012-03-23 19:31:01 +01:00
ShyPike
eda7d3b560 Improve detection of older versions in Windows installer.
Remove more residue in Windows uninstaller.
2012-03-23 19:21:04 +01:00
ShyPike
7559804cbc Sync Config skin. 2012-03-23 16:47:01 +01:00
ShyPike
0630cd35d6 Update CHANGELOG.txt 2012-03-23 16:42:56 +01:00
ShyPike
28554e787c Prevent potential cash in par2-check when no par2 blocks are available. 2012-03-22 21:19:51 +01:00
ShyPike
965e69b378 ntfosd_enable should default to False for OSX and Windows. 2012-03-22 21:02:08 +01:00
ShyPike
f9c3a9441a Prevent double entries in History's "Source" section. 2012-03-22 20:13:27 +01:00
ShyPike
5ece2ce844 Replace real spaces in RSS-urls with %20. 2012-03-21 19:18:17 +01:00
ShyPike
20c857dc31 Update COPYRIGHT.txt file. 2012-03-20 20:55:20 +01:00
ShyPike
2a5ec5a65d Remove flags from the Wizard. 2012-03-20 20:54:50 +01:00
ShyPike
6654f75699 Prevent .git residue from ending up in the distribution files.
Add removal of Config skin in Windows uninstaller.
2012-03-20 19:26:36 +01:00
ShyPike
14d52a4ae7 Prevent crash when NotifyOSD support is not working on a Posix system. 2012-03-20 19:11:36 +01:00
ShyPike
b07214b706 Add Config skin to Win/GZ distros. 2012-03-19 21:35:39 +01:00
ShyPike
a929d9d8ce Update text files for 0.7.0 Alpha 1 2012-03-19 19:22:21 +01:00
shypike
386601ccdd Update translations 2012-03-19 19:04:47 +01:00
ShyPike
75677d88cf Fix crash in urlgrabber shutdown. 2012-03-18 21:57:42 +01:00
ShyPike
1e8d51d921 Test explicitly for hourly/daily limit messages from nzbmatrix. 2012-03-18 21:14:45 +01:00
ShyPike
9dadef60e7 Sync Config skin. 2012-03-18 11:29:16 +01:00
ShyPike
edb39c34b7 Update main POT file. 2012-03-18 11:26:02 +01:00
ShyPike
be96f029a2 Major changes in url-grabber and newzbin queues:
Keep retrying, add variable delays depending on problem, show waiting time in queue.
Only explicit problems (like bad credentials) will move item as "failed" to history.
2012-03-18 11:25:14 +01:00
ShyPike
e741a6644a When newzbin isn't reachable, the report number was dropped from the queue instead of re-added. 2012-03-17 14:25:17 +01:00
ShyPike
55e60a3e8e Update Config skin. 2012-03-15 22:14:36 +01:00
ShyPike
75e8381924 Update POT file. 2012-03-15 21:58:22 +01:00
ShyPike
bf3a1b8d43 Add new universal Config skin as a submodule. 2012-03-15 21:41:09 +01:00
ShyPike
4c5e8a57c5 Add text to skintext.py 2012-03-15 21:38:25 +01:00
shypike
7c5e9b7874 Update translations 2012-03-13 21:22:10 +01:00
ShyPike
1f35bc58e1 Add base_url to RSS page so that RSS "favicons" can be queried. 2012-03-13 20:30:50 +01:00
ShyPike
da105ba12f Update POT file. 2012-03-12 19:18:31 +01:00
ShyPike
fa0474611d Rewrite of file-joining function.
It couldn't handle sequences starting with .000 and the code was needlessly complex.
2012-03-11 21:56:22 +01:00
ShyPike
610c2c7f0f Remove potential crash when dealing with rar segments joined by par2. 2012-03-11 21:52:52 +01:00
shypike
643f20048b Update translations 2012-03-11 17:46:16 +01:00
shypike
6dc59f0d9a Merge pull request #25 from thezoggy/gzip-headers
Gzip headers
2012-03-10 23:35:14 -08:00
Jonathon Saine
30cbe48f10 Defined what mime types for cherrypy to gzip. text/* and javascript. 2012-03-10 23:43:39 -06:00
shypike
dc6f8a19e3 Update Win installer translations. 2012-03-10 12:35:37 +01:00
shypike
1da2c73ce0 Update translations 2012-03-10 12:31:03 +01:00
ShyPike
15537de378 Update POT files. 2012-03-09 18:54:24 +01:00
ShyPike
c72ecf0373 Remove Config skin from Config->General's skin pick lists. 2012-03-07 23:27:42 +01:00
ShyPike
2b6adc90c9 Update copyright dates. 2012-03-07 21:56:08 +01:00
ShyPike
5878ac81d7 Make '$my_lcldata' and '$my_home' available on all UI pages. 2012-03-07 21:32:43 +01:00
ShyPike
92f12911ea Always do a QuickCheck. When the user disabled QC, also do the par2 check and log a warning when QC and par2 have a different outcome. 2012-03-06 19:14:01 +01:00
ShyPike
5863ec6878 Protect launch_a_browser() against None parameter. 2012-03-06 19:08:18 +01:00
ShyPike
aeb83d3c93 Fix Plush speed/pause/options menu behavior for touch devices. 2012-03-05 21:38:09 +01:00
ShyPike
6571788800 Add "special" option "start_paused". 2012-03-02 19:37:29 +01:00
ShyPike
c335371b77 Send email when post-processing crashes.
Fix translation of stages in notification email.
2012-03-02 19:28:26 +01:00
shypike
9f534f788f Prevent hangup of OSX binary after Panic message.
OSX panic messages failed to reach the browser because OSX needs the prefix "file:///"  when showing a file in the browser.
2012-02-29 20:32:03 +01:00
ShyPike
bf6b2c3809 Fix pathbrowser.py further by suppressing CDROM drives and Unix/OSX names starting with ".". 2012-02-28 22:48:36 +01:00
ShyPike
ab00c54270 Improve pathbrowser.py by not showing problematic folders on Windows. 2012-02-28 21:23:57 +01:00
ShyPike
6932ccbfcd Assume user base folder as base for pathbrowser. 2012-02-27 22:40:00 +01:00
ShyPike
dd07dd44ba Flag and pause incomplete NZB files. 2012-02-27 20:40:02 +01:00
ShyPike
81ad6c1b05 Update unrar for Windows to 4.11 2012-02-25 16:05:45 +01:00
shypike
4aa902429e Update unrar for OSX to 4.11 2012-02-25 13:49:44 +01:00
shypike
8a683e4ef0 Fix api call error in Wizard that prevented Server Test from working behind Apache. 2012-02-25 13:07:40 +01:00
shypike
9046658d0f Add URL to "empty NZB" warning. 2012-02-25 13:01:33 +01:00
shypike
4c259d8d2b nzbmatrix changed "TV: Divx/Xvid" category into "TV: SD". 2012-02-25 12:44:04 +01:00
shypike
05ab19b5a6 Email notification for failed URL fetches. 2012-02-25 12:42:27 +01:00
shypike
9fb74616e8 Make newzbin fetch queue more robust. 2012-02-24 16:14:11 +01:00
ShyPike
eea2dab3a8 API call "browse" should expect UTF-8 input and convert that to the appropriate platform encoding. 2012-02-23 21:23:26 +01:00
ShyPike
a133bb28cd Add loglevel -1 to suppress logging altogether. 2012-02-22 22:01:06 +01:00
shypike
b7cda43d36 Merge pull request #22 from thezoggy/api-browse_tweak
Api browse tweak
2012-02-22 12:31:17 -08:00
Jonathon Saine
3bee9b3ec0 Due to the migration of the autocomplete module the api browse&compact command needed to be tweaked. 2012-02-22 14:25:35 -06:00
ShyPike
6281684c46 The schedule evaluator should pass effective server status to the downloader.
This prevents active use of schedule-disabled servers at startup.
2012-02-21 23:19:20 +01:00
ShyPike
41bc05a108 Enable "uniconfig" option by default. 2012-02-21 21:32:24 +01:00
ShyPike
da2c0d5539 Fix encoding issue in API calls "browse" and "get_scripts". 2012-02-21 20:50:37 +01:00
ShyPike
ac1c77d862 Fix for systems that have Python with an incomplete "locale" implementation. 2012-02-21 20:13:20 +01:00
ShyPike
eccece6799 Fix failure to decode uuencoded downloads.
Fix decoding of articles that use \n instead of \r\n as line terminators.
2012-02-16 18:58:14 +01:00
ShyPike
7dd5c03f64 Fix potential crash in API-call queue/sort. 2012-02-14 19:52:08 +01:00
shypike
406e155284 Make "par2_multicore" visible in Config->Special. 2012-02-12 12:33:50 +01:00
shypike
63069407b8 Add "Clear" button for server download stats. 2012-02-11 17:42:04 +01:00
ShyPike
1314219d78 Clean up "osx_menu" option code. 2012-02-08 19:27:51 +01:00
shypike
c602eadfa8 Add missing text. 2012-02-05 20:53:59 +01:00
shypike
ea28b7023f In Sorting expressions, add %sN, %s.N and %s_N for show/movie names without case-adjustment. 2012-02-05 20:49:10 +01:00
ShyPike
c5580f8de4 Update readme files. 2012-02-05 13:58:33 +01:00
ShyPike
c6b361950e API calls "addfile" and "addlocalfile" now return a list of nzo_ids which can be used to access queue items.
Json example:
{"status":0,"nzo_ids":["SABnzbd_nzo_bjibsz", "SABnzbd_nzo_ihfghs"]}
ZIP-files with NZBs will return multiple nzo_ids.
2012-02-05 13:54:44 +01:00
ShyPike
4a12acc82c build_header() did not set newzbin_url variable. 2012-02-05 13:44:31 +01:00
shypike
a1594f4915 Update text files to reflect current 0.7.x status. 2012-02-05 11:12:51 +01:00
ShyPike
be96a611af Fix api call "addfile" (for adding local NZB file). 2012-02-03 20:47:33 +01:00
ShyPike
2104f3cb81 OSX troubleshoot menu: when using 127.0.0.1 also reset http and https ports. 2012-02-03 20:47:20 +01:00
ShyPike
6761338dfe Flag post-processing as failed when files cannot be moved/copied to destination. 2012-01-29 00:15:14 +01:00
ShyPike
08966da22d Replace newzbin.com with newzbin2.es
Add an INI setting for its base URL.
2012-01-29 00:05:20 +01:00
ShyPike
d497ab0232 Don't keep Windows awake when there are only 'paused' items in the queue. 2012-01-23 20:43:26 +01:00
ShyPike
ff371e1328 Update feedparser license file. 2012-01-21 22:32:19 +01:00
ShyPike
0a0a8aae4b Update feedparser.py to version 5.1 2012-01-21 22:01:55 +01:00
ShyPike
b98ef1817c Add special setting to use "pickle" library instead of cPickle. 2012-01-15 14:48:08 +01:00
ShyPike
e9f2abd954 Suppress "incompatible feed" error when doing a scheduled/automatic RSS read-out. 2012-01-15 14:41:50 +01:00
ShyPike
2a7117d324 Windows un-installer now only offers to remove "settings", which is all of the AppLocal data of SABnzbd.
The "cache" part had no meaning since 0.6.0.
2012-01-07 12:39:24 +01:00
ShyPike
816850f211 Prevent crash on restoring URL-fetches when using --repair-all option. 2012-01-06 21:55:56 +01:00
ShyPike
45d14f92e5 Fix "Repair" button on smpl Connection page. Current path fails when using a reverse proxy. 2012-01-06 21:54:25 +01:00
ShyPike
9fa1840c31 Make QR texts translatable. 2012-01-02 21:08:37 +01:00
inpheaux
47854f491b Adds a QR Code popup to Plush Config -> General, as per a feature request on the forums
http://forums.sabnzbd.org/viewtopic.php?f=4&t=9710
2012-01-01 22:17:28 -05:00
ShyPike
bd94148648 Remove all embedded newlines in translated texts that contain HTML elements. 2011-12-31 13:24:21 +01:00
ShyPike
a54df8f2be Ignore whitespace around regular expressions in RSS filters. 2011-12-31 13:22:54 +01:00
shypike
3563b6450b Merge pull request #17 from thezoggy/pathbrowser-adjustment
Pathbrowser adjustment
2011-12-29 11:50:08 -08:00
Jonathon Saine
5bb995f601 Keep the '..' at the top, to return to the previous directory. 2011-12-29 12:22:31 -06:00
shypike
649ca5ad9a Merge pull request #16 from thezoggy/smpl_update_for_uc
Smpl update for Universal Config
2011-12-28 12:25:52 -08:00
Jonathon Saine
fed1c16524 Corrected smpl changes so it works for secondary skins. Also changed onClick to onclick to be proper (mainly cosmetic). 2011-12-28 12:38:35 -06:00
ShyPike
e9a6f4b9bc Preparation for Universal Config pages.
Will use 'Config' skin for configuration pages for all other skins.
Disabled by default, can be enabled by 'uniconfig' setting in [misc] section.
2011-12-28 17:57:50 +01:00
shypike
8bb6d676ef Make Growl on OSX find its icon again. 2011-12-28 09:24:56 +01:00
ShyPike
8b3954d31b Move all icons into a folder.
Windows SysTray started a bit earlier.
2011-12-27 22:56:33 +01:00
ShyPike
fa7ef0fc65 Further tuning of Windows Tray.
Move generic systrayiconthread.py to utils.
Cleanup text and other small stuff.
2011-12-27 21:47:14 +01:00
ShyPike
b9f42f689e Improvements for Windows SysTray. 2011-12-27 16:12:49 +01:00
shypike
9f85063eb3 Merge pull request #7 from janschejbal/develop
Tray icon for Windows
2011-12-27 06:17:28 -08:00
ShyPike
f5a0ba59e5 Update translation master. 2011-12-26 13:59:05 +01:00
shypike
8f553199a4 Improve OSX version detection and give Leopard image the name "*-osx-leopard.dmg". 2011-12-23 15:41:27 +01:00
shypike
c9347eece8 Extend OSX menu with Troubleshoot items and "Scan watched folder". 2011-12-23 15:38:44 +01:00
ShyPike
b94c3ede4a Move "locale" construction from Plush skin to Python code.
Some embedded Linux platforms show bizarre behavior with the original construction.
2011-12-21 19:29:17 +01:00
ShyPike
0fafbd6f2e Work-around for malfunctioning IPv6 one some Linux platforms.
Specifically: situation where "localhost" resolves to "::1" and "127.0.0.1" while IPV6 is disabled and therefor "::1" not really working.
2011-12-18 12:28:52 +01:00
ShyPike
db8f9b5f8c Typo in previous commit. 2011-12-17 21:08:43 +01:00
ShyPike
76449721b1 Only try to remove a nzbmatrix bookmark when an URL is known. 2011-12-17 20:57:35 +01:00
ShyPike
bb29eb9dbc misc.format_source_url() crashed when receiving newzbin report number. 2011-12-17 14:47:44 +01:00
ShyPike
15a66b1f28 Add config item for the movie rename threshold and lower the default to 100M. 2011-12-16 21:15:24 +01:00
ShyPike
722c523893 Plush: Don't show newzbin icons in history when they have no info. 2011-12-13 22:46:11 +01:00
ShyPike
9f553fd61f Make "speedlimit" translatable in Config->Scheduler and show value 0 as "off". 2011-12-13 21:57:17 +01:00
ShyPike
b16b625c7c Don't accept the scheduler event "speedlimit" without a proper speed value. 2011-12-12 18:51:46 +01:00
shypike
dd7335c257 Use default Python interpreter only for non-executable Python scripts (OSX and Unix). 2011-12-11 20:35:09 +01:00
ShyPike
e6cb6d0171 Merge remote-tracking branch 'remotes/origin/develop' into develop 2011-12-11 16:08:55 +01:00
ShyPike
af54add471 Add NZB source to History (for watched-folder files and URL-based). 2011-12-11 16:05:53 +01:00
shypike
f812e9f6e6 Improve support for old RAR multipart numbering schema (r01, s01, t01 etc.) 2011-12-10 13:58:45 +01:00
shypike
96e1b81179 Only warn about identical port for HTTP and HTTPS when HTTPS is actually enabled. 2011-12-10 13:53:46 +01:00
shypike
8b2bcc8d10 Prevent setting watched-folder speed to 0 (while having no watched-folder) from triggering an inifinite loop. 2011-12-10 11:50:05 +01:00
ShyPike
140cb53485 Prevent "watched folder" from being reset to default when it cannot be created at startup time. 2011-12-10 11:49:55 +01:00
ShyPike
975bbbdbcd Prevent potential crash in RSS link analysis. 2011-12-08 21:55:23 +01:00
shypike
fe6d4f3673 Add --console option to force console logging for OSX app. This will help diagnose startup problems. 2011-12-08 21:55:12 +01:00
ShyPike
095f3576c4 Make sure that paths coming from Sorting are normalized. 2011-12-08 21:55:00 +01:00
ShyPike
753ed1c846 Prevent crash when 'localhost' does not resolve in all_localhosts(). 2011-12-07 20:25:21 +01:00
ShyPike
ab80e2b15b When retrying an URL fetch from History, remove the History entry and don't show a re-queue message anymore. 2011-12-05 20:46:02 +01:00
ShyPike
11b5d2e998 Report failed URL fetches to Growl. 2011-12-05 19:33:59 +01:00
ShyPike
3568d067be Handle OS-es that returns multiple identical IPs for localhost (like Ubuntu-s). 2011-12-05 18:42:33 +01:00
ShyPike
f85f5bf317 Movie sort failed to created specified job folder (regression error). 2011-12-05 18:33:14 +01:00
ShyPike
3f772b3ba7 Don't ask for gzip compression when getting a ZIP file from a web site. 2011-12-02 21:30:44 +01:00
ShyPike
293882b2bb Prevent rare crash in TestServer function. 2011-11-29 23:09:43 +01:00
ShyPike
103f71fa01 Modify analysis of RSS feeds so that links in a Yahoo pipe are handled
conform the rules for the original index site.
2011-11-29 23:09:13 +01:00
ShyPike
f30b5b1063 Handle incorrect date fields in NZB files and accept the NZB anyway. 2011-11-23 19:28:18 +01:00
ShyPike
ae6deed7cd Fix two potential crashes. 2011-11-20 14:14:50 +01:00
ShyPike
eaa1efb910 When secondary web-ui is not defined, redirect to primary web-ui. 2011-11-20 13:00:18 +01:00
ShyPike
c5b7e021cd Use the mini-par2 file instead of the smallest vol-par2 file.
Send all the vol-par2 files immediately to the end of the queue so that nothing of it is downloaded when not needed.
2011-11-19 21:41:00 +01:00
ShyPike
7b3c400124 Store verified status in job admin, so that when retrying from History, a successful verification isn't repeated. 2011-11-19 21:31:26 +01:00
ShyPike
6e31d55941 Handle special case of joined RAR files.
This prevents the joined files from being seen as an extra RAR-set.
2011-11-19 21:22:50 +01:00
ShyPike
7ae2396167 When trying passwords on encrypted RAR files, only log failure once. 2011-11-19 21:18:40 +01:00
ShyPike
d6bb30c84b Make sure SFV check isn't done when more par2 files can still be downloaded. 2011-11-17 21:10:31 +01:00
ShyPike
9ffb2c2438 Handle unknown Growl errors too. 2011-11-17 20:56:26 +01:00
ShyPike
c0a12d5a11 Further improvement of detection of encrypted RAR files. 2011-11-17 20:55:21 +01:00
ShyPike
ed720fffec Improve detection of encrypted rar files. 2011-11-17 20:55:09 +01:00
ShyPike
a7e0cffe0f api call "get_bookmarks" should ignore "auto-fetch bookmark" setting too. 2011-11-14 20:55:20 +01:00
ShyPike
cf6eca6789 After failed par2 verification do an SFV check (if enabled and SFV files available).
Previously an SFV check was only done when no par2 files were available.
2011-11-11 22:05:31 +01:00
shypike
d5e60bf250 Make "Get Bookmarks Now" (newzbin) button work when auto-fetch for bookmarks is off. 2011-11-11 19:56:00 +01:00
shypike
93f0f6f08c Prevent crash after failed Growl registration. 2011-11-08 21:27:44 +01:00
shypike
7ef486c44e Fix logging of Pre-Q script result. 2011-11-08 21:26:31 +01:00
shypike
36704e4ad6 Fix crash in pre-queue script due to incorrect parameter in SeriesSorter call. 2011-11-08 21:17:35 +01:00
ShyPike
c735e1da89 Always run Python user scripts, despite not being "executable". 2011-11-04 22:21:36 +01:00
ShyPike
2ae38c8c04 Log invocation of par2 command (debug level). 2011-11-02 20:20:40 +01:00
ShyPike
9b79188526 Only run extension-based cleanup when verification was OK.
Otherwise the user may lose par2 files if .par2 was in the cleanup list.
2011-11-02 20:20:30 +01:00
ShyPike
d792ef30f9 Re-apply patch to GNTP. 2011-10-22 21:43:47 +02:00
ShyPike
60e1569016 Update GNTP module. 2011-10-22 21:35:45 +02:00
ShyPike
de8ba245e9 Eliminate extra (temporary) folder level when using GenericSort. 2011-10-22 17:50:37 +02:00
Jan Schejbal
1f125ca0fc windows systray: fixed newlines, file locations and names 2011-10-21 02:33:08 +02:00
ShyPike
bb30a5600a Growl improvements.
Reduce GNTP logging.
Limit GNTP timeout to 10 seconds (if Python version supports it).
Start GNTP notification in separate thread to prevent lockups when server does not respond.
2011-10-18 23:00:07 +02:00
ShyPike
0229335c15 Make sure that "complete" and "incomplete" paths are different.
If needed, make "incomplete" a sub folder of "complete".
This is to avoid name clashes between complete and incomplete jobs.
2011-10-18 22:29:35 +02:00
shypike
d3d16da3f0 OSX menu should show 10 queued items (like the text claims) instead of 9. 2011-10-18 22:00:09 +02:00
ShyPike
4b959da7d2 Make SABnzbd listen on all available localhost equivalents (usually 127.0.0.1 and [::1]).
Add command line option --stack with values '4', '6', 'b' meaning IPv4-only, IPv6-only and both, respectively.
This will fix connection problems on systems with an ambiguous localhost.
2011-10-18 21:07:48 +02:00
ShyPike
eae8f65e15 Prevent reading newzbin bookmarks when no newzbin credentials are known. 2011-10-17 12:03:33 +02:00
ShyPike
816ea12993 Add sabnzbd.ico to OSX dmg so that Growl can use it. 2011-10-16 22:11:32 +02:00
ShyPike
7d186e4585 Add remark about pynotify. 2011-10-16 20:57:39 +02:00
ShyPike
b128f4fa89 Make sure that having Growl support will not override NotifyOSD. 2011-10-16 16:34:50 +02:00
ShyPike
7c7cb84fd1 Fix small typo. 2011-10-16 16:20:06 +02:00
ShyPike
d3b2fba6a1 When reading file names from an SFV file, convert to encoding suitable for the platform. 2011-10-16 12:13:27 +02:00
ShyPike
330b836e08 Render ambiguous Windows paths like "D:" and "D:folder" as "D:\" and "D:\folder". 2011-10-12 23:28:44 +02:00
ShyPike
a7defa37bf Add gntp module to source distribution file. 2011-10-12 23:28:19 +02:00
shypike
a7724069b5 OSX: Generate a SnowLeopard/Lion DMG and a Leopard DMG. 2011-10-12 23:27:45 +02:00
ShyPike
e4bba37ccb Create backup of the INI file before changing it.
Add some more steps in an order that avoids damaging it at all times.
2011-10-12 23:27:05 +02:00
ShyPike
28aff1e8f3 Fix failure to recognize "encrypted file" message from unrar 4.01. 2011-10-12 23:26:18 +02:00
ShyPike
8c060dbd44 Merge remote-tracking branch 'remotes/origin/master' into develop 2011-10-12 23:05:01 +02:00
ShyPike
c2ade817e7 Enable "with" statement in growler.py for Python 2.5. 2011-10-11 21:24:55 +02:00
shypike
681c466a18 Merge pull request #8 from mjc/displayvar-warning
Suppress pynotify gtk display warning during startup when DISPLAY environment variable is not set
2011-10-11 12:23:13 -07:00
Michael J. Cohen
d328762cd3 turn warnings into exceptions during loading of pynotify, preventing a console outputed warning when X11 is not available 2011-10-11 03:25:17 -04:00
ShyPike
7299b61244 OSX: Detect presence of pre-1.3 Growl.
If found, then use old-style SDK otherwise assume GNTP protocol.
2011-10-08 16:09:07 +02:00
shypike
361c7dd51e OSX: prevent changes to App after first run. Remove site.py from DMG distribution so that the compiled site.pyc file cannot change anymore. 2011-10-08 12:22:07 +02:00
ShyPike
cb2922a805 Add license text for GNTP module. 2011-10-05 20:52:22 +02:00
ShyPike
17e33def2e Fix failing app build. 2011-10-05 20:33:52 +02:00
ShyPike
d817af8897 Fix Plush's bad links due to having added one slash too much. 2011-10-04 21:17:14 +02:00
Jan Schejbal
2904075724 tray icon update frequency decreased, newlines added 2011-10-04 16:33:50 +02:00
Jan Schejbal
730c2ec631 tray icon 2011-10-04 16:20:15 +02:00
ShyPike
8b45af529a Fix regression error that made the no-folders option in Config->Categories impossible. 2011-09-30 20:41:46 +02:00
ShyPike
fbb5bc608c Fixed failing loglevel in Plush.
Fixed missing trailing slashes in Plush config paths.
2011-09-26 21:21:34 +02:00
ShyPike
87bfcd4fb4 Make default for size_limit option "0" instead of an empty string. 2011-09-26 21:05:40 +02:00
ShyPike
0fb45ba563 Refuse to add empty RSS filter.
Update text.
2011-09-26 20:00:09 +02:00
ShyPike
2eb62bf8c9 Add Config->Special page.
Meant to give access to rarely used options which were up to now INI-file only.
(Plush only)
2011-09-25 21:43:34 +02:00
ShyPike
4f87b33df7 Add save_rss_feed method to ConfigRSS (this one will handle the 'enable' parameter). 2011-09-25 16:40:31 +02:00
ShyPike
97de9cb93e Refuse to store a new RSS feed without a name or URI. 2011-09-25 14:49:33 +02:00
ShyPike
72cfd48135 Missed a link in Classic in previous commit. 2011-09-25 14:42:57 +02:00
ShyPike
a886253bea Rename several Web-UI pages to follow their current visible naming.
Config->Directories ==> Config->Folders
Config->Newzbin ==> Config->Indexers
Config->Email ==> Config->Notify
Connection_info ==> Status
2011-09-25 14:28:31 +02:00
ShyPike
d6363252ea Fix text typo. 2011-09-24 12:06:40 +02:00
ShyPike
d2c4f316c7 Fix text typo. 2011-09-24 12:05:50 +02:00
ShyPike
cdec953d96 Make scheduler show hh:mm instead of h:mm. 2011-09-23 21:52:41 +02:00
ShyPike
0bc0d4b0e8 Add hidden (for now) option fsys_type to override 8bitASCII/UTF-8 detection on Posix systems.
0 = automatic, 1 = 8bit-ASCII, 2 = UTF-8
2011-09-23 21:46:47 +02:00
ShyPike
ec0555a86f Postpone substitution of %ext in folder names until the phase that the extension is known. 2011-09-23 21:19:30 +02:00
ShyPike
e89ad70701 Fix preview of Generic Sort, element multipart. 2011-09-23 21:01:54 +02:00
ShyPike
95c8440c36 Fix eval_sort API call. 2011-09-21 21:24:23 +02:00
ShyPike
a814b45851 Sanitize the "name" parameter in eval_sort API call. 2011-09-20 23:17:57 +02:00
ShyPike
0730f86304 Add API calls "test_email" and "test_notif".
Return error messages when Growl (gntp) notifications fail.
Prevent creation of an registration object for each OSX Growl notification, just re-use it.
2011-09-20 16:33:54 +02:00
shypike
84029045ba Fix api function "eval_sort". 2011-09-20 00:30:10 +02:00
shypike
37cd87f3f4 Show original command line in Config->Base 2011-09-15 21:08:37 +02:00
shypike
b362ae3d8a Don't let make_mo.py depend on having executable .py files on Windows. 2011-09-13 20:51:31 +02:00
shypike
a39cfa485b Work-around for Python bug when dealing with space in script path on Windows. 2011-09-13 20:00:49 +02:00
shypike
b4ae208943 Support spaces in the current path when running tools/make_mo.py 2011-09-13 18:56:56 +02:00
shypike
45369fb5b2 Suppress unreliable ETA predictions when pre-checking jobs. 2011-09-11 11:10:55 +02:00
shypike
713993140f Update text files for 0.6.9 Final. 2011-09-10 10:57:25 +02:00
ShyPike
57476ef7b7 Merge remote-tracking branch 'remotes/origin/master' into develop 2011-09-08 22:00:34 +02:00
shypike
176e6b6afd Merge pull request #4 from sabnzbd/0.6.x
Merge 0.6.9RC2 into master.
2011-09-08 12:42:33 -07:00
ShyPike
5ecbf795af Merge remote-tracking branch 'remotes/origin/master' into 0.6.x
Conflicts:
	.gitignore
2011-09-08 21:39:43 +02:00
shypike
42b1202b06 Extend OSX DMG background images to avoid obscuring by the path bar. 2011-09-07 21:53:49 +02:00
shypike
37acad6ec7 Update text files for 0.6.9 RC2 2011-09-06 20:18:49 +02:00
shypike
74d90b8361 Correct formatting errors in translations. 2011-09-06 19:12:59 +02:00
shypike
952682d557 Update translations 2011-09-06 19:02:37 +02:00
shypike
c1f3e44fb8 Add hidden option "allow_64bit_tools" to allow or prevent use of 64bit par2 and unrar. 2011-09-04 16:37:38 +02:00
shypike
fa2a6c673c Improve handling of non-creatable "complete" and category folders.
Remove the tendency of the status-api to create final folders prematurely.
Make error message about folder creation translatable.
2011-09-04 13:00:32 +02:00
shypike
b46ae4782d GenericSorter should always uppercase the initial letter of "the", "a", "of" etc. at the start of a title.
"the way of the few" => "The Way of the Few".
2011-09-03 13:43:24 +02:00
shypike
6ebbc81792 Change the OSX DMG window size so that the optional "path bar" doesn't obsure part of the background image. 2011-09-03 13:12:35 +02:00
shypike
1d7170f193 Fix and I18N the "Downloaded in X days Y hours Z seconds" text in the history report. 2011-09-01 21:42:46 +02:00
shypike
3dea948ecc Update text files for 0.6.9 RC1. 2011-09-01 18:58:25 +02:00
shypike
1e960ae2e9 Check email parameters also when only email_rss option is enabled. 2011-09-01 18:55:44 +02:00
shypike
e6b21f1ca5 Attempt to remove memory leak due to pickling the admin files. Use Pickler object (and clear_memo) instead of pickle function. 2011-08-30 23:32:17 +02:00
shypike
ebd3cbd932 When removing a job folder in the "incomplete" folder, also remove files in subfolders and the subfolders. This is necessary because some operating systems produce stray files/folders. 2011-08-30 22:28:17 +02:00
shypike
b8fb5cbfbf Setting of WIN64 flag was done only after selecting unrar/par2 variants.
The result was that the x64 variants were never used.
Closes #3
2011-08-29 21:54:49 +02:00
shypike
01494bd010 When using the Download button in RSS feeds for newzbin, the nzb name was incorrect. Closes #2 2011-08-29 20:04:21 +02:00
ShyPike
dd95becdff Fix small differences caused by the rebase of "develop" to "master". 2011-08-27 11:10:45 +02:00
ShyPike
8d532a75ee Servers with square brackets in the name were not removed from the INI file.
The reason was that the required substitution of "[]" with "{}" wasn't done in all situations.
2011-08-27 10:18:32 +02:00
ShyPike
27ac80c154 Make package.py work in a git environment.
Remove bazaar specials and add .gitignore

Signed-off-by: ShyPike <shypike@sabnzbd.org>
2011-08-27 10:18:32 +02:00
ShyPike
15058c37b3 Move sabnzbd-template.sparseimage.zip from root to sub folder. 2011-08-27 10:18:32 +02:00
ShyPike
6b85bf0a49 Fix crash in test email due to incorrect type. 2011-08-27 10:18:31 +02:00
ShyPike
2b24ec2ece Windows binaries are now based on Python 2.6 or 2.7.
Installer will detect missing MS runtimes and offer to download and install.
2011-08-27 10:18:31 +02:00
ShyPike
d99369d4f0 Allow 'paused' as priority in Config->Categories. 2011-08-27 10:18:30 +02:00
ShyPike
f85ec9007c Fix failing server test (due to introduction of random IP pick). 2011-08-27 10:18:30 +02:00
ShyPike
f89ca6b390 Fix unset variable that prevented save of "future" queue item. 2011-08-27 10:18:30 +02:00
ShyPike
6571ba90ca Limited article retries for servers.
This will prevent most deadlock situations, if configured right.
2011-08-27 10:18:30 +02:00
ShyPike
a638d182ab For Usenet servers with multiple IP addresses, pick a random one per connection. 2011-08-27 10:17:00 +02:00
ShyPike
d5caeddb30 Remove smpl skin version number. 2011-08-27 10:17:00 +02:00
ShyPike
d818020915 Add pseudo-priority "Stop" that will send the job immediately to the post-processing queue. 2011-08-27 10:17:00 +02:00
ShyPike
9a0c8aa660 The -p command line parameter must override auto-resume by quota management at startup. 2011-08-27 10:16:47 +02:00
ShyPike
dd4918b6d8 Add missing 'week' text in skintext.py.
Update translations.
2011-08-27 10:16:47 +02:00
ShyPike
11f7260b40 Fixes for handling of priorities in job's attrib file. 2011-08-27 10:14:48 +02:00
ShyPike
052746e16f Update POT file. 2011-08-27 10:14:48 +02:00
ShyPike
c03921ad55 Rename some bps variables.
Rename quotum to quota.
2011-08-27 10:13:09 +02:00
ShyPike
b0cd07dfc0 Add manual quotum period.
Add reset-quotum button.
2011-08-27 10:05:59 +02:00
ShyPike
7e635e1f3d Improve quotum-period end-detection, despite SABnzbd not running during end event. 2011-08-27 10:05:36 +02:00
ShyPike
c5d675ac88 Webserver 404 (NotFound) errors will automatically redirect to the main page. 2011-08-27 10:05:35 +02:00
ShyPike
94b3629fa5 Bump version to 0.7.x. 2011-08-27 10:05:35 +02:00
ShyPike
0956915d33 Show quotum overshoot by showing negative numbers.
Better initial situation after upgrading from previous totals9.sab file.
2011-08-27 10:05:35 +02:00
ShyPike
5e3cd8d77b Rename "email" menu to "Notifications". 2011-08-27 10:05:35 +02:00
ShyPike
53aed799a1 Add universal Growl support.
- Windows: through GNTP library
- OSX: through Growl library for local server and GTNP for remote server
- Unix/Linux: through GNTP library
For Ubuntu there's support for NotifyOSD.
Made Growl class texts translatable.
2011-08-27 10:05:35 +02:00
ShyPike
3a95707d79 Implement pre-check.
Check presence of articles on the servers before actual downloading.
If the amount is too small for a successful download & repair, fail the job.
2011-08-27 09:59:56 +02:00
ShyPike
c59a7a91a9 Use a separate field for quotum period (m/w/d). 2011-08-27 09:56:19 +02:00
ShyPike
cdc086f2db Add support for quotum management. 2011-08-27 09:52:16 +02:00
Jon Saine
6dd4c0551e Reverted hoverIntent version back to r5. It was breaking the menu.
Forgot to upgrade plush.js for jquery 1.6.x compatibility.
2011-08-22 09:09:04 -05:00
Jon Saine
8225dae182 Converted some jquery code for 1.6.x compliance.
With jquery 1.6.x the attributes module was modified slightly to eliminate ambiguity between attributes and properties.
Long story short, $('blah').attr('checked', true) is now $('blah').prop('checked', true).
2011-08-22 09:09:03 -05:00
Jon Saine
9999065f0a jQuery 1.5.1 -> 1.6.2
-- http://blog.jquery.com/2011/06/30/jquery-162-released/
 -- 1.5 to 1.6 notes: http://blog.jquery.com/2011/05/12/jquery-1-6-1-released/
jQuery UI 1.8.10 -> 1.8.15
 -- http://jqueryui.com/docs/Changelog/1.8.15
 -- Several additional modules was included due to dependency for the upcoming folder browser code.
ColorBox 1.3.15 -> 1.3.17
 -- http://colorpowered.com/colorbox/core/README
jQuery.hoverIntent r5 -> r6
 -- http://cherne.net/brian/resources/jquery.hoverIntent.html#defects
2011-08-22 09:09:03 -05:00
Jeff George
90d11ada6b Kill .bzr stuff, add .gitignore & a pretty README.md for GitHub. 2011-08-22 09:09:03 -05:00
shypike
67a1ee3cde Update translations. 2011-08-20 17:00:40 +02:00
shypike
0f8df0f072 Update POT files. 2011-08-20 16:11:13 +02:00
shypike
9c9936e7ae Update UnRAR to release 4.01 2011-08-20 15:26:34 +02:00
shypike
92926a4825 Use different background images for Lion and (Snow)Leopard DMG files. 2011-08-20 12:10:13 +02:00
Jeff George
92ac787315 Kill .bzr stuff, add .gitignore & a pretty README.md for GitHub. 2011-08-16 16:17:27 -04:00
shypike
2cc53e8dc3 Make package.py work in a git environment.
Remove bazaar specials and add .gitignore
2011-08-16 10:43:55 +02:00
ShyPike
18cb5710f9 Servers with square brackets in the name were not removed from the INI file.
The reason was that the required substitution of "[]" with "{}" wasn't done in all situations.
2011-08-16 10:32:33 +02:00
ShyPike
11b8f23528 Fix crash of emailer.py when email notification is on, without having proper email settings. 2011-08-16 10:30:22 +02:00
ShyPike
cf72abedfd Update text files or 0.6.8 Final. 2011-08-11 20:42:14 +02:00
ShyPike
31f92ebdd5 Update text files for 0.6.8 RC1 2011-08-08 16:22:41 +02:00
ShyPike
39a6635229 Prevent occasional exit crashes of the Kronos module. 2011-08-08 14:11:00 +02:00
ShyPike
a87a70d9c6 Solve several RSS issues:
- Remove links from feeds that have been deleted from rss_data.sab
- Expired links should be re-examined when they re-appear in the feed
- Sanity check of rss_data.sab at startup could expire links without reason
- Reduce storage of expired links from 7 days to 3 days.
- Duplicates detected were not given the pseudo-priority "Duplicate" (if that option was set by the user)
2011-08-07 16:11:05 +02:00
ShyPike
2d79e2ac92 Update to 0.6.7 2011-08-04 19:30:26 +02:00
ShyPike
7982ef2f11 Update text files for 0.6.7 RC2. 2011-08-03 20:55:33 +02:00
ShyPike
9280daad41 - Classic/smpl: add link "Purge failed NZBs & delete files" 2011-08-03 20:54:59 +02:00
ShyPike
e22709b5ff Actually delete files when user selects "Purge History" and picks "Purge Failed NZBs & Delete files".
Add "Purge failed & delete files" to the Classic and smpl skins.
2011-08-03 20:21:39 +02:00
ShyPike
ed37330fee Work-around for the Plush top menu issue in Safari 5.1. 2011-08-01 23:10:12 +02:00
ShyPike
f1fb3f904b Update French. 2011-08-01 21:18:59 +02:00
ShyPike
a3b6bb9ad9 Update text files for 0.6.7 2011-08-01 19:56:29 +02:00
ShyPike
54e6431d11 Update translations. 2011-08-01 19:55:54 +02:00
ShyPike
3e6f46483c Launch browser for a second instance when no -b parameter is given.
This means that browser launch is always active when using the standard shortcut.
Only an explicit -b0 will suppress the launch.
2011-07-31 10:53:41 +02:00
ShyPike
f7aadb1b61 Allow jobs still waiting for post-processing to be deleted too. 2011-07-30 15:52:48 +02:00
ShyPike
c7d5ea6ecd Delay starting most tasks until the webserver is ready.
This prevents slow starting of the web server due to heavy downloading.
2011-07-30 13:45:31 +02:00
ShyPike
1060f15463 Prevent persistent end-of-queue action to be triggered at program startup. 2011-07-30 13:44:23 +02:00
ShyPike
559d3064a9 Support both ActiveState (for OSX older than 10.5 and 10.6) and Apple Python (for 10.6 and 10.7). 2011-07-29 00:00:16 +02:00
ShyPike
084e638d30 Update text files for 0.6.6 Final. 2011-07-26 20:55:11 +02:00
ShyPike
428ab1ad96 Prevent UI of running SABnzbd instance coming up when second instance is started with "--browser 0". 2011-07-26 20:53:48 +02:00
ShyPike
b15673d0cd Prevent crash on exit when there are disk problems. 2011-07-26 19:59:49 +02:00
ShyPike
abf15149e9 Fix crash in assembler when fatal disk error (like "full") occurs. 2011-07-26 18:48:01 +02:00
ShyPike
1f9e328c2d Fix bad formatting in README.rtf 2011-07-24 13:37:13 +02:00
ShyPike
2c2c1c93a1 Update text files for 0.6.6 RC1 2011-07-24 12:43:19 +02:00
ShyPike
196c06a17e Don't re-queue NZB-only downloads when the PP is set to "Download only". 2011-07-24 11:38:26 +02:00
ShyPike
be78580ece When retrying a download, ignore the "do not download samples" setting.
That setting can misfire without much clues for the user and this will silently correct it.
2011-07-24 11:17:49 +02:00
ShyPike
748d525ccc File an error message if RSS email template not found. 2011-07-24 00:41:28 +02:00
ShyPike
2269407b1c Improved fix for nzbclub RSS link issues. 2011-07-23 10:36:09 +02:00
ShyPike
af7beab5e9 In RSS: don't remove spaces from NZB URLs, but replace with '%20'.
Needed for proper nzbclub.com support.
2011-07-22 21:51:36 +02:00
ShyPike
fb9abcc583 Start generating the OSX tar.gz distro file again. 2011-07-18 22:52:22 +02:00
ShyPike
29e829de73 Update text files for 0.6.6 Beta 2. 2011-07-18 22:17:40 +02:00
ShyPike
5225925a78 Limit the amount of URL fetch attempts when the NZB is incomplete. 2011-07-18 22:15:37 +02:00
ShyPike
15bd0f8a78 For RSS feeds, show 401/402/403 authentication errors clearly. 2011-07-18 21:39:24 +02:00
ShyPike
1557f3f8e8 OSX did not handle nzb.gz files properly when using "Open With". 2011-07-18 20:36:13 +02:00
ShyPike
b0a833f3a9 Log original command line before any tampering. 2011-07-18 19:56:27 +02:00
ShyPike
8b66deef0e Log command line parameters. 2011-07-18 19:25:32 +02:00
ShyPike
61608545c5 Improve diskfree/disktotal for Posix.
Some systems have such large disks that they return a negative number for free/total blocks.
Detect this and trim to the highest possible size that could be represented (usually 8T).
2011-07-17 22:46:13 +02:00
ShyPike
4f163ad979 Delay removing an NZF file until it's payload file is complete and written to disk.
This will make recovery from crashes more likely.
2011-07-17 21:38:16 +02:00
ShyPike
a01cd34b58 Fix handle leak.
Caused by bug in CherryPy's memory-based session support.
Replace by file-based.
As we don't really need session support, disable it and make it available through the --sessions option.
2011-07-16 00:03:14 +02:00
ShyPike
e6a5b0de57 Remove obsolete function from api.py 2011-07-15 20:34:16 +02:00
ShyPike
b3b1209293 Fix duplicate email problem.
For each recipient a seperate email was sent, but the "to" was always the full list instead of just the current recipient.
2011-07-11 20:01:12 +02:00
ShyPike
4cc21efe90 OSX binary: stop needless console logging. 2011-07-10 13:05:12 +02:00
ShyPike
6acd599d53 Fix package.py. 2011-07-10 12:23:36 +02:00
ShyPike
b73f74470b Fix error message about missing Cheetah. 2011-07-10 12:16:35 +02:00
ShyPike
6b3efe9398 OSX binary: remove odd -psn_0_12345 parameter that would be mistaken for "-p -s -n". 2011-07-09 09:55:39 +02:00
ShyPike
e5c9058201 Update text files for 0.6.6 Beta 1. 2011-07-08 22:04:41 +02:00
ShyPike
897d982e2f Handle special login failure situation (we sent empty username, server doesn't accept). 2011-07-07 22:34:24 +02:00
ShyPike
333b83f3c5 Fix error in German translation. 2011-07-06 19:52:45 +02:00
ShyPike
94938847fb Due to 64bit ApplePython, disable argv-emulation feature in py2app. 2011-07-06 19:35:47 +02:00
ShyPike
64f17675e2 Compensate for some Apple-Python bugs. 2011-07-06 19:23:15 +02:00
ShyPike
a97974d66c Replace colon in folder names by a dash. 2011-07-04 21:13:35 +02:00
ShyPike
349395f67a For Windows only show script files with executable extensions. 2011-07-03 12:41:05 +02:00
ShyPike
344c1ff42d Update POT file. 2011-06-30 19:55:16 +02:00
ShyPike
08435bc138 Show proper error message when only invalid par2 files are found for a set. 2011-06-30 19:54:38 +02:00
ShyPike
ce00490b40 Handle error 502 during nntp login properly. 2011-06-29 20:44:29 +02:00
ShyPike
cc702dde6d Improve handling when user has set username/password for a server, while the server doesn't need authentication.
For incorrect authentication, some servers return code '502 ... Access denied...' instead of the official '481' code.
Handle that as a bad login too.
2011-06-26 15:56:08 +02:00
ShyPike
913121bcc4 Update translations. 2011-06-26 12:43:33 +02:00
ShyPike
6448f4ff5e Add texts for pre-check. 2011-06-25 21:55:21 +02:00
ShyPike
1fbb56fc6f Update quotum texts. 2011-06-25 09:49:36 +02:00
ShyPike
6442391589 Output po/nsis/SABnsis.pot in Unix format too. 2011-06-25 09:49:06 +02:00
ShyPike
80ca43fe7d Extension-based cleanup_list should delete files in all sub-folders too. 2011-06-24 20:52:55 +02:00
ShyPike
3514973f2a Avoid logging an exception when postproc1.sab is missing. 2011-06-24 00:34:41 +02:00
ShyPike
def241c5c4 Early addition of quotum management texts to enable translation. 2011-06-23 19:49:50 +02:00
ShyPike
04e1605b27 Smpl: remove "active" job name below the speed graph. It's inaccurate and misleading. 2011-06-22 21:26:29 +02:00
ShyPike
a931e80f2e Fix error in German output of Launchpad. 2011-06-21 21:40:30 +02:00
ShyPike
f891a0ceac Update translations. 2011-06-21 21:39:02 +02:00
ShyPike
41697d022f Set top_only off by default. 2011-06-21 21:21:00 +02:00
ShyPike
f0b1b9d284 Update POT files. 2011-06-21 21:15:43 +02:00
ShyPike
b1455398c8 Use "repair priority" in the sorting functions too. Make the auto_sort option effective immediately instead of only at a (re-)start. 2011-06-21 21:09:30 +02:00
ShyPike
396e779517 Fetching extra par2 files no longer ignores paused state (it will use priority value 3).
Top-priority will still ignore paused state.
2011-06-21 20:27:29 +02:00
ShyPike
e9bc192bc7 Do end-of-queue action also when only paused download queue items remain after the post-processing queue becomes empty. 2011-06-20 21:23:50 +02:00
ShyPike
287490d549 Split misc.py into misc.py, panic.py and powersup.py. 2011-06-20 20:45:13 +02:00
ShyPike
dcd55b552a Add some more translatable texts in anticipation of Growl support. 2011-06-19 20:24:48 +02:00
ShyPike
4ac2b0d27c Update Growl texts again. 2011-06-15 20:07:21 +02:00
ShyPike
ea4d1e9474 Update Growl texts. 2011-06-15 19:37:07 +02:00
ShyPike
61c29e232d Add translation hints to Growl texts. 2011-06-14 23:47:12 +02:00
ShyPike
7b6ef157e9 Add translation hints to Growl texts. 2011-06-14 23:43:52 +02:00
ShyPike
14d6a033ae Add translatable texts for future merge of extended Growl support. 2011-06-14 23:20:42 +02:00
ShyPike
1db567b5ae Update text files for 0.6.5 2011-06-13 12:05:07 +02:00
ShyPike
ad6c9a56d8 Update translations. 2011-06-13 11:58:26 +02:00
ShyPike
15f6732a5b Generic sort: failed to recognize year surounded with underscores. 2011-06-11 14:13:26 +02:00
ShyPike
e517218240 Quick fix for failing end-of-queue script (side-effect of removing Q-> delay). 2011-06-11 13:21:20 +02:00
ShyPike
89fc5fc124 Update translations. 2011-06-09 21:45:15 +02:00
ShyPike
4025087f89 msgfmt.py : add detection of %s mismatches between original and translated texts.
Print warning and skip text.
2011-06-08 19:55:29 +02:00
ShyPike
31b620b8e5 Update text files for 0.6.4 2011-06-07 23:59:42 +02:00
ShyPike
55786df7bd Fix 'have_sll' crash in the Wizard. 2011-06-07 23:56:35 +02:00
ShyPike
0a31600e50 Update text files for 0.6.3 Final. 2011-06-07 20:52:53 +02:00
ShyPike
5bf316ee68 Update translations. 2011-06-07 20:48:01 +02:00
ShyPike
9ca53f4c5c Save bookmarks file right after reading bookmarks from newzbin. 2011-06-07 20:26:33 +02:00
ShyPike
be7075afb5 Wizard: Step One: disable SSL checkbox when SSL support is not available. 2011-06-05 23:45:05 +02:00
ShyPike
138b3d9ae2 Add --new command line parameter to force a new instance instead of starting the UI of the existing instance. 2011-06-05 21:54:03 +02:00
ShyPike
2ac2ce6996 Plush: Add "missing articles" to hover popup in Queue. 2011-06-05 20:07:49 +02:00
ShyPike
0bc7e28aa6 Plush is the default skin, so it should be the fall-back for invalid names too. 2011-06-05 17:29:39 +02:00
ShyPike
477b058f3d Update text files for 0.6.3RC3 2011-06-05 09:59:17 +02:00
ShyPike
4762d5367b Prevent saving of job-admin when a duplicate is detected, because it doesn't have an ID yet. 2011-06-05 00:19:00 +02:00
ShyPike
bc382524c7 Removing job from download queue must be done when the Assembler has finished the last file, not when the Queue has read the last article.
This explains the time a job is invisible when going from Queue to History.
2011-06-04 16:12:46 +02:00
ShyPike
76597fbc9e Add logging when detecting a corrupt post-process queue file. 2011-06-04 15:10:21 +02:00
ShyPike
6f2e63b28f Add support for folder browser in the WebUI.
Extra api call:
Full form:
    api?mode=browser&name=<path>&output=json
Compact form:
    api?mode=browser&name=<path>&output=json&compact=1
2011-06-04 12:48:34 +02:00
ShyPike
3592a28873 Remove random string from Plush skin. 2011-06-04 11:14:30 +02:00
ShyPike
7055314c33 Better support for Linux hibernate/suspend by supporting DBus.UPower too. 2011-06-03 13:28:43 +02:00
ShyPike
c8782021b3 Stop making the tgz distro for osx sources. 2011-06-03 10:08:27 +02:00
ShyPike
361f641bd0 Repair entries from rss_data.sab as much as possible instead of discarding them. 2011-06-03 09:46:50 +02:00
ShyPike
f488482bfe Update translations. 2011-06-02 17:48:28 +02:00
ShyPike
951b35f26b Update text files for 0.6.3RC2 2011-06-02 17:41:31 +02:00
ShyPike
122bedeab8 In Config->Servers, when saving server settings, don't verify server address when 'enable' is off.
Otherwise it's not possible to disable a server that doesn't resolve any longer.
2011-06-02 16:29:10 +02:00
ShyPike
f7213767f0 Fine-tune the %fn support in folder names. 2011-06-02 16:13:05 +02:00
ShyPike
5bd84ac3b5 Sorting functions: allow %fn to be used in folder names too. 2011-06-02 16:06:44 +02:00
ShyPike
fc9ef22a1e Save job's admin to disk when it's individually paused. 2011-06-02 10:11:19 +02:00
ShyPike
66d42f7924 Log a bit more for Postprocessing. 2011-06-01 20:37:28 +02:00
ShyPike
d24fea13fd In API: use callback support also in error messages. 2011-06-01 20:17:17 +02:00
ShyPike
f34a02bcef Remove obsolete commented-out code from package.py 2011-06-01 10:49:39 +02:00
ShyPike
5b4ea24345 Remove obsolete cherrypy warning from package.py 2011-06-01 10:41:43 +02:00
ShyPike
309c556f35 On OSX, the topmenu did not always send the right URL to the browser. 2011-06-01 10:30:29 +02:00
ShyPike
bc92c51d5e Update text files for 0.6.3RC1 2011-05-31 23:46:30 +02:00
ShyPike
a54a1b4545 Title was not used as job name when manually downloading an item from an RSS overview.
Add special option rss_filenames to suppress use of RSS titles as job names.
Sanitize RSS titles before using them as name parameter in the manual RSS download links (due to trouble with the " character).
2011-05-31 23:31:34 +02:00
ShyPike
a511c22c54 Verify integrity of rss_data.sab at startup to prevent later crashes. 2011-05-31 22:28:33 +02:00
ShyPike
a7392f8991 Convert double quote character in Unix folder names to single quote (to avoid Samba issues). 2011-05-31 21:12:24 +02:00
ShyPike
44f189671a The single folder option will now let unrar and unzip unpack without any folders.
Also: skip unpack_check for any file containing characters unmappable to latin1.
2011-05-31 21:01:34 +02:00
ShyPike
de88dca243 Fix typo in unpack_check UI in Plush. 2011-05-31 20:26:31 +02:00
ShyPike
6c76c6a461 Update Curl license text. 2011-05-31 19:48:39 +02:00
ShyPike
2f0ab6979d Do a more reliable Python version check in get_from_url(). 2011-05-30 22:28:37 +02:00
ShyPike
de222cdb44 Use curl .exe only to communicate with an https session, othewise use urllib2. 2011-05-30 22:06:00 +02:00
ShyPike
f0129d4a76 Update translations. 2011-05-30 22:00:50 +02:00
ShyPike
64fe224160 Final attempt to fix the NZB-association problem for Windows (Vista/7/Server2008).
Replace standalone "curl" with the cygwin version of curl, which depends only on standard Windows DLLs.
Also fix a typo that prevented Python 2.5 from being detected.
2011-05-30 21:08:07 +02:00
ShyPike
f32cdc8c3a Fix saving of attributes of the Default catagory in the smpl skin. 2011-05-30 19:27:55 +02:00
ShyPike
0f1a03497c The NSIS uninstaller must remove Romanian email templates too. 2011-05-29 22:44:41 +02:00
ShyPike
a94d1145cd Support for binaries based on Python 2.6+. Include the VS90 DLL and manifest files as distrubuted by the Bazaar Windows installer. 2011-05-29 16:53:08 +02:00
ShyPike
8d470ca57f Add CherryPy module as complete source tree. 2011-05-28 20:48:56 +02:00
ShyPike
0bbc060c65 Support storing of files in the category folder without a job folder.
The user sets this behaviour by ending the catagory path with an asterisk *
The Sorting functions will ignore this feature.
2011-05-28 20:14:22 +02:00
ShyPike
62f4647117 Implement automatic redirection of HTTP to HTTPS, when server is set to HTTPS-only.
Required a patch of CherryPy (wsgiserver/__init__.py).
2011-05-28 13:50:31 +02:00
ShyPike
ce305e5bf9 Add UI for unpack_check and make the default value "on" again. 2011-05-27 19:37:02 +02:00
499 changed files with 135750 additions and 49375 deletions

15
.bzreol
View File

@@ -1,15 +0,0 @@
[LF]
.bzreol
.bzrignore
*.py
*.tmpl
*.js
*.css
*.txt
*.po
*.pot
*.sh
[CRLF]
*.bat
*.cmd
*.nsi

View File

@@ -1,10 +0,0 @@
*.pyc
*.pyo
build
dist
locale
srcdist
cherrypy
*.wpr
*.keep
*.bak

13
.bzrtab
View File

@@ -1,13 +0,0 @@
[NOTAB]
*.py
*.txt
*.po
*.pot
*.sh
*.bat
*.cmd
*.nsi
*.tmpl
*.js
*.css
[TAB]

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
#Compiled python
*.py[cod]
# Working folders for Win build
build/
dist/
locale/
srcdist/
# Generated email templates
email/*.tmpl
# Build results
SABnzbd*.zip
SABnzbd*.exe
SABnzbd*.gz
SABnzbd*.dmg
# WingIDE project files
*.wp[ru]
# General junk
*.keep
*.bak
*.log
# Some people use Emacs as an editor
\#*
.\#*

31
ABOUT.txt Normal file
View File

@@ -0,0 +1,31 @@
*******************************************
*** This is SABnzbd 1.2.x ***
*******************************************
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
thanks to its friendly 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.
There is an extensive Wiki on the use of SABnzbd.
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"
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 older releases.
*******************************************
*** Upgrading from 0.7.x and below ***
*******************************************
Empty your current queue
Stop SABnzbd.
Install new version
Start SABnzbd.

View File

@@ -1,718 +0,0 @@
-------------------------------------------------------------------------------
0.6.2Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed association of NZB files with SABnzbd, when using HTTPS-only on Windows Vista/7
(resulted in multiple running copies of SABnzbd instead of an NZB upload)
- Fixed incorrect email date header when using other language than English
-------------------------------------------------------------------------------
0.6.1Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed association of NZB files with SABnzbd (for real this time).
(resulted in multiple running copies of SABnzbd instead of an NZB upload)
-------------------------------------------------------------------------------
0.6.1RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed association of NZB files with SABnzbd
(resulted in multiple running copies of SABnzbd instead of an NZB upload)
- Fixed display of warning when upgrading from 0.4.x
- Fixed problem when entering NZB files with names containing accented characters
(happened only on OSX with Firefox 4)
- Fixed several textual issues
- Add PID file support for OSX/Unix (--pid <path>)
- Fix attribute error caused by missing category field in nzbmatrix RSS feeds
- Fix nzbmatrix category support by just reading the whole "Description" field.
- Pause downloader when downloaded file cannot be written to disk.
- Fix "~/" (home-dir) support for Unix/OSX and disable for Windows
- Add Romanian translation
-------------------------------------------------------------------------------
0.6.0Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix handling of semicolon in parameters for user scripts.
- Fix attribute handling of 'addlocalfile' API.
- Fixed broken Mobile skin
- Prevent loss of sabnzbd.ini when disk is full
-------------------------------------------------------------------------------
0.6.0RC4 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed History in Classic skin
-------------------------------------------------------------------------------
0.6.0RC3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed History paging with regards to queued jobs
- Changing category in the queue failed to change to the PP setting.
- Add support for categories in RSS feeds from nzb.su
-------------------------------------------------------------------------------
0.6.0RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Category returned by pre-queue script wasn't handled properly
- Stop warning about duplicate articles in an NZB, just summarize after download
- Speed-up the generation of RSS filter/preview page
- "paused" priority at the RSS feed-level wasn't handled properly
- Plush: fix some display issues
-------------------------------------------------------------------------------
0.6.0RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix hangup during scheduled restart.
- OSX: Launch browser from menu bar with HTTPS when only HTTPS is available.
- Fix problem that caused SABnzbd always to startup in paused state.
-------------------------------------------------------------------------------
0.6.0Beta5 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Plush: added TopMenu-toggle and show warning-line only when needed
- Plush: added "Now" menu entries for bookmarks, RSS and watched folder
- Fix failing scheduled event "pause-postprocessing"
- Improve handling of nzbmatrix waiting times
- Fix sub-optimal handling of password tries on encrypted rar files
- Allow "Default" category to be picked for all Sort functions
- Fix server test function
-------------------------------------------------------------------------------
0.6.0Beta4 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Unix/OSX: fixed download hangup when having non-responding servers
- Fixed paging problem in Plush
- Plush now uses full window width
- New server could be not added in "disabled" mode
- Prevent a discarded duplicate job from showing up as an orphan
- Fixed disabled default script in Plush
- Emails to multiple recipients are now sent per recipient
allowing more flexibility in email templates.
- Moved "email test" button to Config->Email
- Plush cookies renamed to prevent interference by older versions
- Stop "growl"-ing all queued jobs at startup (OSX)
- Unix/OSX: prevent queue lockup when server connect times out
- Fix problem with setting a scheduled server action
- The API should give the same byte totals as the UI
- Move email test to Config->Email
- Fix loss of PP settings when retrying
- When re-evaluating RSS entries, use original RSS categories
-------------------------------------------------------------------------------
0.6.0Beta3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed odd post-processing after upgrade
- Fixed "Send Group" option
- Fixed incorrect error handling for nzbmatrix unbookmarking
- Remove "unknown decoding" message for files without any downloaded articles
- Unstarted jobs inherited after upgrade would not download properly
- Fix failing server test after change of server address
- Add "Read all feeds now" button (will read and process all enabled feeds)
- Add 'nzb_key' for "add nzb"-only API access
- Prevent crash when incorrect host address triggers unexpected error code
- Interpret 0-sized NZB files from nzbmatrix as "not found" (and don't retry forever)
- Three way option for duplicate detection (off/ignore/pause)
- Plush: lots of styling, navigation and browser compatibility fixes
- Windows installer will now add a "Safe" entry to the Start menu
-------------------------------------------------------------------------------
0.6.0Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix problem in smpl skin's RSS download buttons
- Fix paging and flag display in Wizard
- Add clear-downloaded jobs in RSS
-------------------------------------------------------------------------------
0.6.0Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Text and translation fixes
-------------------------------------------------------------------------------
0.6.0Alpha13 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Duplicate NZBs will not be rejected but entered the queue
as "paused" and flagged. Manual resuming will start the download anyway.
- Add INI-only option to ignore unrar timestamps
- More Plush touch-ups
- Translation updates
-------------------------------------------------------------------------------
0.6.0Alpha12 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Delete from History did not always work
- "Add server" dialog was broken
- Add confirmation dialog to deleting from RSS feed table
- Improve Queue and History deletion and purge in Plush
- Cosmetic improvements to Plush
-------------------------------------------------------------------------------
0.6.0Alpha11 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Plush tweaks
- Failed URL-based NZB fetches were not moved to History but became dead queue entries
- Empty NZB files are now refused
- Make some hard-coded texts in Plush translatable
- Encrypted rar files without available password were not reported properly
- Remove race condition which could cause crash of URL-based NZB fetches
-------------------------------------------------------------------------------
0.6.0Alpha10 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed issue in email that prevented HTML emails templates from working
- Identical host:port combos are allowed for Usenet servers now
- After auto-pause om encrypted files don't stop again after the user resumes
- In RSS feeds give the feed-level category priority over the trackers's derived category
- Add API handlers for new [rss] and [categories] entries.
- Fix incorrect handling of End-Of-Queue script when NICE or IONICE parameters have been set
- Filter hidden files and folders from script pick list
- Make "folder_check" accessible from the UI
- End-of-queue action could be be cleared properly from the INI file
-------------------------------------------------------------------------------
0.6.0Alpha9 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Plush top menu and navigation improvements
- Active jobs sometimes showed up as orphaned jobs
- Post processing retries in SMPL skin (not fully funtional)
- Apply category attributes to RSS feed jobs correctly
- Drop support for Python 2.4
- Add json-p support by support 'callback' parameter in API
-------------------------------------------------------------------------------
0.6.0Alpha8 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Prevent active and waiting post-processing jobs from showing up
in the orphans list.
- Simplified Wizard (No skin selection, Usenet server first)
- Accept non-alphanumerical characters in {{category}}name.nzb
- Add priority support to RSS feeds
- Add 'paused' as an extra priority in RSS feeds
- Add %dn to Series terms (original job name)
- Add hidden option "never_repair".
- The Windows version writes its API URL to the registry
- Colons were removed from folder names
- Fix crash when an RSS feed had no filters
-------------------------------------------------------------------------------
0.6.0Alpha7 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Post-processing would crash on empty category folder
-------------------------------------------------------------------------------
0.6.0Alpha6 by The SABnzbd-Team
-------------------------------------------------------------------------------
Features:
- Plush now has tabbed Config pages
- Add retention time parameter to servers so that articles too old will not be tried
- Disable top_only option (interferes with "retention" implementation)
- Option to automatically remove nzbmatrix bookmark after successful download
Bugs:
- Prevent blocking of RSS preview by automatic RSS read-out
- Windows binary had issues with sending non-english emails
- Explicit RSS filter attributes (category, script, pp) influenced each other
within a feed definition
- rss_scan is now a scheduable event and will override the rss_rate option.
- Improve handling of colon characters in filenames (compensating for par2 issues)
- NZBs would be left behind, despite ".nzb" being in the cleanup-list
- When only individually paused jobs are in the queue, unpausing them
would not always resume downloading
- Use nzbmatrix.com's new V1.1 api.
- Sanitize nzbname parameter coming from API calls (led to problems with some
third-party utilities)
- NZB-chaining should ignore the duplicate NZB check and fix cleanup
of "complete" folder after NZB-chaining.
- Plush did not save end-of-queue action.
- Zipped NZB files from URLs were not accepted.
- OSX: The end-of-queue action "Hibernate" shut down the system.
-------------------------------------------------------------------------------
0.6.0Alpha5 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Plush now has multi operations for the queue
- You can now see any orphaned folders in "temporary download folder"
and choose to delete or to re-add to the queue (Classic: Config-Main, others: Connections)
(This replaces the "Scan" button, which was a blunt instrument)
- There is now Default category, which provides attributes for all "Default" settings.
The default PP, script and priority in Config->Switches are therefor removed.
- nzbmatrix bookmarks are removed after successful download/post-processing
(If you really don't want that, use the INI-only 'del_bookmark' option)
- NZB chaining now ignores duplicate check.
(Chaining is automatic re-queuing of a download containing only NZB(s))
- Improve sorting of rar files to enable streaming
- Normally when enabling HTTPS, the HTTP port will still be active.
Now you can choose to run HTTPS-only on the standard port.
-------------------------------------------------------------------------------
0.6.0Alpha4 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix crash after queue repair cycle, which prevented restart
- end-of-queue hibernate on OSX did shutdown instead of sleep
-------------------------------------------------------------------------------
0.6.0Alpha3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Season sort got confused when show title contained a dot
- Restore newzbin support in Plush
- Show speeds from MB and higher with one decimal
- Restore detection of empty downloads
- Fix crash in post-processor caused by non-plain-ASCII in user script output
- Proper converion of 0.5.x language codes and skin names
- A repair scan would pick up folders waiting for retry in History, creating double entries
- Small fixes
-------------------------------------------------------------------------------
0.6.0Alpha2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Proper packaging
-------------------------------------------------------------------------------
0.6.0Alpha1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Redesign of the download queue allowing repairs and retries of post-processing
- pre-queue user script
- optional email sent when RSS sends jobs to the queue
- Redesign of the Config-RSS pages (note: "smpl" does not work here)
- Full Windows Service support
- Byte counters per server and independent of History database
- Many bugfixes
-------------------------------------------------------------------------------
0.5.5RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Add Norwegian translation
- Remove illegal characters from nzbname parameter in API calls
- Add INI-only option "api_warnings" to enable/disable warnings
about unauthenticated access to the API.
-------------------------------------------------------------------------------
0.5.4Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Ensure that sabnzbd.ini has no group/world access (Unix, OSX)
-------------------------------------------------------------------------------
0.5.4RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Support unpacking of rar-files embedded in zip-files
- Compensate for IPV6 issues of Chrome on OSX
- Changes in dirscan-speed are now immediately effective
- Reduce amount of logging
- On Windows, detect whether sqlite3.dll is missing. If so, panic and
complain about lame virus scanner.
- Add hidden option "queue_complete_pers" to select non-persistence of
non-script end-of-queue actions.
- Restrict square bracket substitution to server names
- Add hidden option "allow_64bit_tools" to allow or prevent use of 64bit par2 and unrar.
-------------------------------------------------------------------------------
0.5.3Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Embed nzbmatrix categories in SABnzbd (preventing problem in OSX)
- Remove leading spaces in user script output display
- Solve problems with passing filenames with '&' as parameter in user scripts
- Allow leading empty lines in email templates
- Prevent oversized logfile when logging fails
- Remove double header in email (some email server choke on that)
-------------------------------------------------------------------------------
0.5.3RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed error in 101 season/episode handling (prevent years like 2010 from matching)
- Fixed error in saving some email options
- Accept ".nzb.gz" files coming from an URL fetch.
- Make sysload support for Linux more universal
- Restore newzbin support in UI
- RSS feeds are now listed in alphabetic order
- Fixed error handling & in names sent to post-processing script (Windows)
-------------------------------------------------------------------------------
0.5.3RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Support of nzbmatrix categories
- Support of X-DNZB headers for all indexing sites (allows categories for nzbs.org)
- Support 101 season/episode notation
- Use all text after the season/episode element as episode name (don't require '-')
- Support of categories in RSS feeds
(only for compatible feeds, like nzbmatrix.com and nzbs.org)
- Accumulate all groups from an NZB (for category matching)
- After reducing connections SABnzbd should retry full amount 10 min later
This event was sometimes missed
- Recursive unpack of archives should be done in nested folders instead of base folder
- Sort strings with an absolute path were not handled properly
- Restore compatibility with Python 2.4
- Several fixes to improve robustness of NZB retrieval from indexers
- Solved crash in NZB retrieval from indexers when explicit PP value was set
- Remove newzbin features from UI
-------------------------------------------------------------------------------
0.5.2 Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- API: &nzbname= was ignored for some sites
- API: Server rename did not work properly
- Remember end-of-queue action for next end-of-queue
- Some updates to Danish
-------------------------------------------------------------------------------
0.5.2 RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed: Large queue caused very slow UI and high memory usage
- Fixed: Some very large rar sets failed to unpack
- Fixed: garbled emails were sent
- Python user scripts now run properly on OSX
- Fixed: SeasonSort could produce trailing spaces, which fail on Windows
- Fixed: unpacking errors not properly reported to user script
- Keep trying when receiving garbage from nzbmatrix.com
- Add total-size fields to API-call History.
- Auto-search for free port when specified port is occupied
- Improve reporting of newzbin errors
-------------------------------------------------------------------------------
0.5.1 Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix accented character problems in emails
- Warn when user doesn't have permission to use a web port (Linux/OSX)
-------------------------------------------------------------------------------
0.5.1 RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Accept comma-separated email address in "email_to" option.
- Allow manual retry of URL-based NZB fetches when a bad NZB is received
- Add INI-file option "size_limit" that sets any oversized job
to paused and low priority.
- Added French translation of email template
- Add Danish translation
-------------------------------------------------------------------------------
0.5.1 RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Add 'size_limit' option. Any job bigger than this limit will be set to
low priority and be paused.
- TV Season sort now has "affected categories" too. Check your settings!!
- Fixed problems that could lead to an infinite unpacking loop
(when using a "sick" NZB and using +U instead of +D postprocessing)
- Duplicate title checking in RSS is now done across all feeds
- Fixed issues with accented characters in NZB names when using RSS
- Fixed issues with accented characters in category folder names when using RSS
- Plush: fix nzo range selection when clicking checkboxes
- When detecting an unsupported unrar on Posix systems, drop unsupported features
(This may lead to files being lost when duplicate names exist.)
- Sorting examples were sometimes wrong
- Sorting couldn't handle some pattern combinations
- Fixed priority handling for RSS feeds and newzbin bookmarks
- Fixed crash occuring when user profile contains characters outside ISO-8859-1
- Allow nzb-backup folder field to be cleared
- When using only manual newzbin bookmark reading, allow automatic un-bookmarking
- Fixed several translation errors and omissions
- Keep Windows awake during post-processing of last job (while queue is already empty)
- Make it possible to run Python user scripts on OSX
-------------------------------------------------------------------------------
0.5.0Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Relabeled to 0.5.0
-------------------------------------------------------------------------------
0.5.0RC8 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Add par2_multicore option to Config->Switches
- Windows/OSX: Replaced OpenSSL libraries
- Windows: Replaced par2-multicore by new version
-------------------------------------------------------------------------------
0.5.0RC7 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
- When priority of newzbin or RSS job comes from the category, it wasn't effective.
- When unpacked file and directly download file have the same name,
the latter got lost
- Make nzbmatrix error handling robust
- When apikey is present in API call, do not require username/password
- Windows installer is now multi-language too
- Windows installer warns about queue when upgrading from 0.4.x
-------------------------------------------------------------------------------
0.5.0RC6 by The SABnzbd-Team
-------------------------------------------------------------------------------
Changes:
- Add INI-only option "par2_multicore" to enable/disable par2-multicore
Fixes:
- Prevent crash in RSS due to odd characters
- Prevent deadlock in downloader when server is blocked
- Unrar option -ai should only be used on Windows
- Prevent deletion of par files of other sets in multi-set NZBs
-------------------------------------------------------------------------------
0.5.0RC5 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
- Incorrect job rename in SMPL skin on Windows
- INI-only option "no_penalties" to limit timeout penalties for
non-optional servers to 1 minute
- HTTPS certificate paths should default to admin_dir directive
- The second parameter of the user script should be the original NZB file name,
even when the job has been renamed.
- Show clear error message when RSS feed preview or force-download fails.
- Don't warn about RSS reading problems when a scheduled scan is done.
- Ignore empty "username" and "apikey" parameters in nzbmatrix.com URLs
and add stored values.
- Suppress authentication warning for API call "get_scripts".
It's used by some external utilities to determine if username/password is required.
-------------------------------------------------------------------------------
0.5.0RC4 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
- Ignore read-only attributes of files in a RAR archive
- Remove potential crash on OSX when handling corrupt par2 file
- Enable manual unblocking of permantly blocked servers
- Retry permanently blocked servers after 30 minutes (compensate for shoddy servers)
- INI-only option "folder_length_max" for trimming folder name size
(default 128 for Windows, for others 256)
- Give unrar preference over rar when both installed on a Posix system
-------------------------------------------------------------------------------
0.5.0RC3 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
- Avoid potential crash when server closes connection
- On Connections Page: show original error message sent by server
- Make sure unrar check isn't done for OSX
-------------------------------------------------------------------------------
0.5.0RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
- Increase timeout for nzb fetches from URLs to 60 sec
- Duplicate title checking in RSS is now case-insensitive
- Only warn about presense of old queue when no new queue is present yet
- Don't warn when a supported RAR (instead of UNRAR) program is present
-------------------------------------------------------------------------------
0.5.0RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
- Fix broken server test in Wizard and Config->Servers
- Add tbbmalloc.dll to par2 program (prevents problems when you have
another version of that file on your system)
- Fix uneven download speed of very large NZBs
- Include special par2 variant for OSX 10.6+ (automatically selected)
- Fix problem with the script folder path containing a space
- Fix disabling of RSS feeds when changing settings
- When forcing disconnect don't try to send "Quit" to a non-responding server
- Warn user (once) about presence of old queue file
- Updated German texts
-------------------------------------------------------------------------------
0.5.0Beta6 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
- Solve problems with unwilling servers and "random" server time-outs.
- Do not generate warnings for intermittent server problems.
- Joinable rar-sets were not always properly joined.
- Fixed timing problems when SABnzbd is running behind an Apache server
- Don't report failed default permissions setting when the user has set none
-------------------------------------------------------------------------------
0.5.0Beta5 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
- Removed potential deadlock from Downloader
- Allow backup servers to be used when all primaries blocked or inactive
- Scheduled PAUSE_ALL failed to stop downloading
- Will now continue unpacking other rar-sets when one rar-set fails to unpack
(but only when option "Post-process Verified jobs" is off)
- QuickPar messed up the par files of multi-set NZBs
- When the par2 tool joined multi-part files of a multi-NZB, it mixed up files
from other sets
- Work-around for a bug in par2-tbb (making if fail to join some multi-part jobs).
- When scaling down the number of connections of a server,
will now retry with the full amount 10 minutes later.
-------------------------------------------------------------------------------
0.5.0Beta4 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
* Fixed poor reliability of reading RSS feeds and subsequent NZB fetches
(caused by a too short time-out value).
* Posix: when user has not set a permissions value, use the account's
default permissions (prevents problems with permissions coming from rar files)
* When option "Post-process only verified jobs" is set, do not execute
user-script when job fails
* NZB files read in from categorized watched folder did not inherit
the category's priority
* Fixed error reporting about incorrect NZB file
* Improved year recognition in tvsort
* Some skin fixes
-------------------------------------------------------------------------------
0.5.0Beta3 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes:
* Solved problems with adding NZB by URL (also through RSS)
* Proper display of download speed in OSX menu
* Fixed compatibility of Plush and IE8
-------------------------------------------------------------------------------
0.5.0Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
Fixes
* nzbmatrix.com support fixed (fetch NZB and Wizard settings)
* Problems on some systems because of reading NZB-spec from newzbin
for each NZB file
* When an RSS feed has more than 9 filters, the order was disturbed
* Fixed several TVSort problems including a hangup
* Each RSS preview added jobs marked "will not be downloaded"
instead of just the very first scan.
* Email reporting disk-full was malformed
* Sample detection was to eager to detect
* Improve detection of 64bit versions of Windows Vista/7/Server2008
* Explicit priorities in nzb upload were ignored
* Corrections in translations
Improvements
* INI-only option to reduce the amount of admin saves (important for very large NZBs)
* INI-only option to enable/disable final folder renaming (__UNPACK/__FAILED)
* Fixed OSX uninstaller so that it will leave extra skins on the disk
* Association of NZB files with SABnzbd for OSX
* Retry folder renames and deletions on Windows
* Don't save RSS admin to disk, when no feeds are active
-------------------------------------------------------------------------------
0.5.0Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
Highlights
* Multi-language UI: English, French, Dutch, German and Swedish
* Start-up Wizard to get you going fast
* QuickCheck : on-the-fly par2 check. When a download is correct,
the normal par2 check is skipped
* Configuration changes take immediate effect (except skins and some system folders)
* Job priorities, individual job pausing, job renaming
* Flexible server setup (scheduled on/off, optional servers)
* The Plush and Smpl skins have been improved and are a lot faster than before
and they have paging
* New "Mobile" skin (replaces iPhone-skin)
* RSS supports more sites and has generally improved
* For Windows distribution: 64-bit par2 and unrar are used
on 64-bit Vista/Win7/Server2008
* Support for password protected rar-files (limited)
* Expanded TV/Movie sorting
More Tech stuff
* HTTPS support for the Web-UI
* Email templates to design your own emails (including multiple recipients)
* Extensive API for utility developers
* History survives a full queue/admin clean and future upgrades
* Windows: optional association of NZB files with SABnzbd (double-click support)
-------------------------------------------------------------------------------
0.5.0Alpha12 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Drop country names from language strings
* Fixed crash when setting script to None
* Add missing "Restart required" messages in Config->General in Classic and Smpl
* When pause-download-while-pp is reset, resume downloading immediately
-------------------------------------------------------------------------------
0.5.0Alpha11 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Fix foreign character problems in several places
* Add Swedish translation
* Plush tweaks
* Allow foreign characters in email templates
* Prevent selection of "Mobile" skin as primary
-------------------------------------------------------------------------------
0.5.0Alpha10 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Fix foreign character problems in several places
* top_only option wasn't changable on-the-fly
* Windows uninstaller left behind some files
* Complete the API with the del_config function
* German translation
* Plush tweaks
-------------------------------------------------------------------------------
0.5.0Alpha9 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Fixed bug in handling fetching of additional par2 files
* Fixed nzbmatrix support properly
* Fixed potential urlgrabber crash on startup
* Fixed problem with NZB files containing space before ".nzb"
* Fixed Windows Vista/7/Server2008 x64 detection
* Allow feed:// syntax in RSS feeds
* Restore 0.4.x compatibility for the RSS URL.
* Even more Plush improvements
-------------------------------------------------------------------------------
0.5.0Alpha8 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Fixed failing RSS filters
* Fixed nzbmatrix.com RSS support
Note: you URL must contain username and ApiKey
-------------------------------------------------------------------------------
0.5.0Alpha7 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Use RSS titles for job names instead of downloaded filename.
(This bypasses newzbin's filename mangling).
* URL grabber will do 5 retries before giving up (and generate
correct retry links in History).
* RSS support for nzbserien.org (and slight improvement for nzbs.org)
* Plush overhaul
* "smpl" is now the preferred skin, "Default" renamed to "Classic".
* Detect internal problems and restart SABnzbd if found
* Enable "hibernate" and "standby" as end-of-queue actions for Linux systems
(Will only work if supported by the kernel of your platform)
* Update to unrar 3.90 (OSX and Windows)
* Remove segment files after par2-based joining
* Internationalisation of all texts in History
* Internationalisation of email templates
* Prevent crash when encountering a corrupt par2 file during QuickCheck
* Added new api call: api?mode=warnings&name=clear
* Added new api call: api?mode=translate&value=ACRONYM&output=json
* Priorities in RSS feeds now work properly
* Add "Require" filter type for RSS feeds
* Add SSL type (V23, V2, V3), only needed for som odd ISP's
* Add INI-file-only option to skip the check after unrar.
This is to support people with consistently failing checks.
* Passwords for encrypted RARs can be set in the job title
-------------------------------------------------------------------------------
0.5.0Alpha6 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Add 64bit unrar/rar 3.90 for Windows
* "Mobile" skin (limited functionality)
* Removed a number category-related problems
* Scan enabled RSS feeds 15 sec after startup
* Get newzbin bookmarks 20 sec after startup
* Add scheduled pause-all command thats stops all disk activity
* Improved handling of accented characters (also in par2)
* Unrelated empty folders are no longer removed from "incomplete" folder
* Initial runs of RSS will no longer be downloaded by "download" button
instead thay are marked and can be downloaded individually
* TV Sort: Fixed double brackets () naming on items with the country in the name.
* Disable password completion by browsers on Config pages.
* Retry links in History were incorrrect
* Solved lots of small bugs
* Skin improvements
-------------------------------------------------------------------------------
0.5.0Alpha5 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Less bugs
-------------------------------------------------------------------------------
0.5.0Alpha4 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Less bugs
-------------------------------------------------------------------------------
0.5.0Alpha3 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Session/API key system ported from 0.4.11
* Multi-language, including Dutch and French translations
* Less bugs
-------------------------------------------------------------------------------
0.5.0Alpha2 by The SABnzbd-Team
-------------------------------------------------------------------------------
* Fixed failing delete-bookmark-on-newzbin feature.
* Wizard improvements
* Plush improvements
* par2 for OSX replaced
* Added penalty timeout for unresponsive servers (when flagged as "optional").
* Fixed some incompatibilities with Python 2.4 and 2.6
* TV sorting improved
* Support for :: or [::] as valid hostnames. :: listens on all ipv6 addresses.
* Allow non-conforming RSS feeds (fixes nzbmatrix.com issue)
-------------------------------------------------------------------------------
0.5.0Alpha1 by The SABnzbd-Team
-------------------------------------------------------------------------------
Core Stuff:
* Updated Cherrypy - Among other things, this means you can now use HTTPS for the
web ui and have a prettier login window if you use a login/pass.
We have also dropped the need for /sabnzbd/ in the urls you use to access sabnzbd.
It'll still work with /sabnzbd/, but it will also work without it now.
* New XML Parser - Results in lower memory usage when reading .nzb files, especially large ones.
* File Quick-Check - We can now skip par2 verification altogether in some cases by performing
a quick-check of file hashes before post-processing.
* New Quick-Start Wizard - If you don't have any servers set, you'll get a neat little
five-page wizard allowing you to change all the config settings you really need to worry about.
Stuff like "How should SABnzbd be accessible? Remotely or locally?" and "What's your server address",
in a step by step manner. Features a button on the server page to test the connection to the news server,
to make sure you have entered your details correctly.
* Revamped Config System - The configuration backend was overhauled in 0.5.
Many of the config pages have been changed around to make more sense,
you can now easily enable/disable servers, and in general all server interaction is much faster than before.
* E-mail Templates - The e-mail system from 0.4.x has been updated to have a full template system,
allowing you to customize e-mail alerts.
* File Association - .nzb files are now associated with SABnzbd, so you can just double click them
to load the file into your queue. Currently only Windows is fully supported, however
you can launch SABnzbd with arguments containing a path, or multiple paths to local nzb/rar/zip files.
* .TS Filejoining - File joining has been improved to allow support for joining multiple .TS files into one file.
* Date Sorting - To compliment series sorting, sorting has now been added for downloads with dates in their names,
allowing you to place files in daily, monthly, yearly folders with proper naming
* General Sorting - Sorting for general downloads allows users to expand the series sorting into
other types of downloads. Has support for years in titles allowing files to be placed in folders
depending on the decade.
The API:
* Totally overhauled for 0.5. Basically, you now have full access to near everything about
SABnzbd via POST and XML/JSON. See the full docs for more details. Why does this matter?
It means if you know any programming language and understand how to parse XML/JSON and POST data to an URI,
then you can write some application which can communicate with SABnzbd almost as easily as a template can.
The Queue & History:
* Per-Item Pause - Now, in addition to being able to pause the whole queue, you can also pause
individual items in the queue. You can also force downloads to start while the whole queue is paused.
* Priorities - The queue now has four priorities, Normal, High, Low and force.
Think of this as an easy method to move things around your queue, or to insert things into
specific areas of your queue. One use case is to set everything to "normal" by default,
so it works like 0.4.x. However, you can then add a new post or RSS feed as high-priority to have it be
inserted to the top of the queue, or add one as low-priority to have it inserted at the bottom of the queue
and keep below normal downloads that are added. Forced items will go straight to the top of the queue,
and will continue to download even if the queue is paused.
* New History Backend - The history is now stored in a database, so we can handle larger histories better,
and store more information about downloads.
Skins:
* General Template Changes - Templates have all been updated to support all the neat new features.
So if you're one of those guys still married to Default, don't worry about missing out on all this new stuff.
* Plush - Complete backend rewrite to work almost exclusively off the API [so it's MUCH faster],
and a reorganized (and more accessible!) main menu.
* SMPL - Has been reworked to be much faster and friendlier to use.
The default page now shows the top 5 items from the queue and history.
The queue and history are also now pages to stop loading a massive number of items.
Bugfixes:
* Sure!

View File

@@ -1,17 +1,35 @@
(c) Copyright 2007-2011 by "The SABnzbd-team" <team@sabnzbd.org>
(c) Copyright 2007-2017 by "The SABnzbd-team" <team@sabnzbd.org>
The SABnzbd-team is:
Current team:
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>
inpheaux <inpheaux@sabnzbd.org>
rAf <rAf@sabnzbd.org>
Honorary member (and original author)
Gregor Kaufmann <tdian@users.sourceforge.net>
The main contributors and moderators of the translations
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, 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.6.1
SABnzbd 1.2.1
-------------------------------------------------------------------------------
0) LICENSE
-------------------------------------------------------------------------------
(c) Copyright 2007-2011 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.
@@ -41,57 +41,69 @@ Use the "Help" button in the web-interface to be directed to the Help Wiki.
-------------------------------------------------------------------------------
3) INSTALL with only sources
3) INSTALL pre-built OSX binaries
-------------------------------------------------------------------------------
Download the DMG file, mount and drag the SABnzbd icon to Programs.
Just like you do with so many apps.
Make sure you pick the right folder, depending on your OSX version.
-------------------------------------------------------------------------------
4) INSTALL with only sources
-------------------------------------------------------------------------------
You need to have Python installed and some modules.
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 Mavericks or newer
Apple Python 2.7 Included in OSX (default)
Windows
Python-2.5.latest http://www.activestate.com
Python-2.6.latest
Python-2.7.latest
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/
yenc module >= 0.3 http://sabnzbd.sourceforge.net/yenc-0.3.tar.gz
http://sabnzbd.sourceforge.net/yenc-0.3-w32fixed.zip (Win32-only)
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/
gnu gettext http://www.gnu.org/software/gettext/
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.10 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.
If fou want multiple languages, you need to compile the translations.
If you want multiple languages, you need to compile the translations.
Start this from a shell terminal (or command prompt):
python tools/make_mo.py
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.
-------------------------------------------------------------------------------
4) TROUBLESHOOTING
5) TROUBLESHOOTING
-------------------------------------------------------------------------------
Your browser may start up with just an error page.
@@ -109,17 +121,17 @@ This will show a black window where logging information will be shown. This
may help you solve problems easier.
-------------------------------------------------------------------------------
5) MORE INFORMATION
6) MORE INFORMATION
-------------------------------------------------------------------------------
Visit the WIKI site:
http://wiki.sabnzbd.org/
https://sabnzbd.org/wiki/
-------------------------------------------------------------------------------
6) CREDITS
7) CREDITS
-------------------------------------------------------------------------------
Serveral parts of SABnzbd were built by other people, illustrating the
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

@@ -2,8 +2,8 @@
*** Known issues ***
*******************************************
- To prevent unexpectedly large NZBs from eating your quotum you can set
the option 'size_limit' in the INI file.
- To prevent unexpectedly large NZBs from eating your download quota you can set
the option 'size_limit' on the Config->Special page.
Any NZB larger than this size will be set to paused and get a low priority.
- When par2 or unrar hang up, never just stop SABnzbd.
@@ -12,33 +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
- 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
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 in the file sabnzbd.ini
setting the option "api_warnings" to 0.
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.
@@ -47,7 +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: 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.
@@ -55,21 +49,36 @@
We cannot solve this problem, because the Operating System (read Windows)
prevents the removal.
- Jobs deleted from the queue will leave downloaded files behind
in the "incomplete" folder.
You will have to remove these files manually.
- 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.
It has the same functionality, but is slower.
We've had reports about this issue on non-mainstream Linux platforms.
- OpenElec
- Squeeze Linux
There is a "special" option that will allow you to select an alternative library.
use_pickle = 1
See: https://sabnzbd.org/wiki/configuration/1.2/special

View File

@@ -1,4 +1,4 @@
(c) Copyright 2007-2011 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,467 +0,0 @@
; -*- coding: latin-1 -*-
;
; Copyright 2008-2011 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"
Name "${SAB_PRODUCT}"
OutFile "${SAB_FILE}"
; 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
InstallDir "$PROGRAMFILES\SABnzbd"
InstallDirRegKey HKEY_LOCAL_MACHINE "SOFTWARE\SABnzbd" ""
;DirText $(MsgSelectDir)
;Vista 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 $(MsgStartSab)
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\README.txt"
!define MUI_FINISHPAGE_SHOWREADME_TEXT $(MsgShowRelNote)
;!define MUI_FINISHPAGE_LINK "View the SABnzbdPlus Wiki"
;!define MUI_FINISHPAGE_LINK_LOCATION "http://wiki.sabnzbd.org/"
!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
;--------------------------------
;Languages
; 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 "Swedish"
!insertmacro MUI_LANGUAGE "Danish"
!insertmacro MUI_LANGUAGE "NORWEGIAN"
!insertmacro MUI_LANGUAGE "Romanian"
;--------------------------------
;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 "" "$INSTDIR\SABnzbd.exe"
FunctionEnd
;--------------------------------
Function .onInit
!insertmacro MUI_LANGDLL_DISPLAY
;make sure sabnzbd.exe isnt running..if so abort
loop:
StrCpy $0 "SABnzbd.exe"
KillProc::FindProcesses
StrCmp $0 "0" endcheck
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgCloseSab) IDOK loop IDCANCEL exitinstall
exitinstall:
Abort
endcheck:
FunctionEnd
Section "SABnzbd" SecDummy
SetOutPath "$INSTDIR"
IfFileExists $INSTDIR\sabnzbd.exe 0 endWarnExist
IfFileExists $INSTDIR\locale\nl\LC_MESSAGES\SABnzbd.mo endWarnExist 0
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION $(MsgOldQueue) IDOK endWarnExist IDCANCEL 0
Abort
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\nzb.ico" "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File"
;${registerExtension} "$INSTDIR\SABnzbd.exe" ".nzb" "NZB File"
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"
; Delete installation files are carefully as possible
; Using just rmdir /r "$instdir" is considered unsafe!
Delete "$INSTDIR\email\email-de.tmpl"
Delete "$INSTDIR\email\email-en.tmpl"
Delete "$INSTDIR\email\email-nl.tmpl"
Delete "$INSTDIR\email\email-fr.tmpl"
Delete "$INSTDIR\email\email-sv.tmpl"
Delete "$INSTDIR\email\email-da.tmpl"
Delete "$INSTDIR\email\email-nb.tmpl"
Delete "$INSTDIR\email\rss-de.tmpl"
Delete "$INSTDIR\email\rss-en.tmpl"
Delete "$INSTDIR\email\rss-nl.tmpl"
Delete "$INSTDIR\email\rss-fr.tmpl"
Delete "$INSTDIR\email\rss-sv.tmpl"
Delete "$INSTDIR\email\rss-da.tmpl"
Delete "$INSTDIR\email\rss-nb.tmpl"
RMDir "$INSTDIR\email"
RMDir /r "$INSTDIR\locale"
RMDir /r "$INSTDIR\interfaces\Classic"
RMDir /r "$INSTDIR\interfaces\Plush"
RMDir /r "$INSTDIR\interfaces\smpl"
RMDir /r "$INSTDIR\interfaces\Mobile"
RMDir /r "$INSTDIR\interfaces\wizard"
RMDir "$INSTDIR\interfaces"
RMDir /r "$INSTDIR\win\par2"
RMDir /r "$INSTDIR\win\unrar"
RMDir /r "$INSTDIR\win\unzip"
RMDir /r "$INSTDIR\win"
Delete "$INSTDIR\licenses\*.txt"
Delete "$INSTDIR\licenses\Python\*.txt"
RMDir "$INSTDIR\licenses\Python"
RMDir "$INSTDIR\licenses"
Delete "$INSTDIR\lib\libeay32.dll"
Delete "$INSTDIR\lib\pywintypes25.dll"
Delete "$INSTDIR\lib\ssleay32.dll"
Delete "$INSTDIR\lib\sabnzbd.zip"
Delete "$INSTDIR\lib\*.pyd"
RMDir /r "$INSTDIR\lib\"
RMDir /r "$INSTDIR\po\email"
RMDir /r "$INSTDIR\po\main"
RMDir /r "$INSTDIR\po\nsis"
RMDIR "$INSTDIR\po"
Delete "$INSTDIR\CHANGELOG.txt"
Delete "$INSTDIR\COPYRIGHT.txt"
Delete "$INSTDIR\email.tmpl"
Delete "$INSTDIR\GPL2.txt"
Delete "$INSTDIR\GPL3.txt"
Delete "$INSTDIR\INSTALL.txt"
Delete "$INSTDIR\ISSUES.txt"
Delete "$INSTDIR\LICENSE.txt"
Delete "$INSTDIR\nzbmatrix.txt"
Delete "$INSTDIR\MSVCR71.dll"
Delete "$INSTDIR\nzb.ico"
Delete "$INSTDIR\PKG-INFO"
Delete "$INSTDIR\python25.dll"
Delete "$INSTDIR\python26.dll"
Delete "$INSTDIR\README.txt"
Delete "$INSTDIR\SABnzbd-console.exe"
Delete "$INSTDIR\SABnzbd.exe"
Delete "$INSTDIR\SABnzbd-helper.exe"
Delete "$INSTDIR\SABnzbd-service.exe"
Delete "$INSTDIR\Sample-PostProc.cmd"
Delete "$INSTDIR\Uninstall.exe"
Delete "$INSTDIR\w9xpopen.exe"
RMDir "$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"
SectionEnd ; end of uninstall section
Section "un.$(MsgDelSettings)" DelSettings
Delete "$LOCALAPPDATA\sabnzbd\sabnzbd.ini"
RMDir /r "$LOCALAPPDATA\sabnzbd\admin"
SectionEnd
Section "un.$(MsgDelLogs)" DelLogs
RMDir /r "$LOCALAPPDATA\sabnzbd\logs"
SectionEnd
Section "un.$(MsgDelCache)" DelCache
RMDir /r "$LOCALAPPDATA\sabnzbd\cache"
RMDir "$LOCALAPPDATA\sabnzbd"
SectionEnd
; eof
;--------------------------------
;Language strings
; MsgWarnRunning 'Please close "SABnzbd.exe" first'
LangString MsgStartSab ${LANG_ENGLISH} "Start SABnzbd (hidden)"
LangString MsgStartSab ${LANG_DANISH} "Start SABnzbd"
LangString MsgStartSab ${LANG_GERMAN} "SABnzbd starten (unsichtbar)"
LangString MsgStartSab ${LANG_FRENCH} "D<EFBFBD>marrer SABnzbd (cach<63>)"
LangString MsgStartSab ${LANG_NORWEGIAN} "Start SABnzbd (hidden)"
LangString MsgStartSab ${LANG_DUTCH} "Start SABnzbd (verborgen)"
LangString MsgStartSab ${LANG_ROMANIAN} "Porneste SABnzbd (ascuns)"
LangString MsgStartSab ${LANG_SWEDISH} "Starta SABnzbd (dold)"
LangString MsgShowRelNote ${LANG_ENGLISH} "Show Release Notes"
LangString MsgShowRelNote ${LANG_DANISH} "Vis udgivelsesbem<65>rkninger"
LangString MsgShowRelNote ${LANG_GERMAN} "Versionshinweise anzeigen"
LangString MsgShowRelNote ${LANG_FRENCH} "Afficher les notes de version"
LangString MsgShowRelNote ${LANG_NORWEGIAN} "Show Release Notes"
LangString MsgShowRelNote ${LANG_DUTCH} "Toon vrijgave bericht"
LangString MsgShowRelNote ${LANG_ROMANIAN} "Arat<EFBFBD> Notele de Publicare"
LangString MsgShowRelNote ${LANG_SWEDISH} "Visa release noteringar"
LangString MsgSupportUs ${LANG_ENGLISH} "Support the project, Donate!"
LangString MsgSupportUs ${LANG_DANISH} "St<EFBFBD>tte projektet, donere!"
LangString MsgSupportUs ${LANG_GERMAN} "Bitte unterst<73>tzen Sie das Projekt durch eine Spende!"
LangString MsgSupportUs ${LANG_FRENCH} "Supportez le projet, faites un don !"
LangString MsgSupportUs ${LANG_NORWEGIAN} "Support the project, Donate!"
LangString MsgSupportUs ${LANG_DUTCH} "Steun het project, Doneer!"
LangString MsgSupportUs ${LANG_ROMANIAN} "Sustine proiectul, Doneaz<61>!"
LangString MsgSupportUs ${LANG_SWEDISH} "Donera och st<73>d detta projekt!"
LangString MsgCloseSab ${LANG_ENGLISH} "Please close $\"SABnzbd.exe$\" first"
LangString MsgCloseSab ${LANG_DANISH} "Luk 'SABnzbd.exe' f<>rst"
LangString MsgCloseSab ${LANG_GERMAN} "Schliessen Sie bitte zuerst $\"SABnzbd.exe$\"."
LangString MsgCloseSab ${LANG_FRENCH} "Quittez $\"SABnzbd.exe$\" avant l'installation, SVP"
LangString MsgCloseSab ${LANG_NORWEGIAN} "Please close $\"SABnzbd.exe$\" first"
LangString MsgCloseSab ${LANG_DUTCH} "Sluit $\"SABnzbd.exe$\" eerst af"
LangString MsgCloseSab ${LANG_ROMANIAN} "<EFBFBD>nchideti mai <20>nt<6E>i $\"SABnzbd.exe$\""
LangString MsgCloseSab ${LANG_SWEDISH} "Var v<>nlig st<73>ng $\"SABnzbd.exe$\" f<>rst"
LangString MsgOldQueue ${LANG_ENGLISH} " >>>> WARNING <<<<$\r$\n$\r$\nPlease, first check the release notes or go to http://wiki.sabnzbd.org/introducing-0-6-0 !"
LangString MsgOldQueue ${LANG_DANISH} " >>>> WARNING <<<<$\r$\n$\r$\nVenligst, kontroll<6C>r f<>rst udgivelsesnoter eller g<> til http://wiki.sabnzbd.org/introducing-0-6-0 !"
LangString MsgOldQueue ${LANG_GERMAN} " >>>> WARNUNG <<<<$\r$\n$\r$\nBitte zuerst die Versionsanmerkungen lesen oder $\"http://wiki.sabnzbd.org/introducing-0-6-0 besuchen!$\""
LangString MsgOldQueue ${LANG_FRENCH} " >>>> AVERTISSEMENT<<<<$\r$\n$\r$\nS'il vous pla<6C>t, v<>rifiez d'abord les notes de version ou visiter http://wiki.sabnzbd.org/introducing-0-6-0 !"
LangString MsgOldQueue ${LANG_NORWEGIAN} " >>>> WARNING <<<<$\r$\n$\r$\nPlease, first check the release notes or go to http://wiki.sabnzbd.org/introducing-0-6-0 !"
LangString MsgOldQueue ${LANG_DUTCH} " >>>> WAARSCHUWING <<<<$\r$\n$\r$\nLees eerst het vrijgave bericht of ga naar $\"http://wiki.sabnzbd.org/introducing-0-6-0 !$\""
LangString MsgOldQueue ${LANG_ROMANIAN} " >>>> ATENTIE <<<<$\r$\n$\r$\nV<6E> rug<75>m, verificati mai <20>nt<6E>i notele de publicare sau mergeti la http://wiki.sabnzbd.org/introducing-0-6-0 !"
LangString MsgOldQueue ${LANG_SWEDISH} " >>>> VARNING <<<<$\r$\n$\r$\nVar v<>nlig och l<>s versions noteringarna eller g<> till http://wiki.sabnzbd.org/introducing-0-6-0 !"
LangString MsgUninstall ${LANG_ENGLISH} "This will uninstall SABnzbd from your system"
LangString MsgUninstall ${LANG_DANISH} "Dette vil afinstallere SABnzbd fra dit system"
LangString MsgUninstall ${LANG_GERMAN} "Dies entfernt SABnzbd von Ihrem System"
LangString MsgUninstall ${LANG_FRENCH} "Ceci d<>sinstallera SABnzbd de votre syst<73>me"
LangString MsgUninstall ${LANG_NORWEGIAN} "This will uninstall SABnzbd from your system"
LangString MsgUninstall ${LANG_DUTCH} "Dit verwijdert SABnzbd van je systeem"
LangString MsgUninstall ${LANG_ROMANIAN} "Acest lucru va dezinstala SABnzbd din sistem"
LangString MsgUninstall ${LANG_SWEDISH} "Detta kommer att avinstallera SABnzbd fr<66>n systemet"
LangString MsgRunAtStart ${LANG_ENGLISH} "Run at startup"
LangString MsgRunAtStart ${LANG_DANISH} "K<EFBFBD>r ved opstart"
LangString MsgRunAtStart ${LANG_GERMAN} "Beim Systemstart ausf<73>hren"
LangString MsgRunAtStart ${LANG_FRENCH} "Lancer au d<>marrage"
LangString MsgRunAtStart ${LANG_NORWEGIAN} "Run at startup"
LangString MsgRunAtStart ${LANG_DUTCH} "Opstarten bij systeem start"
LangString MsgRunAtStart ${LANG_ROMANIAN} "Executare la pornire"
LangString MsgRunAtStart ${LANG_SWEDISH} "K<EFBFBD>r vid uppstart"
LangString MsgIcon ${LANG_ENGLISH} "Desktop Icon"
LangString MsgIcon ${LANG_DANISH} "Skrivebords ikon"
LangString MsgIcon ${LANG_GERMAN} "Desktop-Symbol"
LangString MsgIcon ${LANG_FRENCH} "Ic<EFBFBD>ne sur le Bureau"
LangString MsgIcon ${LANG_NORWEGIAN} "Desktop Icon"
LangString MsgIcon ${LANG_DUTCH} "Pictogram op bureaublad"
LangString MsgIcon ${LANG_ROMANIAN} "Icoan<EFBFBD> Desktop"
LangString MsgIcon ${LANG_SWEDISH} "Skrivbordsikon"
LangString MsgAssoc ${LANG_ENGLISH} "NZB File association"
LangString MsgAssoc ${LANG_DANISH} "NZB filtilknytning"
LangString MsgAssoc ${LANG_GERMAN} "Mit NZB-Dateien verkn<6B>pfen"
LangString MsgAssoc ${LANG_FRENCH} "Association des fichiers NZB"
LangString MsgAssoc ${LANG_NORWEGIAN} "NZB File association"
LangString MsgAssoc ${LANG_DUTCH} "NZB bestanden koppelen aan SABnzbd"
LangString MsgAssoc ${LANG_ROMANIAN} "Asociere cu Fisierele NZB"
LangString MsgAssoc ${LANG_SWEDISH} "NZB Filassosication"
LangString MsgDelProgram ${LANG_ENGLISH} "Delete Program"
LangString MsgDelProgram ${LANG_DANISH} "Slet program"
LangString MsgDelProgram ${LANG_GERMAN} "Programm l<>schen"
LangString MsgDelProgram ${LANG_FRENCH} "Supprimer le programme"
LangString MsgDelProgram ${LANG_NORWEGIAN} "Delete Program"
LangString MsgDelProgram ${LANG_DUTCH} "Verwijder programma"
LangString MsgDelProgram ${LANG_ROMANIAN} "Sterge Program"
LangString MsgDelProgram ${LANG_SWEDISH} "Ta bort programmet"
LangString MsgDelSettings ${LANG_ENGLISH} "Delete Settings"
LangString MsgDelSettings ${LANG_DANISH} "Slet instillinger"
LangString MsgDelSettings ${LANG_GERMAN} "Einstellungen l<>schen"
LangString MsgDelSettings ${LANG_FRENCH} "Supprimer les Param<61>tres"
LangString MsgDelSettings ${LANG_NORWEGIAN} "Delete Settings"
LangString MsgDelSettings ${LANG_DUTCH} "Verwijder instellingen"
LangString MsgDelSettings ${LANG_ROMANIAN} "Stergeti Set<65>ri"
LangString MsgDelSettings ${LANG_SWEDISH} "Ta bort inst<73>llningar"
LangString MsgDelLogs ${LANG_ENGLISH} "Delete Logs"
LangString MsgDelLogs ${LANG_DANISH} "Slet logs"
LangString MsgDelLogs ${LANG_GERMAN} "Protokoll l<>schen"
LangString MsgDelLogs ${LANG_FRENCH} "Supprimer les logs"
LangString MsgDelLogs ${LANG_NORWEGIAN} "Delete Logs"
LangString MsgDelLogs ${LANG_DUTCH} "Verwijder logging"
LangString MsgDelLogs ${LANG_ROMANIAN} "Stergeti Activitate"
LangString MsgDelLogs ${LANG_SWEDISH} "Ta bort logg"
LangString MsgDelCache ${LANG_ENGLISH} "Delete Cache"
LangString MsgDelCache ${LANG_DANISH} "Slet hukommelse"
LangString MsgDelCache ${LANG_GERMAN} "Cache l<>schen"
LangString MsgDelCache ${LANG_FRENCH} "Supprimer le Cache"
LangString MsgDelCache ${LANG_NORWEGIAN} "Delete Cache"
LangString MsgDelCache ${LANG_DUTCH} "Verwijder Cache"
LangString MsgDelCache ${LANG_ROMANIAN} "Stergeti Cache"
LangString MsgDelCache ${LANG_SWEDISH} "Ta bort tempor<6F>r-mapp"
Function un.onInit
!insertmacro MUI_UNGETLANGUAGE
FunctionEnd

View File

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

80
README.md Normal file
View File

@@ -0,0 +1,80 @@
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.
SABnzbd makes Usenet as simple and streamlined as possible by automating everything we can. All you have to do is add an .nzb. SABnzbd takes over from there, where it will be automatically downloaded, verified, repaired, extracted and filed away with zero human interaction.
If you want to know more you can head over to our website: http://sabnzbd.org.
## Resolving Dependencies
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` (only 2.7.x and higher, but not 3.x.x)
- `python-cheetah`
- `python-support`
- `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).
## Running SABnzbd from source
Once you've sorted out all the dependencies, simply run:
```
python SABnzbd.py
```
Or, if you want to run in the background:
```
python SABnzbd.py -d -f /path/to/sabnzbd.ini
```
If you want multi-language support, run:
```
python tools/make_mo.py
```
Our many other command line options are explained in depth [here](https://sabnzbd.org/wiki/advanced/command-line-parameters).
## About Our Repo
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.

61
README.mkd Normal file
View File

@@ -0,0 +1,61 @@
Release Notes - SABnzbd 1.2.1
==============================================
## 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,79 +0,0 @@
{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350
{\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\ql\qnatural
\f0\b\fs48 \cf0 SABnzbd 0.6.2\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural
\b0\fs26 \cf0 \
SABnzbd is an open-source cross-platform binary newsreader.\
It simplifies the process of downloading from Usenet dramatically,\
thanks to its friendly 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.\
\
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.6.0:\
{\field{\*\fldinst{HYPERLINK "http://wiki.sabnzbd.org/introducing-0-6-0"}}{\fldrslt http://wiki.sabnzbd.org/introducing-0-6-0}}\
\
\b Known problems and solutions\
\b0 Read the file
\b
\b0 "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.\
\
\
\b\fs40 Changes since 0.5.6
\b0\fs26 \
See: {\field{\*\fldinst{HYPERLINK "http://wiki.sabnzbd.org/introducing-0-6-0"}}{\fldrslt http://wiki.sabnzbd.org/introducing-0-6-0}}\
}

View File

@@ -1,63 +0,0 @@
*******************************************
*** This is SABnzbd 0.6.2 ***
*******************************************
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
thanks to its friendly 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.
There is an extensive Wiki on the use of SABnzbd.
http://wiki.sabnzbd.org/
IMPORTANT INFORMATION about release 0.6.0:
http://wiki.sabnzbd.org/introducing-0-6-0
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.
Also, your sabnzbd.ini file will be upgraded, making it
incompatible with release 0.5.x
*******************************************
*** Upgrading from 0.4.x ***
*******************************************
>>>>> 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.
*******************************************
*** Changes since 0.5.6 ***
*******************************************
See: http://wiki.sabnzbd.org/introducing-0-6-0

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python -OO
# Copyright 2008-2011 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

Binary file not shown.

5
cherrypy/VERSION.txt Normal file
View File

@@ -0,0 +1,5 @@
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.

371
cherrypy/__init__.py Normal file
View File

@@ -0,0 +1,371 @@
"""CherryPy is a pythonic, object-oriented HTTP framework.
CherryPy consists of not one, but four separate API layers.
The APPLICATION LAYER is the simplest. CherryPy applications are written as
a tree of classes and methods, where each branch in the tree corresponds to
a branch in the URL path. Each method is a 'page handler', which receives
GET and POST params as keyword arguments, and returns or yields the (HTML)
body of the response. The special method name 'index' is used for paths
that end in a slash, and the special method name 'default' is used to
handle multiple paths via a single handler. This layer also includes:
* the 'exposed' attribute (and cherrypy.expose)
* cherrypy.quickstart()
* _cp_config attributes
* cherrypy.tools (including cherrypy.session)
* cherrypy.url()
The ENVIRONMENT LAYER is used by developers at all levels. It provides
information about the current request and response, plus the application
and server environment, via a (default) set of top-level objects:
* cherrypy.request
* cherrypy.response
* cherrypy.engine
* cherrypy.server
* cherrypy.tree
* cherrypy.config
* cherrypy.thread_data
* cherrypy.log
* cherrypy.HTTPError, NotFound, and HTTPRedirect
* cherrypy.lib
The EXTENSION LAYER allows advanced users to construct and share their own
plugins. It consists of:
* Hook API
* Tool API
* Toolbox API
* Dispatch API
* Config Namespace API
Finally, there is the CORE LAYER, which uses the core API's to construct
the default components which are available at higher layers. You can think
of the default components as the 'reference implementation' for CherryPy.
Megaframeworks (and advanced users) may replace the default components
with customized or extended components. The core API's are:
* Application API
* Engine API
* Request API
* Server API
* WSGI API
These API's are described in the `CherryPy specification <https://bitbucket.org/cherrypy/cherrypy/wiki/CherryPySpec>`_.
"""
try:
import pkg_resources
except ImportError:
pass
from threading import local as _local
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect # noqa
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError # noqa
from cherrypy import _cplogging
from cherrypy import _cpdispatch as dispatch # noqa
from cherrypy import _cptools
from cherrypy._cptools import default_toolbox as tools, Tool
from cherrypy import _cprequest
from cherrypy.lib import httputil as _httputil
from cherrypy import _cptree
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
engine = win32.Win32Bus()
engine.console_control_handler = win32.ConsoleCtrlHandler(engine)
del win32
except ImportError:
engine = process.bus
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 before_request(self):
self.servings.append((serving.request, serving.response))
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:
resp.check_timeout()
engine.timeout_monitor = _TimeoutMonitor(engine)
engine.timeout_monitor.subscribe()
engine.autoreload = process.plugins.Autoreloader(engine)
engine.autoreload.subscribe()
engine.thread_manager = process.plugins.ThreadManager(engine)
engine.thread_manager.subscribe()
engine.signal_handler = process.plugins.SignalHandler(engine)
class _HandleSignalsPlugin(object):
"""Handle signals from other processes based on the configured
platform handlers above."""
def __init__(self, bus):
self.bus = bus
def subscribe(self):
"""Add the handlers based on the platform"""
if hasattr(self.bus, 'signal_handler'):
self.bus.signal_handler.subscribe()
if hasattr(self.bus, 'console_control_handler'):
self.bus.console_control_handler.subscribe()
engine.signals = _HandleSignalsPlugin(engine)
server = _cpserver.Server()
server.subscribe()
def quickstart(root=None, script_name='', config=None):
"""Mount the given root, start the builtin server (and engine), then block.
root: an instance of a "controller class" (a collection of page handler
methods) which represents the root of the application.
script_name: a string containing the "mount point" of the application.
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. If this contains
a [global] section, those entries will be used in the global
(site-wide) config.
"""
if config:
_global_conf_alias.update(config)
tree.mount(root, script_name, config)
engine.signals.subscribe()
engine.start()
engine.block()
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
way, we can easily dump those objects when we stop/start a new HTTP
conversation, yet still refer to them as module-level globals in a
thread-safe way.
"""
request = _cprequest.Request(_httputil.Host('127.0.0.1', 80),
_httputil.Host('127.0.0.1', 1111))
"""
The request object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None."""
response = _cprequest.Response()
"""
The response object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None."""
def load(self, request, response):
self.request = request
self.response = response
def clear(self):
"""Remove all attributes of self."""
self.__dict__.clear()
serving = _Serving()
class _ThreadLocalProxy(object):
__slots__ = ['__attrname__', '__dict__']
def __init__(self, attrname):
self.__attrname__ = attrname
def __getattr__(self, name):
child = getattr(serving, self.__attrname__)
return getattr(child, name)
def __setattr__(self, name, value):
if name in ('__attrname__', ):
object.__setattr__(self, name, value)
else:
child = getattr(serving, self.__attrname__)
setattr(child, name, value)
def __delattr__(self, name):
child = getattr(serving, self.__attrname__)
delattr(child, name)
def _get_dict(self):
child = getattr(serving, self.__attrname__)
d = child.__class__.__dict__.copy()
d.update(child.__dict__)
return d
__dict__ = property(_get_dict)
def __getitem__(self, key):
child = getattr(serving, self.__attrname__)
return child[key]
def __setitem__(self, key, value):
child = getattr(serving, self.__attrname__)
child[key] = value
def __delitem__(self, key):
child = getattr(serving, self.__attrname__)
del child[key]
def __contains__(self, key):
child = getattr(serving, self.__attrname__)
return key in child
def __len__(self):
child = getattr(serving, self.__attrname__)
return len(child)
def __nonzero__(self):
child = getattr(serving, self.__attrname__)
return bool(child)
# 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
# to the "serving" object)
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()
# Monkeypatch pydoc to allow help() to go through the threadlocal proxy.
# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve.
# The only other way would be to change what is returned from type(request)
# and that's not possible in pure Python (you'd have to fake ob_type).
def _cherrypy_pydoc_resolve(thing, forceload=0):
"""Given an object or a path to an object, get the object and its name."""
if isinstance(thing, _ThreadLocalProxy):
thing = getattr(serving, thing.__attrname__)
return _pydoc._builtin_resolve(thing, forceload)
try:
import pydoc as _pydoc
_pydoc._builtin_resolve = _pydoc.resolve
_pydoc.resolve = _cherrypy_pydoc_resolve
except ImportError:
pass
class _GlobalLogManager(_cplogging.LogManager):
"""A site-wide LogManager; routes to app.log or global log as appropriate.
This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
cherrypy.log() and cherrypy.log.access(). If either
function is called during a request, the message will be sent to the
logger for the current Application. If they are called outside of a
request, the message will be sent to the site-wide logger.
"""
def __call__(self, *args, **kwargs):
"""Log the given message to the app.log or global log as appropriate.
"""
# Do NOT use try/except here. See
# https://github.com/cherrypy/cherrypy/issues/945
if hasattr(request, 'app') and hasattr(request.app, 'log'):
log = request.app.log
else:
log = self
return log.error(*args, **kwargs)
def access(self):
"""Log an access message to the app.log or global log as appropriate.
"""
try:
return request.app.log.access()
except AttributeError:
return _cplogging.LogManager.access(self)
log = _GlobalLogManager()
# Set a default screen handler on the global log.
log.screen = True
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)
from cherrypy._helper import expose, popargs, url # noqa
# import _cpconfig last so it can reference other top-level objects
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 # 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()

332
cherrypy/_cpchecker.py Normal file
View File

@@ -0,0 +1,332 @@
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.
When this object is called at engine startup, it executes each
of its own methods whose names start with ``check_``. If you wish
to disable selected checks, simply add a line in your global
config which sets the appropriate method to False::
[global]
checker.check_skipped_app_config = False
You may also dynamically add or replace ``check_*`` methods in this way.
"""
on = True
"""If True (the default), run all checks; if False, turn off all checks."""
def __init__(self):
self._populate_known_types()
def __call__(self):
"""Run all check_* methods."""
if self.on:
oldformatwarning = warnings.formatwarning
warnings.formatwarning = self.formatwarning
try:
for name in dir(self):
if name.startswith('check_'):
method = getattr(self, name)
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
# This value should be set inside _cpconfig.
global_config_contained_paths = False
def check_app_config_entries_dont_start_with_script_name(self):
"""Check for Application config with sections that repeat script_name.
"""
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('/')
for key in app.config.keys():
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))
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):
"""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
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)')
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.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')
conf = request.config.get
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.'
else:
fulldir = ''
if os.path.isabs(dir):
fulldir = dir
if root:
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,))
else:
if not root:
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,))
if fulldir and not os.path.exists(fulldir):
if msg:
msg += '\n'
msg += ('%r (root + dir) is not an existing '
'filesystem path.' % fulldir)
if msg:
warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r'
% (msg, section, root, dir))
# -------------------------- Compatibility -------------------------- #
obsolete = {
'server.default_content_type': 'tools.response_headers.headers',
'log_access_file': 'log.access_file',
'log_config_options': None,
'log_file': 'log.error_file',
'log_file_not_found': None,
'log_request_headers': 'tools.log_headers.on',
'log_to_screen': 'log.screen',
'show_tracebacks': 'request.show_tracebacks',
'throw_errors': 'request.throw_errors',
'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
'cherrypy.Application(Root())))'),
}
deprecated = {}
def _compat(self, config):
"""Process config and warn on each obsolete or deprecated entry."""
for section, conf in config.items():
if isinstance(conf, dict):
for k, v in conf.items():
if k in self.obsolete:
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]' %
(k, self.deprecated[k], section))
else:
if section in self.obsolete:
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.'
% (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.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(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.items():
is_path_section = section.startswith('/')
if is_path_section and isinstance(conf, dict):
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))
else:
msg = (
'The config entry %r is invalid, '
'because the %r config namespace '
'is unknown.\n'
'section: [%s]' % (k, atoms[0], section))
warnings.warn(msg)
elif atoms[0] == 'tools':
if atoms[1] not in dir(cherrypy.tools):
msg = (
'The config entry %r may be invalid, '
'because the %r tool was not found.\n'
'section: [%s]' % (k, atoms[1], section))
warnings.warn(msg)
def check_config_namespaces(self):
"""Process config and warn on each unknown config namespace."""
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
self._known_ns(app)
# -------------------------- Config Types -------------------------- #
known_config_types = {}
def _populate_known_types(self):
b = [x for x in vars(builtins).values()
if type(x) is type(str)]
def traverse(obj, namespace):
for name in dir(obj):
# Hack for 3.2's warning about body_params
if name == 'body_params':
continue
vtype = type(getattr(obj, name, None))
if vtype in b:
self.known_config_types[namespace + '.' + name] = vtype
traverse(cherrypy.request, 'request')
traverse(cherrypy.response, 'response')
traverse(cherrypy.server, 'server')
traverse(cherrypy.engine, 'engine')
traverse(cherrypy.log, 'log')
def _known_types(self, config):
msg = ('The config entry %r in section %r is of type %r, '
'which does not match the expected type %r.')
for section, conf in config.items():
if isinstance(conf, dict):
for k, v in conf.items():
if v is not None:
expected_type = self.known_config_types.get(k, None)
vtype = type(v)
if expected_type and vtype != expected_type:
warnings.warn(msg % (k, section, vtype.__name__,
expected_type.__name__))
else:
k, v = section, conf
if v is not None:
expected_type = self.known_config_types.get(k, None)
vtype = type(v)
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.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.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.")

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

303
cherrypy/_cpconfig.py Normal file
View File

@@ -0,0 +1,303 @@
"""
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.
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
cherrypy.config.
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.
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
page handler (see next).
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::
[/path/to/my/page]
request.stream = True
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, set with the ``cherrypy.config`` decorator. For example::
@cherrypy.config(**{'tools.gzip.on': True})
class Demo:
@cherrypy.expose
@cherrypy.config(**{'request.show_tracebacks': False})
def index(self):
return "Hello world"
.. note::
This behavior is only guaranteed for the default dispatcher.
Other dispatchers may have different restrictions on where
you can attach config attributes.
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.
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
cherrypy._cpconfig.environments[environment]. It only applies to the global
config, and only when you use cherrypy.config.update.
You can define your own namespaces to be called at the Global, Application,
or Request level, by adding a named handler to cherrypy.config.namespaces,
app.namespaces, or app.request_class.namespaces. The name can
be any string, and the handler must be either a callable or a (Python 2.5
style) context manager.
"""
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
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,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': False,
'request.show_mismatched_params': False,
'log.screen': False,
},
'embedded': {
# For use with CherryPy embedded in another deployment stack.
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': False,
'request.show_mismatched_params': False,
'log.screen': False,
'engine.SIGHUP': None,
'engine.SIGTERM': None,
},
'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,
},
}
# Sphinx end config.environments
def _server_namespace_handler(k, v):
"""Config handler for the "server" namespace."""
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'):
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()
else:
cherrypy.servers[servername].unsubscribe()
else:
setattr(cherrypy.servers[servername], k, v)
else:
setattr(cherrypy.server, k, v)
Config.namespaces['server'] = _server_namespace_handler
def _engine_namespace_handler(k, v):
"""Config handler for the "engine" namespace."""
engine = cherrypy.engine
if k == 'SIGHUP':
engine.subscribe('SIGHUP', v)
elif k == 'SIGTERM':
engine.subscribe('SIGTERM', v)
elif '.' in k:
plugin, attrname = k.split('.', 1)
plugin = getattr(engine, plugin)
if attrname == 'on':
if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
plugin.subscribe()
return
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
def _tree_namespace_handler(k, v):
"""Namespace handler for the 'tree' config namespace."""
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

685
cherrypy/_cpdispatch.py Normal file
View File

@@ -0,0 +1,685 @@
"""CherryPy dispatchers.
A 'dispatcher' is the object which looks up the 'page handler' callable
and collects config for the current request based on the path_info, other
request attributes, and the application architecture. The core calls the
dispatcher as early as possible, passing it a 'path_info' argument.
The default dispatcher discovers the page handler by matching path_info
to a hierarchical arrangement of objects, starting at request.app.root.
"""
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 = sys.exc_info()[1]
try:
test_callable_spec(self.callable, self.args, self.kwargs)
except cherrypy.HTTPError:
raise sys.exc_info()[1]
except:
raise x
raise
def test_callable_spec(callable, callable_args, callable_kwargs):
"""
Inspect callable and test to see if the given args are suitable for it.
When an error occurs during the handler's invoking stage there are 2
erroneous cases:
1. Too many parameters passed to a function which doesn't define
one of *args or **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.
2. body parameters are also passed as keyword parameters.
3. when partial matching occurs, the final path atoms are passed as
positional args.
Both the query string and path atoms are part of the URI. If they are
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.serving.request, 'show_mismatched_params', False)
try:
(args, varargs, varkw, defaults) = getargspec(callable)
except TypeError:
if isinstance(callable, object) and hasattr(callable, '__call__'):
(args, varargs, varkw,
defaults) = getargspec(callable.__call__)
else:
# If it wasn't one of our own types, re-raise
# the original error
raise
if args and args[0] == 'self':
args = args[1:]
arg_usage = dict([(arg, 0,) for arg in args])
vararg_usage = 0
varkw_usage = 0
extra_kwargs = set()
for i, value in enumerate(callable_args):
try:
arg_usage[args[i]] += 1
except IndexError:
vararg_usage += 1
for key in callable_kwargs.keys():
try:
arg_usage[key] += 1
except KeyError:
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_with_defaults[i]] == 0:
arg_usage[args_with_defaults[i]] += 1
missing_args = []
multiple_args = []
for key, usage in arg_usage.items():
if usage == 0:
missing_args.append(key)
elif usage > 1:
multiple_args.append(key)
if missing_args:
# In the case where the method allows body arguments
# there are 3 potential errors:
# 1. not enough query string parameters -> 404
# 2. not enough body parameters -> 400
# 3. not enough path parts (partial matches) -> 404
#
# 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)
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.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
error = 404
else:
# Otherwise it's a 400 Bad Request
error = 400
message = None
if show_mismatched_params:
message = 'Multiple values for parameters: '\
'%s' % ','.join(multiple_args)
raise cherrypy.HTTPError(error, message=message)
if not varkw and varkw_usage > 0:
# If there were extra query string parameters, it's a 404 Not Found
extra_qs_params = set(qs_params).intersection(extra_kwargs)
if extra_qs_params:
message = None
if show_mismatched_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
extra_body_params = set(body_params).intersection(extra_kwargs)
if extra_body_params:
message = None
if show_mismatched_params:
message = 'Unexpected body parameters: '\
'%s' % ', '.join(extra_body_params)
raise cherrypy.HTTPError(400, message=message)
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
of the handler and the actual calling of the handler. This subclass
takes that into account, and allows request.params to be 'bound late'
(it's more complicated than that, but that's the effect).
"""
def _get_kwargs(self):
kwargs = cherrypy.serving.request.params.copy()
if self._kwargs:
kwargs.update(self._kwargs)
return kwargs
def _set_kwargs(self, kwargs):
cherrypy.serving.request.kwargs = kwargs
self._kwargs = kwargs
kwargs = property(_get_kwargs, _set_kwargs,
doc='page handler kwargs (with '
'cherrypy.request.params copied in)')
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'
attribute which evaluates to True. The special method name "index"
matches a URI which ends in a slash ("/"). The special method name
"default" may match a portion of the path_info (but only when no longer
substring of the path_info matches some other object).
This is the default, built-in dispatcher for CherryPy.
"""
dispatch_method_name = '_cp_dispatch'
"""
The name of the dispatch method that nodes may optionally implement
to provide their own dynamic dispatch algorithm.
"""
def __init__(self, dispatch_method_name=None,
translate=punctuation_to_underscores):
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.serving.request
func, vpath = self.find_handler(path_info)
if func:
# Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace('%2F', '/') for x in vpath]
request.handler = LateParamPageHandler(func, *vpath)
else:
request.handler = cherrypy.NotFound()
def find_handler(self, path):
"""Return the appropriate page handler, plus any virtual path.
This will return two objects. The first will be a callable,
which can be used to generate page output. Any parameters from
the query string or request body will be sent to that callable
as keyword arguments.
The callable is found by traversing the application's tree,
starting from cherrypy.request.app.root, and matching path
components to successive objects in the tree. For example, the
URL "/path/to/handler" might return root.path.to.handler.
The second object returned will be a list of names which are
'virtual path' components: parts of the URL which are dynamic,
and were not used when looking up the handler.
These virtual path components are passed to the handler as
positional arguments.
"""
request = cherrypy.serving.request
app = request.app
root = app.root
dispatch_name = self.dispatch_method_name
# Get config for the root object/path.
fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
fullpath_len = len(fullpath)
segleft = fullpath_len
nodeconf = {}
if hasattr(root, '_cp_config'):
nodeconf.update(root._cp_config)
if '/' in app.config:
nodeconf.update(app.config['/'])
object_trail = [['root', root, nodeconf, segleft]]
node = root
iternames = fullpath[:]
while iternames:
name = iternames[0]
# map to legal Python identifiers (e.g. replace '.' with '_')
objname = name.translate(self.translate)
nodeconf = {}
subnode = getattr(node, objname, None)
pre_len = len(iternames)
if subnode is None:
dispatch = getattr(node, dispatch_name, None)
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)
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'):
nodeconf.update(node._cp_config)
# Mix in values from app.config for this path.
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.
"""
base = cherrypy.config.copy()
# Note that we merge the config from each node
# even if that node was None.
for name, obj, conf, segleft in object_trail:
base.update(conf)
if 'tools.staticdir.dir' in conf:
base['tools.staticdir.section'] = '/' + \
'/'.join(fullpath[0:fullpath_len - segleft])
return base
# Try successive objects (reverse order)
num_candidates = len(object_trail) - 1
for i in range(num_candidates, -1, -1):
name, candidate, nodeconf, segleft = object_trail[i]
if candidate is None:
continue
# Try a "default" method on the current leaf.
if hasattr(candidate, 'default'):
defhandler = candidate.default
if getattr(defhandler, 'exposed', False):
# Insert any extra _cp_config from the default handler.
conf = getattr(defhandler, '_cp_config', {})
object_trail.insert(
i + 1, ['default', defhandler, conf, segleft])
request.config = set_conf()
# 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()
if i == num_candidates:
# We found the extra ".index". Mark request so tools
# can redirect if path_info has no trailing slash.
request.is_index = True
else:
# We're not at an 'index' handler. Mark request so tools
# can redirect if path_info has NO trailing slash.
# Note that this also includes handlers which take
# positional parameters (virtual paths).
request.is_index = False
return candidate, fullpath[fullpath_len - segleft:-1]
# We didn't find anything
request.config = set_conf()
return None, []
class MethodDispatcher(Dispatcher):
"""Additional dispatch based on cherrypy.request.method.upper().
Methods named GET, POST, etc will be called on an exposed class.
The method names must be all caps; the appropriate Allow header
will be output showing all capitalized method names as allowable
HTTP verbs.
Note that the containing class must be exposed, not the methods.
"""
def __call__(self, path_info):
"""Set handler and config for the current request."""
request = cherrypy.serving.request
resource, vpath = self.find_handler(path_info)
if resource:
# Set Allow header
avail = [m for m in dir(resource) if m.isupper()]
if 'GET' in avail and 'HEAD' not in avail:
avail.append('HEAD')
avail.sort()
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:
# Grab any _cp_config on the subhandler.
if hasattr(func, '_cp_config'):
request.config.update(func._cp_config)
# Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace('%2F', '/') for x in vpath]
request.handler = LateParamPageHandler(func, *vpath)
else:
request.handler = cherrypy.HTTPError(405)
else:
request.handler = cherrypy.NotFound()
class RoutesDispatcher(object):
"""A Routes based dispatcher for CherryPy."""
def __init__(self, full_result=False, **mapper_options):
"""
Routes dispatcher
Set full_result to True if you wish the controller
and the action to be passed on to the page handler
parameters. By default they won't be.
"""
import routes
self.full_result = full_result
self.controllers = {}
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.serving.request.handler = LateParamPageHandler(func)
else:
cherrypy.serving.request.handler = cherrypy.NotFound()
def find_handler(self, path_info):
"""Find the right page handler, and set request.config."""
import routes
request = cherrypy.serving.request
config = routes.request_config()
config.mapper = self.mapper
if hasattr(request, 'wsgi_environ'):
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:
params = result.copy()
if not self.full_result:
params.pop('controller', None)
params.pop('action', None)
request.params.update(params)
# Get config for the root object/path.
request.config = base = cherrypy.config.copy()
curpath = ''
def merge(nodeconf):
if 'tools.staticdir.dir' in nodeconf:
nodeconf['tools.staticdir.section'] = curpath or '/'
base.update(nodeconf)
app = request.app
root = app.root
if hasattr(root, '_cp_config'):
merge(root._cp_config)
if '/' in app.config:
merge(app.config['/'])
# Mix in values from app.config.
atoms = [x for x in path_info.split('/') if x]
if atoms:
last = atoms.pop()
else:
last = None
for atom in atoms:
curpath = '/'.join((curpath, atom))
if curpath in app.config:
merge(app.config[curpath])
handler = None
if result:
controller = result.get('controller')
controller = self.controllers.get(controller, controller)
if controller:
if isinstance(controller, classtype):
controller = controller()
# Get config from the controller.
if hasattr(controller, '_cp_config'):
merge(controller._cp_config)
action = result.get('action')
if action is not None:
handler = getattr(controller, action, None)
# Get config from the handler
if hasattr(handler, '_cp_config'):
merge(handler._cp_config)
else:
handler = controller
# Do the last path atom here so it can
# override the controller's _cp_config.
if last:
curpath = '/'.join((curpath, last))
if curpath in app.config:
merge(app.config[curpath])
return handler
def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
from cherrypy.lib import xmlrpcutil
def xmlrpc_dispatch(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.
This can be useful when running multiple sites within one CP server.
It allows several domains to point to different parts of a single
website structure. For example::
http://www.domain.example -> root
http://www.domain2.example -> root/domain2/
http://www.domain2.example:443 -> root/secure
can be accomplished via the following config::
[/]
request.dispatch = cherrypy.dispatch.VirtualHost(
**{'www.domain2.example': '/domain2',
'www.domain2.example:443': '/secure',
})
next_dispatcher
The next dispatcher object in the dispatch chain.
The VirtualHost dispatcher adds a prefix to the URL and calls
another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
use_x_forwarded_host
If True (the default), any "X-Forwarded-Host"
request header will be used instead of the "Host" header. This
is commonly added by HTTP servers (such as Apache) when proxying.
``**domains``
A dict of {host header value: virtual prefix} pairs.
The incoming "Host" request header is looked up in this dict,
and, if a match is found, the corresponding "virtual prefix"
value will be prepended to the URL path before calling the
next dispatcher. Note that you often need separate entries
for "example.com" and "www.example.com". In addition, "Host"
headers may contain the port number.
"""
from cherrypy.lib import httputil
def vhost_dispatch(path_info):
request = cherrypy.serving.request
header = request.headers.get
domain = header('Host', '')
if use_x_forwarded_host:
domain = header('X-Forwarded-Host', domain)
prefix = domains.get(domain, '')
if prefix:
path_info = httputil.urljoin(prefix, path_info)
result = next_dispatcher(path_info)
# Touch up staticdir config. See
# https://github.com/cherrypy/cherrypy/issues/614.
section = request.config.get('tools.staticdir.section')
if section:
section = section[len(prefix):]
request.config['tools.staticdir.section'] = section
return result
return vhost_dispatch

622
cherrypy/_cperror.py Normal file
View File

@@ -0,0 +1,622 @@
"""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 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.
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, query_string=''):
import cherrypy
self.request = cherrypy.serving.request
self.query_string = query_string
if '?' in path:
# Separate any params included in the path
path, self.query_string = path.split('?', 1)
# Note that urljoin will "do the right thing" whether url is:
# 1. a URL relative to root (e.g. "/dummy")
# 2. a URL relative to the current path
# Note that any query string will be discarded.
path = _urljoin(self.request.path_info, path)
# Set a 'path' member attribute so that code which traps this
# error can have access to it.
self.path = path
CherryPyException.__init__(self, path, self.query_string)
class HTTPRedirect(CherryPyException):
"""Exception raised when the request should be redirected.
This exception will force a HTTP redirect to the URL or URL's you give it.
The new URL must be passed as the first argument to the Exception,
e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
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.
"""
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.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")
# 3. a URL relative to the current path
# Note that any query string in cherrypy.request is discarded.
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
if status is None:
if request.protocol >= (1, 1):
status = 303
else:
status = 302
else:
status = int(status)
if status < 300 or status > 399:
raise ValueError('status must be between 300 and 399.')
self.status = status
CherryPyException.__init__(self, abs_urls, status)
def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent
self.
CherryPy uses this internally, but you can also use it to create an
HTTPRedirect object and set its output without *raising* the exception.
"""
import cherrypy
response = cherrypy.serving.response
response.status = status = self.status
if status in (300, 301, 302, 303, 307):
response.headers['Content-Type'] = 'text/html;charset=utf-8'
# "The ... URI SHOULD be given by the Location field
# in the response."
response.headers['Location'] = self.urls[0]
# "Unless the request method was HEAD, the entity of the response
# SHOULD contain a short hypertext note with a hyperlink to the
# new URI(s)."
msg = {
300: 'This resource can be found at ',
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)
elif status == 304:
# Not Modified.
# "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',
'Content-Range', 'Content-Type', 'Expires',
'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.
response.headers.pop('Content-Length', None)
elif status == 305:
# Use Proxy.
# self.urls[0] should be the URI of the proxy.
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)
def __call__(self):
"""Use this exception as a request.handler (raise self)."""
raise self
def clean_headers(status):
"""Remove any headers which should not apply to an error response."""
import cherrypy
response = cherrypy.serving.response
# Remove headers which applied to the original content,
# but do not apply to the error page.
respheaders = response.headers
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
# with a byte-range-resp-spec of "*". The instance-length
# 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 '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 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 = _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.')
# 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.
CherryPy uses this internally, but you can also use it to create an
HTTPError object and set its output without *raising* the exception.
"""
import cherrypy
response = cherrypy.serving.response
clean_headers(self.code)
# In all cases, finalize will be called after this method,
# so don't bother cleaning up response values here.
response.status = self.status
tb = None
if cherrypy.serving.request.show_tracebacks:
tb = format_exc()
response.headers.pop('Content-Length', None)
content = self.get_error_page(self.status, traceback=tb,
message=self._message)
response.body = content
_be_ie_unfriendly(self.code)
def get_error_page(self, *args, **kwargs):
return get_error_page(*args, **kwargs)
def __call__(self):
"""Use this exception as a request.handler (raise self)."""
raise self
@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).
This is equivalent to raising
:class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
"""
def __init__(self, path=None):
if path is None:
import cherrypy
request = cherrypy.serving.request
path = request.script_name + request.path_info
self.args = (path,)
HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
<title>%(status)s</title>
<style type="text/css">
#powered_by {
margin-top: 20px;
border-top: 2px solid black;
font-style: italic;
}
#traceback {
color: red;
}
</style>
</head>
<body>
<h2>%(status)s</h2>
<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>
</div>
</body>
</html>
'''
def get_error_page(status, **kwargs):
"""Return an HTML page, containing a pretty error response.
status should be an int or a str.
kwargs will be interpolated into the page template.
"""
import cherrypy
try:
code, reason, message = _httputil.valid_status(status)
except ValueError:
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)
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 iteritems(kwargs):
if v is None:
kwargs[k] = ''
else:
kwargs[k] = _escape(kwargs[k])
# Use a custom template or callable for the error page?
pages = cherrypy.serving.request.error_page
error_page = pages.get(code) or pages.get('default')
# Default template, can be overridden below.
template = _HTTPErrorTemplate
if error_page:
try:
if hasattr(error_page, '__call__'):
# 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:
# 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
kwargs['message'] = m
response = cherrypy.serving.response
response.headers['Content-Type'] = 'text/html;charset=utf-8'
result = template % kwargs
return result.encode('utf-8')
_ie_friendly_error_sizes = {
400: 512, 403: 256, 404: 512, 405: 256,
406: 512, 408: 512, 409: 512, 410: 256,
500: 512, 501: 512, 505: 512,
}
def _be_ie_unfriendly(status):
import cherrypy
response = cherrypy.serving.response
# For some statuses, Internet Explorer 5+ shows "friendly error
# messages" instead of our response.body if the body is smaller
# than a given size. Fix this by returning a body over that size
# (by adding whitespace).
# See http://support.microsoft.com/kb/q218155/
s = _ie_friendly_error_sizes.get(status, 0)
if s:
s += 1
# Since we are issuing an HTTP error status, we assume that
# the entity is short, and we should just collapse it.
content = response.collapse_body()
l = len(content)
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 + (ntob(' ') * (s - l))
response.body = content
response.headers['Content-Length'] = str(len(content))
def format_exc(exc=None):
"""Return exc (or sys.exc_info if None), formatted."""
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 = ntob('Unrecoverable error in the server.')
if extrabody is not None:
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])

464
cherrypy/_cplogging.py Normal file
View File

@@ -0,0 +1,464 @@
"""
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
import os
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
"""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"'
)
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)
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.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):
for h in log.handlers:
if isinstance(h, logging.FileHandler):
h.acquire()
h.stream.close()
h.stream = open(h.baseFilename, h.mode)
h.release()
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:
exc_info = _cperror._exc_info()
self.error_log.log(severity, ' '.join((self.time(), context, msg)), exc_info=exc_info)
def __call__(self, *args, **kwargs):
"""An alias for ``error``."""
return self.error(*args, **kwargs)
def access(self):
"""Write to the access log (in Apache/NCSA Combined Log format).
See 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
of the raw byte. Exceptions from this rule are " and \\, which are
escaped by prepending a backslash, and all whitespace characters,
which are written in their C-style notation (\\n, \\t, etc).
"""
request = cherrypy.serving.request
remote = request.remote
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 '-',
't': self.time(),
'r': request.request_line,
'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', '-'),
}
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()
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:
return h
# ------------------------- Screen handlers ------------------------- #
def _set_screen_handler(self, log, enable, stream=None):
h = self._get_builtin_handler(log, 'screen')
if enable:
if not h:
if stream is None:
stream = sys.stderr
h = logging.StreamHandler(stream)
h.setFormatter(logfmt)
h._cpbuiltin = 'screen'
log.addHandler(h)
elif h:
log.handlers.remove(h)
def _get_screen(self):
h = self._get_builtin_handler
has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen')
return bool(has_h)
def _set_screen(self, newvalue):
self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
screen = property(_get_screen, _set_screen,
doc="""Turn stderr/stdout logging on or off.
If you set this to True, it'll add the appropriate StreamHandler for
you. If you set it to False, it will remove the handler.
""")
# -------------------------- File handlers -------------------------- #
def _add_builtin_file_handler(self, log, fname):
h = logging.FileHandler(fname)
h.setFormatter(logfmt)
h._cpbuiltin = 'file'
log.addHandler(h)
def _set_file_handler(self, log, filename):
h = self._get_builtin_handler(log, 'file')
if filename:
if h:
if h.baseFilename != os.path.abspath(filename):
h.close()
log.handlers.remove(h)
self._add_builtin_file_handler(log, filename)
else:
self._add_builtin_file_handler(log, filename)
else:
if h:
h.close()
log.handlers.remove(h)
def _get_error_file(self):
h = self._get_builtin_handler(self.error_log, 'file')
if h:
return h.baseFilename
return ''
def _set_error_file(self, newvalue):
self._set_file_handler(self.error_log, newvalue)
error_file = property(_get_error_file, _set_error_file,
doc="""The filename for self.error_log.
If you set this to a string, it'll add the appropriate FileHandler for
you. If you set it to ``None`` or ``''``, it will remove the handler.
""")
def _get_access_file(self):
h = self._get_builtin_handler(self.access_log, 'file')
if h:
return h.baseFilename
return ''
def _set_access_file(self, newvalue):
self._set_file_handler(self.access_log, newvalue)
access_file = property(_get_access_file, _set_access_file,
doc="""The filename for self.access_log.
If you set this to a string, it'll add the appropriate FileHandler for
you. If you set it to ``None`` or ``''``, it will remove the handler.
""")
# ------------------------- WSGI handlers ------------------------- #
def _set_wsgi_handler(self, log, enable):
h = self._get_builtin_handler(log, 'wsgi')
if enable:
if not h:
h = WSGIErrorHandler()
h.setFormatter(logfmt)
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'))
def _set_wsgi(self, newvalue):
self._set_wsgi_handler(self.error_log, newvalue)
wsgi = property(_get_wsgi, _set_wsgi,
doc="""Write errors to wsgi.errors.
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.serving.request.wsgi_environ.get('wsgi.errors')
except (AttributeError, KeyError):
pass
else:
stream.flush()
def emit(self, record):
"""Emit a record."""
try:
stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
except (AttributeError, KeyError):
pass
else:
try:
msg = self.format(record)
fs = '%s\n'
import types
# 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'))
self.flush()
except:
self.handleError(record)

354
cherrypy/_cpmodpy.py Normal file
View File

@@ -0,0 +1,354 @@
"""Native adapter for serving CherryPy via mod_python
Basic usage:
##########################################
# Application in a module called myapp.py
##########################################
import cherrypy
class Root:
@cherrypy.expose
def index(self):
return 'Hi there, Ho there, Hey there'
# We will use this method from the mod_python configuration
# as the entry point to our application
def setup_server():
cherrypy.tree.mount(Root())
cherrypy.config.update({'environment': 'production',
'log.screen': False,
'show_tracebacks': False})
##########################################
# mod_python settings for apache2
# This should reside in your httpd.conf
# or a file that will be loaded at
# apache startup
##########################################
# Start
DocumentRoot "/"
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>
# End
The actual path to your mod_python.so is dependent on your
environment. In this case we suppose a global mod_python
installation on a Linux distribution such as Ubuntu.
We do set the PythonPath configuration setting so that
your application can be found by from the user running
the apache2 instance. Of course if your application
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 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 httputil
# ------------------------------ Request-handling
def setup(req):
from mod_python import apache
# Run any setup functions defined by a "PythonOption cherrypy.setup"
# directive.
options = req.get_options()
if 'cherrypy.setup' in options:
for function in options['cherrypy.setup'].split():
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'],
})
engine = cherrypy.engine
if hasattr(engine, 'signal_handler'):
engine.signal_handler.unsubscribe()
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:
newlevel = apache.APLOG_DEBUG
elif logging.INFO >= level:
newlevel = apache.APLOG_INFO
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
# 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:
# apache.register_cleanup wasn't available until 3.1.4.
apache.register_cleanup(cherrypy_cleanup)
except AttributeError:
req.server.register_cleanup(req, cherrypy_cleanup)
class _ReadOnlyRequest:
expose = ('read', 'readline', 'readlines')
def __init__(self, req):
for method in self.expose:
self.__dict__[method] = getattr(req, method)
recursive = False
_isSetUp = False
def handler(req):
from mod_python import apache
try:
global _isSetUp
if not _isSetUp:
setup(req)
_isSetUp = True
# Obtain a Request object from CherryPy
local = req.connection.local_addr
local = httputil.Host(
local[0], local[1], req.connection.local_host or '')
remote = req.connection.remote_addr
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
threaded = q(apache.AP_MPMQ_IS_THREADED)
forked = q(apache.AP_MPMQ_IS_FORKED)
except AttributeError:
bad_value = ("You must provide a PythonOption '%s', "
"either 'on' or 'off', when running a version "
'of mod_python < 3.1')
threaded = options.get('multithread', '').lower()
if threaded == 'on':
threaded = True
elif threaded == 'off':
threaded = False
else:
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 '/')
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 ''
reqproto = req.protocol
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')
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 = 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)
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()
send_response(
req, response.output_status, response.header_list,
response.body, response.stream)
finally:
app.release_serving()
except:
tb = format_exc()
cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR)
s, h, b = bare_error()
send_response(req, s, h, b)
return apache.OK
def send_response(req, status, headers, body, stream=False):
# Set response status
req.status = int(status[:3])
# Set response headers
req.content_type = 'text/plain'
for header, value in headers:
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, 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
def read_process(cmd, args=''):
fullcmd = '%s %s' % (cmd, args)
pipeout = popen(fullcmd)
try:
firstline = pipeout.readline()
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:
pipeout.close()
return output
class ModPythonServer(object):
template = """
# Apache2 server configuration file for running CherryPy with mod_python.
DocumentRoot "/"
Listen %(port)s
LoadModule python_module modules/mod_python.so
<Location %(loc)s>
SetHandler python-program
PythonHandler %(handler)s
PythonDebug On
%(opts)s
</Location>
"""
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)
for k, v in self.opts])
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')
f = open(mpconf, 'wb')
try:
f.write(conf_data)
finally:
f.close()
response = read_process(self.apache_path, '-k start -f %s' % mpconf)
self.ready = True
return response
def stop(self):
os.popen('apache -k stop')
self.ready = False

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

970
cherrypy/_cprequest.py Normal file
View File

@@ -0,0 +1,970 @@
import sys
import time
import warnings
import six
import cherrypy
from cherrypy._cpcompat import text_or_bytes, copykeys, ntob
from cherrypy._cpcompat import SimpleCookie, CookieError
from cherrypy import _cpreqbody, _cpconfig
from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import httputil, file_generator
class Hook(object):
"""A callback and its metadata: failsafe, priority, and kwargs."""
callback = None
"""
The bare callable that this Hook object is wrapping, which will
be called when the Hook is called."""
failsafe = False
"""
If True, the callback is guaranteed to run even if other callbacks
from the same call point raise exceptions."""
priority = 50
"""
Defines the order of execution for a list of Hooks. Priority numbers
should be limited to the closed interval [0, 100], but values outside
this range are acceptable, as are fractional values."""
kwargs = {}
"""
A set of keyword arguments that will be passed to the
callable on each call."""
def __init__(self, callback, failsafe=None, priority=None, **kwargs):
self.callback = callback
if failsafe is None:
failsafe = getattr(callback, 'failsafe', False)
self.failsafe = failsafe
if priority is None:
priority = getattr(callback, 'priority', 50)
self.priority = priority
self.kwargs = kwargs
def __lt__(self, other):
# Python 3
return self.priority < other.priority
def __cmp__(self, other):
# Python 2
return cmp(self.priority, other.priority)
def __call__(self):
"""Run self.callback(**self.kwargs)."""
return self.callback(**self.kwargs)
def __repr__(self):
cls = self.__class__
return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)'
% (cls.__module__, cls.__name__, self.callback,
self.failsafe, self.priority,
', '.join(['%s=%r' % (k, v)
for k, v in self.kwargs.items()])))
class HookMap(dict):
"""A map of call points to lists of callbacks (Hook objects)."""
def __new__(cls, points=None):
d = dict.__new__(cls)
for p in points or []:
d[p] = []
return d
def __init__(self, *a, **kw):
pass
def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
"""Append a new Hook made from the supplied arguments."""
self[point].append(Hook(callback, failsafe, priority, **kwargs))
def run(self, point):
"""Execute all registered Hooks (callbacks) for the given point."""
exc = None
hooks = self[point]
hooks.sort()
for hook in hooks:
# Some hooks are guaranteed to run even if others at
# the same hookpoint fail. We will still log the failure,
# but proceed on to the next hook. The only way
# to stop all processing from one of these hooks is
# to raise SystemExit and stop the whole server.
if exc is None or hook.failsafe:
try:
hook()
except (KeyboardInterrupt, SystemExit):
raise
except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
cherrypy.InternalRedirect):
exc = sys.exc_info()[1]
except:
exc = sys.exc_info()[1]
cherrypy.log(traceback=True, severity=40)
if exc:
raise exc
def __copy__(self):
newmap = self.__class__()
# We can't just use 'update' because we want copies of the
# mutable values (each is a list) as well.
for k, v in self.items():
newmap[k] = v[:]
return newmap
copy = __copy__
def __repr__(self):
cls = self.__class__
return '%s.%s(points=%r)' % (
cls.__module__,
cls.__name__,
copykeys(self)
)
# Config namespace handlers
def hooks_namespace(k, v):
"""Attach bare hooks declared in config."""
# Use split again to allow multiple hooks for a single
# hookpoint per path (e.g. "hooks.before_handler.1").
# Little-known fact you only get from reading source ;)
hookpoint = k.split('.', 1)[0]
if isinstance(v, text_or_bytes):
v = cherrypy.lib.attributes(v)
if not isinstance(v, Hook):
v = Hook(v)
cherrypy.serving.request.hooks[hookpoint].append(v)
def request_namespace(k, v):
"""Attach request attributes declared in config."""
# Provides config entries to set request.body attrs (like
# attempt_charsets).
if k[:5] == 'body.':
setattr(cherrypy.serving.request.body, k[5:], v)
else:
setattr(cherrypy.serving.request, k, v)
def response_namespace(k, v):
"""Attach response attributes declared in config."""
# Provides config entries to set default response headers
# http://cherrypy.org/ticket/889
if k[:8] == 'headers.':
cherrypy.serving.response.headers[k.split('.', 1)[1]] = v
else:
setattr(cherrypy.serving.response, k, v)
def error_page_namespace(k, v):
"""Attach error pages declared in config."""
if k != 'default':
k = int(k)
cherrypy.serving.request.error_page[k] = v
hookpoints = ['on_start_resource', 'before_request_body',
'before_handler', 'before_finalize',
'on_end_resource', 'on_end_request',
'before_error_response', 'after_error_response']
class Request(object):
"""An HTTP request.
This object represents the metadata of an HTTP request message;
that is, it contains attributes which describe the environment
in which the request URL, headers, and body were sent (if you
want tools to interpret the headers and body, those are elsewhere,
mostly in Tools). This 'metadata' consists of socket data,
transport characteristics, and the Request-Line. This object
also contains data regarding the configuration in effect for
the given URL, and the execution plan for generating a response.
"""
prev = None
"""
The previous Request object (if any). This should be None
unless we are processing an InternalRedirect."""
# Conversation/connection attributes
local = httputil.Host('127.0.0.1', 80)
'An httputil.Host(ip, port, hostname) object for the server socket.'
remote = httputil.Host('127.0.0.1', 1111)
'An httputil.Host(ip, port, hostname) object for the client socket.'
scheme = 'http'
"""
The protocol used between client and server. In most cases,
this will be either 'http' or 'https'."""
server_protocol = 'HTTP/1.1'
"""
The HTTP version for which the HTTP server is at least
conditionally compliant."""
base = ''
"""The (scheme://host) portion of the requested URL.
In some cases (e.g. when proxying via mod_rewrite), this may contain
path segments which cherrypy.url uses when constructing url's, but
which otherwise are ignored by CherryPy. Regardless, this value
MUST NOT end in a slash."""
# Request-Line attributes
request_line = ''
"""
The complete Request-Line received from the client. This is a
single string consisting of the request method, URI, and protocol
version (joined by spaces). Any final CRLF is removed."""
method = 'GET'
"""
Indicates the HTTP method to be performed on the resource identified
by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
DELETE. CherryPy allows any extension method; however, various HTTP
servers and gateways may restrict the set of allowable methods.
CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
query_string = ''
"""
The query component of the Request-URI, a string of information to be
interpreted by the resource. The query portion of a URI follows the
path component, and is separated by a '?'. For example, the URI
'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
'a=3&b=4'."""
query_string_encoding = 'utf8'
"""
The encoding expected for query string arguments after % HEX HEX decoding).
If a query string is provided that cannot be decoded with this encoding,
404 is raised (since technically it's a different URI). If you want
arbitrary encodings to not error, set this to 'Latin-1'; you can then
encode back to bytes and re-decode to whatever encoding you like later.
"""
protocol = (1, 1)
"""The HTTP protocol version corresponding to the set
of features which should be allowed in the response. If BOTH
the client's request message AND the server's level of HTTP
compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
If either is 1.0, this attribute will be the tuple (1, 0).
Lower HTTP protocol versions are not explicitly supported."""
params = {}
"""
A dict which combines query string (GET) and request entity (POST)
variables. This is populated in two stages: GET params are added
before the 'on_start_resource' hook, and POST params are added
between the 'before_request_body' and 'before_handler' hooks."""
# Message attributes
header_list = []
"""
A list of the HTTP request headers as (name, value) tuples.
In general, you should use request.headers (a dict) instead."""
headers = httputil.HeaderMap()
"""
A dict-like object containing the request headers. Keys are header
names (in Title-Case format); however, you may get and set them in
a case-insensitive manner. That is, headers['Content-Type'] and
headers['content-type'] refer to the same value. Values are header
values (decoded according to :rfc:`2047` if necessary). See also:
httputil.HeaderMap, httputil.HeaderElement."""
cookie = SimpleCookie()
"""See help(Cookie)."""
rfile = None
"""
If the request included an entity (body), it will be available
as a stream in this attribute. However, the rfile will normally
be read for you between the 'before_request_body' hook and the
'before_handler' hook, and the resulting string is placed into
either request.params or the request.body attribute.
You may disable the automatic consumption of the rfile by setting
request.process_request_body to False, either in config for the desired
path, or in an 'on_start_resource' or 'before_request_body' hook.
WARNING: In almost every case, you should not attempt to read from the
rfile stream after CherryPy's automatic mechanism has read it. If you
turn off the automatic parsing of rfile, you should read exactly the
number of bytes specified in request.headers['Content-Length'].
Ignoring either of these warnings may result in a hung request thread
or in corruption of the next (pipelined) request.
"""
process_request_body = True
"""
If True, the rfile (if any) is automatically read and parsed,
and the result placed into request.params or request.body."""
methods_with_bodies = ('POST', 'PUT')
"""
A sequence of HTTP methods for which CherryPy will automatically
attempt to read a body from the rfile. If you are going to change
this property, modify it on the configuration (recommended)
or on the "hook point" `on_start_resource`.
"""
body = None
"""
If the request Content-Type is 'application/x-www-form-urlencoded'
or multipart, this will be None. Otherwise, this will be an instance
of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
can .read()); this value is set between the 'before_request_body' and
'before_handler' hooks (assuming that process_request_body is True)."""
# Dispatch attributes
dispatch = cherrypy.dispatch.Dispatcher()
"""
The object which looks up the 'page handler' callable and collects
config for the current request based on the path_info, other
request attributes, and the application architecture. The core
calls the dispatcher as early as possible, passing it a 'path_info'
argument.
The default dispatcher discovers the page handler by matching path_info
to a hierarchical arrangement of objects, starting at request.app.root.
See help(cherrypy.dispatch) for more information."""
script_name = ''
"""
The 'mount point' of the application which is handling this request.
This attribute MUST NOT end in a slash. If the script_name refers to
the root of the URI, it MUST be an empty string (not "/").
"""
path_info = '/'
"""
The 'relative path' portion of the Request-URI. This is relative
to the script_name ('mount point') of the application which is
handling this request."""
login = None
"""
When authentication is used during the request processing this is
set to 'False' if it failed and to the 'username' value if it succeeded.
The default 'None' implies that no authentication happened."""
# Note that cherrypy.url uses "if request.app:" to determine whether
# the call is during a real HTTP request or not. So leave this None.
app = None
"""The cherrypy.Application object which is handling this request."""
handler = None
"""
The function, method, or other callable which CherryPy will call to
produce the response. The discovery of the handler and the arguments
it will receive are determined by the request.dispatch object.
By default, the handler is discovered by walking a tree of objects
starting at request.app.root, and is then passed all HTTP params
(from the query string and POST body) as keyword arguments."""
toolmaps = {}
"""
A nested dict of all Toolboxes and Tools in effect for this request,
of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
config = None
"""
A flat dict of all configuration entries which apply to the
current request. These entries are collected from global config,
application config (based on request.path_info), and from handler
config (exactly how is governed by the request.dispatch object in
effect for this request; by default, handler config can be attached
anywhere in the tree between request.app.root and the final handler,
and inherits downward)."""
is_index = None
"""
This will be True if the current request is mapped to an 'index'
resource handler (also, a 'default' handler if path_info ends with
a slash). The value may be used to automatically redirect the
user-agent to a 'more canonical' URL which either adds or removes
the trailing slash. See cherrypy.tools.trailing_slash."""
hooks = HookMap(hookpoints)
"""
A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
Each key is a str naming the hook point, and each value is a list
of hooks which will be called at that hook point during this request.
The list of hooks is generally populated as early as possible (mostly
from Tools specified in config), but may be extended at any time.
See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
error_response = cherrypy.HTTPError(500).set_response
"""
The no-arg callable which will handle unexpected, untrapped errors
during request processing. This is not used for expected exceptions
(like NotFound, HTTPError, or HTTPRedirect) which are raised in
response to expected conditions (those should be customized either
via request.error_page or by overriding HTTPError.set_response).
By default, error_response uses HTTPError(500) to return a generic
error response to the user-agent."""
error_page = {}
"""
A dict of {error code: response filename or callable} pairs.
The error code must be an int representing a given HTTP error code,
or the string 'default', which will be used if no matching entry
is found for a given numeric code.
If a filename is provided, the file should contain a Python string-
formatting template, and can expect by default to receive format
values with the mapping keys %(status)s, %(message)s, %(traceback)s,
and %(version)s. The set of format mappings can be extended by
overriding HTTPError.set_response.
If a callable is provided, it will be called by default with keyword
arguments 'status', 'message', 'traceback', and 'version', as for a
string-formatting template. The callable must return a string or
iterable of strings which will be set to response.body. It may also
override headers or perform any other processing.
If no entry is given for an error code, and no 'default' entry exists,
a default template will be used.
"""
show_tracebacks = True
"""
If True, unexpected errors encountered during request processing will
include a traceback in the response body."""
show_mismatched_params = True
"""
If True, mismatched parameters encountered during PageHandler invocation
processing will be included in the response body."""
throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
"""The sequence of exceptions which Request.run does not trap."""
throw_errors = False
"""
If True, Request.run will not trap any errors (except HTTPRedirect and
HTTPError, which are more properly called 'exceptions', not errors)."""
closed = False
"""True once the close method has been called, False otherwise."""
stage = None
"""
A string containing the stage reached in the request-handling process.
This is useful when debugging a live server with hung requests."""
namespaces = _cpconfig.NamespaceSet(
**{'hooks': hooks_namespace,
'request': request_namespace,
'response': response_namespace,
'error_page': error_page_namespace,
'tools': cherrypy.tools,
})
def __init__(self, local_host, remote_host, scheme='http',
server_protocol='HTTP/1.1'):
"""Populate a new Request object.
local_host should be an httputil.Host object with the server info.
remote_host should be an httputil.Host object with the client info.
scheme should be a string, either "http" or "https".
"""
self.local = local_host
self.remote = remote_host
self.scheme = scheme
self.server_protocol = server_protocol
self.closed = False
# Put a *copy* of the class error_page into self.
self.error_page = self.error_page.copy()
# Put a *copy* of the class namespaces into self.
self.namespaces = self.namespaces.copy()
self.stage = None
def close(self):
"""Run cleanup code. (Core)"""
if not self.closed:
self.closed = True
self.stage = 'on_end_request'
self.hooks.run('on_end_request')
self.stage = 'close'
def run(self, method, path, query_string, req_protocol, headers, rfile):
r"""Process the Request. (Core)
method, path, query_string, and req_protocol should be pulled directly
from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
path
This should be %XX-unquoted, but query_string should not be.
When using Python 2, they both MUST be byte strings,
not unicode strings.
When using Python 3, they both MUST be unicode strings,
not byte strings, and preferably not bytes \x00-\xFF
disguised as unicode.
headers
A list of (name, value) tuples.
rfile
A file-like object containing the HTTP request entity.
When run() is done, the returned object should have 3 attributes:
* status, e.g. "200 OK"
* header_list, a list of (name, value) tuples
* body, an iterable yielding strings
Consumer code (HTTP servers) should then access these response
attributes to build the outbound stream.
"""
response = cherrypy.serving.response
self.stage = 'run'
try:
self.error_response = cherrypy.HTTPError(500).set_response
self.method = method
path = path or '/'
self.query_string = query_string or ''
self.params = {}
# Compare request and server HTTP protocol versions, in case our
# server does not support the requested protocol. Limit our output
# to min(req, server). We want the following output:
# request server actual written supported response
# protocol protocol response protocol feature set
# a 1.0 1.0 1.0 1.0
# b 1.0 1.1 1.1 1.0
# c 1.1 1.0 1.0 1.0
# d 1.1 1.1 1.1 1.1
# Notice that, in (b), the response will be "HTTP/1.1" even though
# the client only understands 1.0. RFC 2616 10.5.6 says we should
# only return 505 if the _major_ version is different.
rp = int(req_protocol[5]), int(req_protocol[7])
sp = int(self.server_protocol[5]), int(self.server_protocol[7])
self.protocol = min(rp, sp)
response.headers.protocol = self.protocol
# Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
url = path
if query_string:
url += '?' + query_string
self.request_line = '%s %s %s' % (method, url, req_protocol)
self.header_list = list(headers)
self.headers = httputil.HeaderMap()
self.rfile = rfile
self.body = None
self.cookie = SimpleCookie()
self.handler = None
# path_info should be the path from the
# app root (script_name) to the handler.
self.script_name = self.app.script_name
self.path_info = pi = path[len(self.script_name):]
self.stage = 'respond'
self.respond(pi)
except self.throws:
raise
except:
if self.throw_errors:
raise
else:
# Failure in setup, error handler or finalize. Bypass them.
# Can't use handle_error because we may not have hooks yet.
cherrypy.log(traceback=True, severity=40)
if self.show_tracebacks:
body = format_exc()
else:
body = ''
r = bare_error(body)
response.output_status, response.header_list, response.body = r
if self.method == 'HEAD':
# HEAD requests MUST NOT return a message-body in the response.
response.body = []
try:
cherrypy.log.access()
except:
cherrypy.log.error(traceback=True)
if response.timed_out:
raise cherrypy.TimeoutError()
return response
# Uncomment for stage debugging
# stage = property(lambda self: self._stage, lambda self, v: print(v))
def respond(self, path_info):
"""Generate a response for the resource at self.path_info. (Core)"""
response = cherrypy.serving.response
try:
try:
try:
if self.app is None:
raise cherrypy.NotFound()
# Get the 'Host' header, so we can HTTPRedirect properly.
self.stage = 'process_headers'
self.process_headers()
# Make a copy of the class hooks
self.hooks = self.__class__.hooks.copy()
self.toolmaps = {}
self.stage = 'get_resource'
self.get_resource(path_info)
self.body = _cpreqbody.RequestBody(
self.rfile, self.headers, request_params=self.params)
self.namespaces(self.config)
self.stage = 'on_start_resource'
self.hooks.run('on_start_resource')
# Parse the querystring
self.stage = 'process_query_string'
self.process_query_string()
# Process the body
if self.process_request_body:
if self.method not in self.methods_with_bodies:
self.process_request_body = False
self.stage = 'before_request_body'
self.hooks.run('before_request_body')
if self.process_request_body:
self.body.process()
# Run the handler
self.stage = 'before_handler'
self.hooks.run('before_handler')
if self.handler:
self.stage = 'handler'
response.body = self.handler()
# Finalize
self.stage = 'before_finalize'
self.hooks.run('before_finalize')
response.finalize()
except (cherrypy.HTTPRedirect, cherrypy.HTTPError):
inst = sys.exc_info()[1]
inst.set_response()
self.stage = 'before_finalize (HTTPError)'
self.hooks.run('before_finalize')
response.finalize()
finally:
self.stage = 'on_end_resource'
self.hooks.run('on_end_resource')
except self.throws:
raise
except:
if self.throw_errors:
raise
self.handle_error()
def process_query_string(self):
"""Parse the query string into Python structures. (Core)"""
try:
p = httputil.parse_query_string(
self.query_string, encoding=self.query_string_encoding)
except UnicodeDecodeError:
raise cherrypy.HTTPError(
404, 'The given query string could not be processed. Query '
'strings for this resource must be encoded with %r.' %
self.query_string_encoding)
# Python 2 only: keyword arguments must be byte strings (type 'str').
if six.PY2:
for key, value in p.items():
if isinstance(key, six.text_type):
del p[key]
p[key.encode(self.query_string_encoding)] = value
self.params.update(p)
def process_headers(self):
"""Parse HTTP header data into Python structures. (Core)"""
# Process the headers into self.headers
headers = self.headers
for name, value in self.header_list:
# Call title() now (and use dict.__method__(headers))
# so title doesn't have to be called twice.
name = name.title()
value = value.strip()
# Warning: if there is more than one header entry for cookies
# (AFAIK, only Konqueror does that), only the last one will
# remain in headers (but they will be correctly stored in
# request.cookie).
if '=?' in value:
dict.__setitem__(headers, name, httputil.decode_TEXT(value))
else:
dict.__setitem__(headers, name, value)
# Handle cookies differently because on Konqueror, multiple
# cookies come on different lines with the same key
if name == 'Cookie':
try:
self.cookie.load(value)
except CookieError:
msg = 'Illegal cookie name %s' % value.split('=')[0]
raise cherrypy.HTTPError(400, msg)
if not dict.__contains__(headers, 'Host'):
# All Internet-based HTTP/1.1 servers MUST respond with a 400
# (Bad Request) status code to any HTTP/1.1 request message
# which lacks a Host header field.
if self.protocol >= (1, 1):
msg = "HTTP/1.1 requires a 'Host' request header."
raise cherrypy.HTTPError(400, msg)
host = dict.get(headers, 'Host')
if not host:
host = self.local.name or self.local.ip
self.base = '%s://%s' % (self.scheme, host)
def get_resource(self, path):
"""Call a dispatcher (which sets self.handler and .config). (Core)"""
# First, see if there is a custom dispatch at this URI. Custom
# dispatchers can only be specified in app.config, not in _cp_config
# (since custom dispatchers may not even have an app.root).
dispatch = self.app.find_config(
path, 'request.dispatch', self.dispatch)
# dispatch() should set self.handler and self.config
dispatch(path)
def handle_error(self):
"""Handle the last unanticipated exception. (Core)"""
try:
self.hooks.run('before_error_response')
if self.error_response:
self.error_response()
self.hooks.run('after_error_response')
cherrypy.serving.response.finalize()
except cherrypy.HTTPRedirect:
inst = sys.exc_info()[1]
inst.set_response()
cherrypy.serving.response.finalize()
# ------------------------- Properties ------------------------- #
def _get_body_params(self):
warnings.warn(
'body_params is deprecated in CherryPy 3.2, will be removed in '
'CherryPy 3.3.',
DeprecationWarning
)
return self.body.params
body_params = property(_get_body_params,
doc="""
If the request Content-Type is 'application/x-www-form-urlencoded' or
multipart, this will be a dict of the params pulled from the entity
body; that is, it will be the portion of request.params that come
from the message body (sometimes called "POST params", although they
can be sent with various HTTP method verbs). This value is set between
the 'before_request_body' and 'before_handler' hooks (assuming that
process_request_body is True).
Deprecated in 3.2, will be removed for 3.3 in favor of
:attr:`request.body.params<cherrypy._cprequest.RequestBody.params>`.""")
class ResponseBody(object):
"""The body of the HTTP response (the response entity)."""
if six.PY3:
unicode_err = ('Page handlers MUST return bytes. Use tools.encode '
'if you wish to return unicode.')
def __get__(self, obj, objclass=None):
if obj is None:
# When calling on the class instead of an instance...
return self
else:
return obj._body
def __set__(self, obj, value):
# Convert the given value to an iterable object.
if six.PY3 and isinstance(value, str):
raise ValueError(self.unicode_err)
if isinstance(value, 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 value:
value = [value]
else:
# [''] doesn't evaluate to False, so replace it with [].
value = []
elif six.PY3 and isinstance(value, list):
# every item in a list must be bytes...
for i, item in enumerate(value):
if isinstance(item, str):
raise ValueError(self.unicode_err)
# Don't use isinstance here; io.IOBase which has an ABC takes
# 1000 times as long as, say, isinstance(value, str)
elif hasattr(value, 'read'):
value = file_generator(value)
elif value is None:
value = []
obj._body = value
class Response(object):
"""An HTTP Response, including status, headers, and body."""
status = ''
"""The HTTP Status-Code and Reason-Phrase."""
header_list = []
"""
A list of the HTTP response headers as (name, value) tuples.
In general, you should use response.headers (a dict) instead. This
attribute is generated from response.headers and is not valid until
after the finalize phase."""
headers = httputil.HeaderMap()
"""
A dict-like object containing the response headers. Keys are header
names (in Title-Case format); however, you may get and set them in
a case-insensitive manner. That is, headers['Content-Type'] and
headers['content-type'] refer to the same value. Values are header
values (decoded according to :rfc:`2047` if necessary).
.. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
"""
cookie = SimpleCookie()
"""See help(Cookie)."""
body = ResponseBody()
"""The body (entity) of the HTTP response."""
time = None
"""The value of time.time() when created. Use in HTTP dates."""
timeout = 300
"""Seconds after which the response will be aborted."""
timed_out = False
"""
Flag to indicate the response should be aborted, because it has
exceeded its timeout."""
stream = False
"""If False, buffer the response body."""
def __init__(self):
self.status = None
self.header_list = None
self._body = []
self.time = time.time()
self.headers = httputil.HeaderMap()
# Since we know all our keys are titled strings, we can
# bypass HeaderMap.update and get a big speed boost.
dict.update(self.headers, {
'Content-Type': 'text/html',
'Server': 'CherryPy/' + cherrypy.__version__,
'Date': httputil.HTTPDate(self.time),
})
self.cookie = SimpleCookie()
def collapse_body(self):
"""Collapse self.body to a single string; replace it and return it."""
if isinstance(self.body, text_or_bytes):
return self.body
newbody = []
for chunk in self.body:
if six.PY3 and not isinstance(chunk, bytes):
raise TypeError("Chunk %s is not of type 'bytes'." %
repr(chunk))
newbody.append(chunk)
newbody = ntob('').join(newbody)
self.body = newbody
return newbody
def finalize(self):
"""Transform headers (and cookies) into self.header_list. (Core)"""
try:
code, reason, _ = httputil.valid_status(self.status)
except ValueError:
raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
headers = self.headers
self.status = '%s %s' % (code, reason)
self.output_status = ntob(str(code), 'ascii') + \
ntob(' ') + headers.encode(reason)
if self.stream:
# The upshot: wsgiserver will chunk the response if
# you pop Content-Length (or set it explicitly to None).
# Note that lib.static sets C-L to the file's st_size.
if dict.get(headers, 'Content-Length') is None:
dict.pop(headers, 'Content-Length', None)
elif code < 200 or code in (204, 205, 304):
# "All 1xx (informational), 204 (no content),
# and 304 (not modified) responses MUST NOT
# include a message-body."
dict.pop(headers, 'Content-Length', None)
self.body = ntob('')
else:
# Responses which are not streamed should have a Content-Length,
# but allow user code to set Content-Length if desired.
if dict.get(headers, 'Content-Length') is None:
content = self.collapse_body()
dict.__setitem__(headers, 'Content-Length', len(content))
# Transform our header dict into a list of tuples.
self.header_list = h = headers.output()
cookie = self.cookie.output()
if cookie:
for line in cookie.split('\r\n'):
name, value = line.split(': ', 1)
if isinstance(name, six.text_type):
name = name.encode('ISO-8859-1')
if isinstance(value, six.text_type):
value = headers.encode(value)
h.append((name, value))
def check_timeout(self):
"""If now > self.time + self.timeout, set self.timed_out.
This purposefully sets a flag, rather than raising an error,
so that a monitor thread can interrupt the Response thread.
"""
if time.time() > self.time + self.timeout:
self.timed_out = True

226
cherrypy/_cpserver.py Normal file
View File

@@ -0,0 +1,226 @@
"""Manage HTTP servers with CherryPy."""
import six
import cherrypy
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.
from cherrypy.process.servers import *
class Server(ServerAdapter):
"""An adapter for an HTTP server.
You can set attributes (like socket_host and socket_port)
on *this* object (which is probably cherrypy.server), and call
quickstart. For example::
cherrypy.server.socket_port = 80
cherrypy.quickstart()
"""
socket_port = 8080
"""The TCP port on which to listen for connections."""
_socket_host = '127.0.0.1'
def _get_socket_host(self):
return self._socket_host
def _set_socket_host(self, value):
if value == '':
raise ValueError("The empty string ('') is not an allowed value. "
"Use '0.0.0.0' instead to listen on all active "
'interfaces (INADDR_ANY).')
self._socket_host = value
socket_host = property(
_get_socket_host,
_set_socket_host,
doc="""The hostname or IP address on which to listen for connections.
Host values may be any IPv4 or IPv6 address, or any valid hostname.
The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
your hosts file prefers IPv6). The string '0.0.0.0' is a special
IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
is the similar IN6ADDR_ANY for IPv6. The empty string or None are
not allowed.""")
socket_file = None
"""If given, the name of the UNIX socket to use instead of TCP/IP.
When this option is not None, the `socket_host` and `socket_port` options
are ignored."""
socket_queue_size = 5
"""The 'backlog' argument to socket.listen(); specifies the maximum number
of queued connections (default 5)."""
socket_timeout = 10
"""The timeout in seconds for accepted connections (default 10)."""
accepted_queue_size = -1
"""The maximum number of requests which will be queued up before
the server refuses to accept it (default -1, meaning no limit)."""
accepted_queue_timeout = 10
"""The timeout in seconds for attempting to add a request to the
queue when the queue is full (default 10)."""
shutdown_timeout = 5
"""The time to wait for HTTP worker threads to clean up."""
protocol_version = 'HTTP/1.1'
"""The version string to write in the Status-Line of all HTTP responses,
for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
this should also limit the supported features used in the response."""
thread_pool = 10
"""The number of worker threads to start up in the pool."""
thread_pool_max = -1
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit.
"""
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 httpserver_from_self(self, httpserver=None):
"""Return a (httpserver, bind_addr) pair based on self attributes."""
if httpserver is None:
httpserver = self.instance
if httpserver is None:
from cherrypy import _cpwsgi_server
httpserver = _cpwsgi_server.CPWSGIServer(self)
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, text_or_bytes):
self.socket_file = value
self.socket_host = None
self.socket_port = None
else:
try:
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,
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.
"""
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.
# Look up the host name, which should be the
# 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)

538
cherrypy/_cptools.py Normal file
View File

@@ -0,0 +1,538 @@
"""CherryPy tools. A "tool" is any helper, adapted to CP.
Tools are usually designed to be used in a variety of ways (although some
may only offer one if they choose):
Library calls
All tools are callables that can be used wherever needed.
The arguments are straightforward and should be detailed within the
docstring.
Function decorators
All tools, when called, may be used as decorators which configure
individual CherryPy page handlers (methods on the CherryPy tree).
That is, "@tools.anytool()" should "turn on" the tool via the
decorated function's _cp_config attribute.
CherryPy config
If a tool exposes a "_setup" callable, it will be called
once per Request (if the feature is "turned on" via config).
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 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'
def __init__(self, point, callable, name=None, priority=50):
self._point = point
self.callable = callable
self._name = name
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__'):
for arg in _getargs(self.callable.__call__):
setattr(self, arg, None)
# IronPython 1.0 raises NotImplementedError because
# inspect.getargspec tries to access Python bytecode
# in co_code attribute.
except NotImplementedError:
pass
# IronPython 1B1 may raise IndexError in some cases,
# but if we trap it here it doesn't prevent CP from working.
except IndexError:
pass
def _merged_args(self, d=None):
"""Return a dict of configuration entries for this Tool."""
if d:
conf = d.copy()
else:
conf = {}
tm = cherrypy.serving.request.toolmaps[self.namespace]
if self._name in tm:
conf.update(tm[self._name])
if 'on' in conf:
del conf['on']
return conf
def __call__(self, *args, **kwargs):
"""Compile-time decorator (turn on the tool in config).
For example::
@expose
@tools.proxy()
def whats_my_base(self):
return cherrypy.request.base
"""
if args:
raise TypeError('The %r Tool does not accept positional '
'arguments; you must use keyword arguments.'
% self._name)
def tool_decorator(f):
if not hasattr(f, '_cp_config'):
f._cp_config = {}
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)
if p is None:
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
to tell CherryPy to continue on and call the normal page handler. If the
tool is declared AS a page handler (see the 'handler' method), returning
False will raise NotFound.
"""
def __init__(self, callable, name=None):
Tool.__init__(self, 'before_handler', callable, name)
def handler(self, *args, **kwargs):
"""Use this tool as a CherryPy page handler.
For example::
class Root:
nav = tools.staticdir.handler(section="/nav", dir="nav",
root=absDir)
"""
@expose
def handle_func(*a, **kw):
handled = self.callable(*args, **self._merged_args(kwargs))
if not handled:
raise cherrypy.NotFound()
return cherrypy.serving.response.body
return handle_func
def _wrapper(self, **kwargs):
if self.callable(**kwargs):
cherrypy.serving.request.handler = None
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
conf = self._merged_args()
p = conf.pop('priority', None)
if p is None:
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
functions, it must return an iterable for use as cherrypy.response.body.
For example, to allow your 'inner' page handlers to return dicts
which then get interpolated into a template::
def interpolator(next_handler, *args, **kwargs):
filename = cherrypy.request.config.get('template')
cherrypy.response.template = env.get_template(filename)
response_dict = next_handler(*args, **kwargs)
return cherrypy.response.template.render(**response_dict)
cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
"""
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, *args, **kwargs):
innerfunc = cherrypy.serving.request.handler
def wrap(*args, **kwargs):
return self.newhandler(innerfunc, *args, **kwargs)
cherrypy.serving.request.handler = wrap
class ErrorTool(Tool):
"""Tool which is used to replace the default request.error_response."""
def __init__(self, callable, name=None):
Tool.__init__(self, None, callable, name)
def _wrapper(self):
self.callable(**self._merged_args())
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
cherrypy.serving.request.error_response = self._wrapper
# Builtin tools #
class SessionTool(Tool):
"""Session Tool for CherryPy.
sessions.locking
When 'implicit' (the default), the session will be locked for you,
just before running the page handler.
When 'early', the session will be locked before reading the request
body. This is off by default for safety reasons; for example,
a large upload would block the session, denying an AJAX
progress meter
(`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.
"""
def __init__(self):
# _sessions.init must be bound after headers are read
Tool.__init__(self, 'before_request_body', _sessions.init)
def _lock_session(self):
cherrypy.serving.session.acquire_lock()
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
hooks = cherrypy.serving.request.hooks
conf = self._merged_args()
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, 'priority', self._priority)
hooks.attach(self._point, self.callable, priority=p, **conf)
locking = conf.pop('locking', 'implicit')
if locking == 'implicit':
hooks.attach('before_handler', self._lock_session)
elif locking == 'early':
# Lock before the request body (but after _sessions.init runs!)
hooks.attach('before_request_body', self._lock_session,
priority=60)
else:
# Don't lock
pass
hooks.attach('before_finalize', _sessions.save)
hooks.attach('on_end_request', _sessions.close)
def regenerate(self):
"""Drop the current session and make a new one (with a new id)."""
sess = cherrypy.serving.session
sess.regenerate()
# Grab cookie-relevant tool args
conf = dict([(k, v) for k, v in self._merged_args().items()
if k in ('path', 'path_header', 'name', 'timeout',
'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::
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
specified in the request body. It also allows a superfluous "/RPC2"
prefix in the URL, supplies its own handler args in the body, and
requires a 200 OK "Fault" response instead of 404 when the desired
method is not found.
Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
This Controller acts as the dispatch target for the first half (based
on the URL); it then reads the RPC method from the request body and
does its own second dispatch step based on that method. It also reads
body params, and returns a Fault on error.
The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
in your URL's, you can safely skip turning on the XMLRPCDispatcher.
Otherwise, you need to use declare it in config::
request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
"""
# Note we're hard-coding this into the 'tools' namespace. We could do
# a huge amount of work to make it relocatable, but the only reason why
# would be if someone actually disabled the default_toolbox. Meh.
_cp_config = {'tools.xmlrpc.on': True}
@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):
body = subhandler(*(vpath + rpcparams), **params)
else:
# 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.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.serving.response.body
class SessionAuthTool(HandlerTool):
def _setargs(self):
for name in dir(cptools.SessionAuth):
if not name.startswith('__'):
setattr(self, name, None)
class CachingTool(Tool):
"""Caching Tool for CherryPy."""
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)
_wrapper.priority = 20
def _setup(self):
"""Hook caching into cherrypy.request."""
conf = self._merged_args()
p = conf.pop('priority', None)
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
priority=p, **conf)
class Toolbox(object):
"""A collection of Tools.
This object also functions as a config namespace handler for itself.
Custom toolboxes should be added to each Application's toolboxes dict.
"""
def __init__(self, namespace):
self.namespace = namespace
def __setattr__(self, name, value):
# If the Tool._name is None, supply it from the attribute name.
if isinstance(value, Tool):
if value._name is None:
value._name = name
value.namespace = self.namespace
object.__setattr__(self, name, value)
def __enter__(self):
"""Populate request.toolmaps from tools specified in config."""
cherrypy.serving.request.toolmaps[self.namespace] = map = {}
def populate(k, v):
toolname, arg = k.split('.', 1)
bucket = map.setdefault(toolname, {})
bucket[arg] = v
return populate
def __exit__(self, exc_type, exc_val, exc_tb):
"""Run tool._setup() for each tool in our toolmap."""
map = cherrypy.serving.request.toolmaps.get(self.namespace)
if map:
for name, settings in map.items():
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
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)
_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_request_body', encoding.decode)
# the order of encoding, gzip, caching is important
_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.caching = CachingTool('before_handler', _caching.get, 'caching')
_d.expires = Tool('before_finalize', _caching.expires)
_d.tidy = DeprecatedTool(
'before_finalize',
'The tidy tool has been removed from the standard distribution of '
'CherryPy. The most recent version can be found at '
'http://tools.cherrypy.org/browser.')
_d.nsgmls = DeprecatedTool(
'before_finalize',
'The nsgmls tool has been removed from the standard distribution of '
'CherryPy. The most recent version can be found at '
'http://tools.cherrypy.org/browser.')
_d.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)
_d.digest_auth = Tool('on_start_resource', auth.digest_auth)
_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

287
cherrypy/_cptree.py Normal file
View File

@@ -0,0 +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 httputil
class Application(object):
"""A CherryPy Application.
Servers and gateways should not instantiate Request objects directly.
Instead, they should ask an Application object for a request object.
An instance of this class may also be used as a WSGI callable
(WSGI application object) for itself.
"""
root = None
"""The top-most container of page handlers for this app. Handlers should
be arranged in a hierarchy of attributes, matching the expected URI
hierarchy; the default dispatcher then searches this hierarchy for a
matching handler. When using a dispatcher other than the default,
this value may be None."""
config = {}
"""A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
of {key: value} pairs."""
namespaces = _cpconfig.NamespaceSet()
toolboxes = {'tools': cherrypy.tools}
log = None
"""A LogManager instance. See _cplogging."""
wsgiapp = None
"""A CPWSGIApp instance. See _cpwsgi."""
request_class = _cprequest.Request
response_class = _cprequest.Response
relative_urls = False
def __init__(self, root, script_name='', config=None):
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
self.root = root
self.script_name = script_name
self.wsgiapp = _cpwsgi.CPWSGIApp(self)
self.namespaces = self.namespaces.copy()
self.namespaces['log'] = lambda k, v: setattr(self.log, k, v)
self.namespaces['wsgi'] = self.wsgiapp.namespace_handler
self.config = self.__class__.config.copy()
if config:
self.merge(config)
def __repr__(self):
return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__,
self.root, self.script_name)
script_name_doc = """The URI "mount point" for this app. A mount point
is that portion of the URI which is constant for all URIs that are
serviced by this application; it does not include scheme, host, or proxy
("virtual host") portions of the URI.
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 not None:
return self._script_name
# A `_script_name` with a value of None signals that the script name
# should be pulled from WSGI environ.
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/')
def _set_script_name(self, value):
if value:
value = value.rstrip('/')
self._script_name = value
script_name = property(fget=_get_script_name, fset=_set_script_name,
doc=script_name_doc)
def merge(self, config):
"""Merge the given config into self.config."""
_cpconfig.merge(self.config, config)
# Handle namespaces specified in config.
self.namespaces(self.config.get('/', {}))
def find_config(self, path, key, default=None):
"""Return the most-specific value for key along path, or default."""
trail = path or '/'
while trail:
nodeconf = self.config.get(trail, {})
if key in nodeconf:
return nodeconf[key]
lastslash = trail.rfind('/')
if lastslash == -1:
break
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.items():
req.namespaces[name] = toolbox
resp = self.response_class()
cherrypy.serving.load(req, resp)
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.publish('after_request')
try:
req.close()
except:
cherrypy.log(traceback=True, severity=40)
cherrypy.serving.clear()
def __call__(self, environ, start_response):
return self.wsgiapp(environ, start_response)
class Tree(object):
"""A registry of CherryPy applications, mounted at diverse points.
An instance of this class may also be used as a WSGI callable
(WSGI application object), in which case it dispatches to all
mounted apps.
"""
apps = {}
"""
A dict of the form {script name: application}, where "script name"
is a string declaring the URI mount point (no trailing slash), and
"application" is an instance of cherrypy.Application (or an arbitrary
WSGI callable if you happen to be using a WSGI server)."""
def __init__(self):
self.apps = {}
def mount(self, root, script_name='', config=None):
"""Mount a new app from a root object, script_name, and config.
root
An instance of a "controller class" (a collection of page
handler methods) which represents the root of the application.
This may also be an Application instance, or None if using
a dispatcher other than the default.
script_name
A string containing the "mount point" of the application.
This should start with a slash, and be the path portion of the
URL at which to mount the given root. For example, if root.index()
will handle requests to "http://www.example.com:8080/dept/app1/",
then the script_name argument would be "/dept/app1".
It MUST NOT end in a slash. If the script_name refers to the
root of the URI, it MUST be an empty string (not "/").
config
A file or dict containing application config.
"""
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.')
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip('/')
if isinstance(root, Application):
app = root
if script_name != '' and script_name != app.script_name:
raise ValueError(
'Cannot specify a different script name and pass an '
'Application instance to cherrypy.mount')
script_name = app.script_name
else:
app = Application(root, script_name)
# If mounted at "", add favicon.ico
if (script_name == '' and root is not None
and not hasattr(root, 'favicon_ico')):
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
'favicon.ico')
root.favicon_ico = tools.staticfile.handler(favicon)
if config:
app.merge(config)
self.apps[script_name] = app
return app
def graft(self, wsgi_callable, script_name=''):
"""Mount a wsgi callable at the given script_name."""
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip('/')
self.apps[script_name] = wsgi_callable
def script_name(self, path=None):
"""The script_name of the app at the given path, or None.
If path is None, cherrypy.request is used.
"""
if path is None:
try:
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 == '':
return None
# Move one node up the tree and try again.
path = path[:path.rfind('/')]
def __call__(self, environ, start_response):
# If you're calling this, then you're probably setting SCRIPT_NAME
# to '' (some WSGI servers always set SCRIPT_NAME to '').
# Try to look up the app using the full path.
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()
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)

466
cherrypy/_cpwsgi.py Normal file
View File

@@ -0,0 +1,466 @@
"""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 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 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::
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,
},
)
cherrypy.tree.graft(vhost)
"""
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)
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):
"""WSGI response iterable for CherryPy applications."""
def __init__(self, environ, start_response, cpapp):
self.cpapp = cpapp
try:
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
def __iter__(self):
return self
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()
# 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 = 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')
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.
request.login = env('LOGON_USER') or env('REMOTE_USER') or None
request.multithread = self.environ['wsgi.multithread']
request.multiprocess = self.environ['wsgi.multiprocess']
request.wsgi_environ = self.environ
request.prev = env('cherrypy.previous_request', None)
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_':
# Hackish attempt at recovering original header names.
translatedHeader = cgiName[5:].replace('_', '-')
yield translatedHeader, environ[cgiName]
class CPWSGIApp(object):
"""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:
# Create and nest the WSGI apps in our pipeline (in reverse order).
# Then memoize the result in self.head.
head = self.tail
for name, callable in self.pipeline[::-1]:
conf = self.config.get(name, {})
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':
# 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':
self.response_class = v
else:
name, arg = k.split('.', 1)
bucket = self.config.setdefault(name, {})
bucket[arg] = v

View File

@@ -0,0 +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 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.
"""
def __init__(self, server_adapter=cherrypy.server):
self.server_adapter = server_adapter
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,
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
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

6
cherrypy/cherryd Executable file
View File

@@ -0,0 +1,6 @@
#! /usr/bin/env python
import cherrypy.daemon
if __name__ == '__main__':
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)

BIN
cherrypy/favicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

85
cherrypy/lib/__init__.py Normal file
View File

@@ -0,0 +1,85 @@
"""CherryPy Library"""
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 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:
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):
chunk = self.input.read(self.chunkSize)
if chunk:
return chunk
else:
if hasattr(self.input, 'close'):
self.input.close()
raise StopIteration()
next = __next__
def file_generator_limited(fileobj, count, chunk_size=65536):
"""Yield the given file object in chunks, stopping after `count`
bytes has been emitted. Default chunk size is 64kB. (Core)
"""
remaining = count
while remaining > 0:
chunk = fileobj.read(min(chunk_size, remaining))
chunklen = len(chunk)
if chunklen == 0:
return
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)

97
cherrypy/lib/auth.py Normal file
View File

@@ -0,0 +1,97 @@
import cherrypy
from cherrypy.lib import httpauth
def check_auth(users, encrypt=None, realm=None):
"""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(request.headers['authorization'])
if ah is None:
raise cherrypy.HTTPError(400, 'Bad Request')
if not encrypt:
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
if hasattr(users, '__call__'):
try:
# backward compatibility
users = users() # expect it to return a dictionary
if not isinstance(users, dict):
raise ValueError(
'Authentication users must be a dictionary')
# fetch the user password
password = users.get(ah['username'], None)
except TypeError:
# returns a password (encrypted or clear text)
password = users(ah['username'])
else:
if not isinstance(users, dict):
raise ValueError('Authentication users must be a dictionary')
# fetch the user password
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=request.method,
encrypt=encrypt, realm=realm):
request.login = ah['username']
return True
request.login = False
return False
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.
"""
if check_auth(users, encrypt):
if debug:
cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH')
return
# 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.
"""
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.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')

470
cherrypy/lib/caching.py Normal file
View File

@@ -0,0 +1,470 @@
"""
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, httputil
from cherrypy._cpcompat import copyitems, ntob, sorted, Event
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."""
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
self.tot_hist = 0
self.tot_expires = 0
self.tot_non_modified = 0
self.cursize = 0
def expire_cache(self):
"""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()
# 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, uri, sel_header_values in objects:
try:
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(self.expire_freq)
def get(self):
"""Return the current variant if in the cache, else None."""
request = cherrypy.serving.request
self.tot_gets += 1
uri = cherrypy.url(qs=request.query_string)
uricache = self.store.get(uri)
if uricache is None:
return None
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 (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((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):
"""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'), 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.
* 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.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 = 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
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(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 = 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
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
# 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, 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
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.serving.response
headers = response.headers
cacheable = False
if not force:
# some header names that indicate that the response can be cached
for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
if indicator in headers:
cacheable = True
break
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.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 = httputil.HTTPDate(1169942400.0)
else:
expiry = httputil.HTTPDate(response.time + secs)
if force or 'Expires' not in headers:
headers['Expires'] = expiry

391
cherrypy/lib/covercp.py Normal file
View File

@@ -0,0 +1,391 @@
"""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/>`_
or Ned Batchelder's `enhanced version:
<http://www.nedbatchelder.com/code/modules/coverage.html>`_
To turn on coverage tracing, use the following code::
cherrypy.engine.subscribe('start', covercp.start)
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.
"""
import re
import sys
import cgi
import os
import os.path
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 coverage
the_coverage = coverage(data_file=localFile)
def start():
the_coverage.start()
except ImportError:
# Setting the_coverage to None will raise errors
# that need to be trapped downstream.
the_coverage = None
import warnings
warnings.warn(
'No code coverage will be performed; '
'coverage.py could not be imported.')
def start():
pass
start.priority = 20
TEMPLATE_MENU = """<html>
<head>
<title>CherryPy Coverage Menu</title>
<style>
body {font: 9pt Arial, serif;}
#tree {
font-size: 8pt;
font-family: Andale Mono, monospace;
white-space: pre;
}
#tree a:active, a:focus {
background-color: black;
padding: 1px;
color: white;
border: 0px solid #9999FF;
-moz-outline-style: none;
}
.fail { color: red;}
.pass { color: #888;}
#pct { text-align: right;}
h3 {
font-size: small;
font-weight: bold;
font-style: italic;
margin-top: 5px;
}
input { border: 1px solid #ccc; padding: 2px; }
.directory {
color: #933;
font-style: italic;
font-weight: bold;
font-size: 10pt;
}
.file {
color: #400;
}
a { text-decoration: none; }
#crumbs {
color: white;
font-size: 8pt;
font-family: Andale Mono, monospace;
width: 100%;
background-color: black;
}
#crumbs a {
color: #f88;
}
#options {
line-height: 2.3em;
border: 1px solid black;
background-color: #eee;
padding: 4px;
}
#exclude {
width: 100%;
margin-bottom: 3px;
border: 1px solid #999;
}
#submit {
background-color: black;
color: white;
border: 0;
margin-bottom: -9px;
}
</style>
</head>
<body>
<h2>CherryPy Coverage</h2>"""
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 />
Exclude files matching<br />
<input type='text' id='exclude' name='exclude'
value='%(exclude)s' size='20' />
<br />
<input type='submit' value='Change view' id="submit"/>
</form>
</div>"""
TEMPLATE_FRAMESET = """<html>
<head><title>CherryPy coverage data</title></head>
<frameset cols='250, 1*'>
<frame src='menu?base=%s' />
<frame name='main' src='' />
</frameset>
</html>
"""
TEMPLATE_COVERAGE = """<html>
<head>
<title>Coverage for %(name)s</title>
<style>
h2 { margin-bottom: .25em; }
p { margin: .25em; }
.covered { color: #000; background-color: #fff; }
.notcovered { color: #fee; background-color: #500; }
.excluded { color: #00f; background-color: #fff; }
table .covered, table .notcovered, table .excluded
{ font-family: Andale Mono, monospace;
font-size: 10pt; white-space: pre; }
.lineno { background-color: #eee;}
.notcovered .lineno { background-color: #000;}
table { border-collapse: collapse;
</style>
</head>
<body>
<h2>%(name)s</h2>
<p>%(fullpath)s</p>
<p>Coverage: %(pc)s%%</p>"""
TEMPLATE_LOC_COVERED = """<tr class="covered">
<td class="lineno">%s&nbsp;</td>
<td>%s</td>
</tr>\n"""
TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
<td class="lineno">%s&nbsp;</td>
<td>%s</td>
</tr>\n"""
TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
<td class="lineno">%s&nbsp;</td>
<td>%s</td>
</tr>\n"""
TEMPLATE_ITEM = (
"%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
)
def _percent(statements, missing):
s = len(statements)
e = s - len(missing)
if s > 0:
return int(round(100.0 * e / s))
return 0
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.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, 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.items() if not v]
files.sort()
for name in files:
newpath = os.path.join(path, name)
pc_str = ''
if showpct:
try:
_, statements, _, missing, _ = coverage.analysis2(newpath)
except:
# Yes, we really want to pass on all errors.
pass
else:
pc = _percent(statements, missing)
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),
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:
p, tail = os.path.split(p)
if not tail:
break
atoms.append(tail)
atoms.append(p)
if p != '/':
atoms.append('/')
atoms.reverse()
for node in atoms:
if node:
d = d.setdefault(node, {})
def get_tree(base, exclude, coverage=the_coverage):
"""Return covered module names as a nested dict."""
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 % 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 = ''
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, quote_plus(exclude), atom, os.sep))
yield '</div>'
yield "<div id='tree'>"
# Then display the tree
tree = get_tree(base, exclude, self.coverage)
if not tree:
yield '<p>No modules covered.</p>'
else:
for chunk in _show_branch(tree, base, '/', pct,
showpct == 'checked', exclude,
coverage=self.coverage):
yield chunk
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')
empty_the_buffer = True
if lineno in excluded:
template = TEMPLATE_LOC_EXCLUDED
elif lineno in missing:
template = TEMPLATE_LOC_NOT_COVERED
elif lineno in statements:
template = TEMPLATE_LOC_COVERED
else:
empty_the_buffer = False
buffer.append((lineno, line))
if empty_the_buffer:
for lno, pastline in buffer:
yield template % (lno, cgi.escape(pastline))
buffer = []
yield template % (lineno, cgi.escape(line))
@cherrypy.expose
def report(self, name):
filename, statements, excluded, missing, _ = self.coverage.analysis2(
name)
pc = _percent(statements, missing)
yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
fullpath=name,
pc=pc)
yield '<table>\n'
for line in self.annotated_file(filename, statements, excluded,
missing):
yield line
yield '</table>'
yield '</body>'
yield '</html>'
def serve(path=localFile, port=8080, root=None):
if coverage is None:
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': int(port),
'server.thread_pool': 10,
'environment': 'production',
})
cherrypy.quickstart(CoverStats(cov, root))
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']}

648
cherrypy/lib/cptools.py Normal file
View File

@@ -0,0 +1,648 @@
"""Functions for builtin CherryPy tools."""
import logging
import re
from hashlib import md5
import six
import cherrypy
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, 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 :rfc:`2616` Section 14.24.
"""
response = cherrypy.serving.response
# Guard against being run twice.
if hasattr(response, 'ETag'):
return
status, reason, msg = _httputil.valid_status(response.status)
etag = response.headers.get('ETag')
# Automatic ETag generation. See warning in docstring.
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.serving.request
conditions = request.headers.elements('If-Match') or []
conditions = [str(x) for x in 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 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))
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.serving.response
lastmod = response.headers.get('Last-Modified')
if lastmod:
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'):
raise cherrypy.HTTPRedirect([], 304)
else:
raise cherrypy.HTTPError(412)
# 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', 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.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'
else:
# This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
scheme = s
if not scheme:
scheme = request.base[:request.base.find('://')]
if local:
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:
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
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':
# Bug #1268
xff = xff.split(',')[0].strip()
request.remote.ip = xff
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.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, 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.serving.response.headers[name] = value
response_headers.failsafe = True
def referer(pattern, accept=True, accept_missing=False, error=403,
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,
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.
"""
try:
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'
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 (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 />
<input type="submit" />
</form>
</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)
response.body = body
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers['Content-Length']
return True
else:
cherrypy.serving.request.login = username
cherrypy.session[self.session_key] = username
self.on_login(username)
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.serving.request.login = None
self.on_logout(username)
raise cherrypy.HTTPRedirect(from_page)
def do_check(self):
"""Assert username. Raise redirect, or return True if request handled.
"""
sess = cherrypy.session
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:
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 response.headers['Content-Length']
return True
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.serving.request
response = cherrypy.serving.response
path = request.path_info
if path.endswith('login_screen'):
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.items():
setattr(sa, k, v)
return sa.run()
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('__')])
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)
def log_request_headers(debug=False):
"""Write request headers to the cherrypy error log."""
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(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 request.hooks.keys():
if k not in points:
points.append(k)
for k in points:
msg.append(' %s:' % k)
v = request.hooks.get(k, [])
v.sort()
for h in v:
msg.append(' %r' % h)
cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
':\n' + '\n'.join(msg), 'HTTP')
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, status=None, debug=False):
"""Redirect if path_info has (missing|extra) trailing slash."""
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, 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, status=status or 301)
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.
"""
def flattener(input):
numchunks = 0
for x in input:
if not is_iterator(x):
numchunks += 1
yield x
else:
for y in flattener(x):
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, 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 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, 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 = 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 == '*/*':
# Matches any type or subtype
if debug:
cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
return media[0]
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 = request.headers.get('Accept')
if ah is None:
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))
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])

424
cherrypy/lib/encoding.py Normal file
View File

@@ -0,0 +1,424 @@
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'):
"""Replace or extend the list of charsets used to decode a request entity.
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).
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).
"""
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
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 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
# 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,
zlib.DEF_MEM_LEVEL, 0)
for line in body:
size += len(line)
crc = zlib.crc32(line, crc)
yield zobj.compress(line)
yield zobj.flush()
# 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
zbuf = io.BytesIO()
zbuf.write(body)
zbuf.seek(0)
zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
data = zfile.read()
zfile.close()
return data
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.
"""
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(request, 'cached', False):
if debug:
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
return
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
# content coding. In this case, if "identity" is one of
# the available content-codings, then the server SHOULD use
# 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 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
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)

378
cherrypy/lib/httpauth.py Normal file
View File

@@ -0,0 +1,378 @@
"""
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.
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>'
__credits__ = """
Peter van Kampen for its recipe which implement most of Digest
authentication:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
"""
__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:
* 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 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
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
__all__ = ('digestAuth', 'basicAuth', 'doAuth', 'checkResponse',
'parseAuthorization', 'SUPPORTED_ALGORITHM', 'md5SessionKey',
'calculateNonce', 'SUPPORTED_QOP')
##########################################################################
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(ntob(val)).hexdigest(),
MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
# SHA: lambda val: sha.new(ntob(val)).hexdigest (),
}
def calculateNonce(realm, algorithm=MD5):
"""This is an auxaliary function that calculates 'nonce' value. It is used
to handle sessions."""
global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS
assert algorithm in SUPPORTED_ALGORITHM
try:
encoder = DIGEST_AUTH_ENCODERS[algorithm]
except KeyError:
raise NotImplementedError('The chosen algorithm (%s) does not have '
'an implementation yet' % algorithm)
return encoder('%d:%s' % (time.time(), realm))
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)
return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
realm, nonce, algorithm, qop
)
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):
"""'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)
##########################################################################
# Parse authorization parameters
#
def _parseDigestAuthorization(auth_params):
# Convert the auth params to a dict
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']
for k in required:
if k not in params:
return None
# If qop is sent then cnonce and nc MUST be present
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 ('cnonce' in params or 'nc' in params) and \
'qop' not in params:
return None
return params
def _parseBasicAuthorization(auth_params):
username, password = base64_decode(auth_params).split(':', 1)
return {'username': username, 'password': password}
AUTH_SCHEMES = {
'basic': _parseBasicAuthorization,
'digest': _parseDigestAuthorization,
}
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()
parser = AUTH_SCHEMES[auth_scheme]
params = parser(auth_params)
if params is None:
return
assert 'auth_scheme' not in params
params['auth_scheme'] = auth_scheme
return params
##########################################################################
# Check provided response for a valid password
#
def md5SessionKey(params, password):
"""
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.
This creates a 'session key' for the authentication of subsequent
requests and responses which is different for each "authentication
session", thus limiting the amount of material hashed with any one
key.
Because the server need only use the hash of the user
credentials in order to create the A1 value, this construction could
be used in conjunction with a third party authentication service so
that the web server would not need the actual password value. The
specification of such a protocol is beyond the scope of this
specification.
"""
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)
def _A1(params, password):
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)
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'])
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':
# 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']
return '%s:%s:%s' % (
method,
params['uri'],
H(entity_body)
)
else:
raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
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)
H = DIGEST_AUTH_ENCODERS[algorithm]
KD = lambda secret, data: H(secret + ':' + data)
qop = params.get('qop', None)
H_A2 = H(_A2(params, method, kwargs))
if algorithm == MD5_SESS and A1 is not None:
H_A1 = H(A1)
else:
H_A1 = H(_A1(params, password))
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
# ":" unq(cnonce-value)
# ":" unq(qop-value)
# ":" H(A2)
# ) <">
request = '%s:%s:%s:%s:%s' % (
params['nonce'],
params['nc'],
params['cnonce'],
params['qop'],
H_A2,
)
elif qop is None:
# If the "qop" directive is not present (this construction is
# for compatibility with RFC 2069):
# request-digest =
# <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
request = '%s:%s' % (params['nonce'], H_A2)
return KD(H_A1, request)
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:
entity_body - when 'qop' is set to 'auth-int' you MUST provide the
raw data you are going to send to the client (usually the
HTML page.
request_uri - the uri from the request line compared with the 'uri'
directive of the authorization map. They must represent
the same resource (unused at this time).
"""
if auth_map['realm'] != kwargs.get('realm', None):
return False
response = _computeDigestResponse(
auth_map, password, method, A1, **kwargs)
return response == auth_map['response']
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:
candidate = encrypt(auth_map['password'], auth_map['username'])
except TypeError:
# if encrypt only takes one parameter, it's the password
candidate = encrypt(auth_map['password'])
return candidate == password
AUTH_RESPONSES = {
'basic': _checkBasicResponse,
'digest': _checkDigestResponse,
}
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
If the response is of type 'Digest' then the function has the following
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.
"""
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

217
cherrypy/lib/profiler.py Normal file
View File

@@ -0,0 +1,217 @@
"""Profiler tools for CherryPy.
CherryPy users
==============
You can profile any of your pages as follows::
from cherrypy.lib import profiler
class Root:
p = profiler.Profiler("/path/to/profile/dir")
@cherrypy.expose
def index(self):
self.p.run(self._index)
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.
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()``
function to browse the results in a web browser. If you run this
module from the command line, it will call ``serve()`` for you.
"""
import io
import os
import os.path
import sys
import warnings
import cherrypy
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
_count = 0
class Profiler(object):
def __init__(self, path=None):
if not path:
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)
prof = profile.Profile()
result = prof.runcall(func, *args, **params)
prof.dump_stats(path)
return result
def statfiles(self):
""":rtype: list of available profiles.
"""
return [f for f in os.listdir(self.path)
if f.startswith('cp_') and f.endswith('.prof')]
def stats(self, filename, sortby='cumulative'):
""":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()
s.sort_stats(sortby)
s.print_stats()
else:
# pstats.Stats before Python 2.5 didn't take a 'stream' arg,
# but just printed to stdout. So re-route stdout.
s = pstats.Stats(os.path.join(self.path, filename))
s.strip_dirs()
s.sort_stats(sortby)
oldout = sys.stdout
try:
sys.stdout = sio
s.print_stats()
finally:
sys.stdout = oldout
response = sio.getvalue()
sio.close()
return response
@cherrypy.expose
def index(self):
return """<html>
<head><title>CherryPy profile data</title></head>
<frameset cols='200, 1*'>
<frame src='menu' />
<frame name='main' src='' />
</frameset>
</html>
"""
@cherrypy.expose
def menu(self):
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)
@cherrypy.expose
def report(self, filename):
cherrypy.response.headers['Content-Type'] = 'text/plain'
return self.stats(filename)
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, **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
cherrypy.Application.
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 = []
for line in self.nextapp(environ, start_response):
result.append(line)
return result
return self.profiler.run(gather)
def serve(path=None, port=8080):
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',
})
cherrypy.quickstart(Profiler(path))
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

883
cherrypy/lib/sessions.py Normal file
View File

@@ -0,0 +1,883 @@
"""Session implementation for CherryPy.
You need to edit your config file to use sessions. Here's an example::
[/]
tools.sessions.on = True
tools.sessions.storage_class = cherrypy.lib.sessions.FileSession
tools.sessions.storage_path = "/home/site/sessions"
tools.sessions.timeout = 60
This sets the session to be stored in files in the directory
/home/site/sessions, and the session timeout to 60 minutes. If you omit
``storage_class``, the sessions will be saved in RAM.
``tools.sessions.on`` is the only required line for working sessions,
the rest are optional.
By default, the session ID is passed in a cookie, so the client's browser must
have cookies enabled for your site.
To set data for the current session, use
``cherrypy.session['fieldname'] = 'fieldvalue'``;
to get data use ``cherrypy.session.get('fieldname')``.
================
Locking sessions
================
By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means
the session is locked early and unlocked late. Be mindful of this default mode
for any requests that take a long time to process (streaming responses,
expensive calculations, database lookups, API calls, etc), as other concurrent
requests that also utilize sessions will hang until the session is unlocked.
If you want to control when the session data is locked and unlocked,
set ``tools.sessions.locking = 'explicit'``. Then call
``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``.
Regardless of which mode you use, the session is guaranteed to be unlocked when
the request is complete.
=================
Expiring Sessions
=================
You can force a session to expire with :func:`cherrypy.lib.sessions.expire`.
Simply call that function at the point you want the session to expire, and it
will cause the session cookie to expire client-side.
===========================
Session Fixation Protection
===========================
If CherryPy receives, via a request cookie, a session id that it does not
recognize, it will reject that id and create a new one to return in the
response cookie. This `helps prevent session fixation attacks
<http://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request>`_.
However, CherryPy "recognizes" a session id by looking up the saved session
data for that id. Therefore, if you never save any session data,
**you will get a new session id for every request**.
================
Sharing Sessions
================
If you run multiple instances of CherryPy (for example via mod_python behind
Apache prefork), you most likely cannot use the RAM session backend, since each
instance of CherryPy will have its own memory space. Use a different backend
instead, and verify that all instances are pointing at the same file or db
location. Alternately, you might try a load balancer which makes sessions
"sticky". Google is your friend, there.
================
Expiration Dates
================
The response cookie will possess an expiration date to inform the client at
which point to stop sending the cookie back in requests. If the server time
and client time differ, expect sessions to be unreliable. **Make sure the
system time of your server is accurate**.
CherryPy defaults to a 60-minute session timeout, which also applies to the
cookie which is sent to the client. Unfortunately, some versions of Safari
("4 public beta" on Windows XP at least) appear to have a bug in their parsing
of the GMT expiration date--they appear to interpret the date as one hour in
the past. Sixty minutes minus one hour is pretty close to zero, so you may
experience this bug as a new session id for every request, unless the requests
are less than one second apart. To fix, try increasing the session.timeout.
On the other extreme, some users report Firefox sending cookies after their
expiration date, although this was on a system with an inaccurate system time.
Maybe FF doesn't trust system time.
"""
import sys
import datetime
import os
import time
import threading
import cherrypy
from cherrypy._cpcompat import copyitems, pickle, random20
from cherrypy.lib import httputil
from cherrypy.lib import lockfile
from cherrypy.lib import locking
from cherrypy.lib import is_iterator
missing = object()
class Session(object):
"""A CherryPy dict-like Session object (one per request)."""
_id = None
id_observers = None
"A list of callbacks to which to pass new id's."
def _get_id(self):
return self._id
def _set_id(self, value):
self._id = value
for o in self.id_observers:
o(value)
id = property(_get_id, _set_id, doc='The current session ID.')
timeout = 60
'Number of minutes after which to delete session data.'
locked = False
"""
If True, this session instance has exclusive read/write access
to session data."""
loaded = False
"""
If True, data has been retrieved from storage. This should happen
automatically on the first attempt to access session data."""
clean_thread = None
'Class-level Monitor which calls self.clean_up.'
clean_freq = 5
'The poll rate for expired session cleanup in minutes.'
originalid = None
'The session id passed by the client. May be missing or unsafe.'
missing = False
'True if the session requested by the client did not exist.'
regenerated = False
"""
True if the application called session.regenerate(). This is not set by
internal calls to regenerate the session id."""
debug = False
'If True, log debug information.'
# --------------------- Session management methods --------------------- #
def __init__(self, id=None, **kwargs):
self.id_observers = []
self._data = {}
for k, v in kwargs.items():
setattr(self, k, v)
self.originalid = id
self.missing = False
if id is None:
if self.debug:
cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS')
self._regenerate()
else:
self.id = id
if self._exists():
if self.debug:
cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS')
else:
if self.debug:
cherrypy.log('Expired or malicious session %r; '
'making a new one' % id, 'TOOLS.SESSIONS')
# Expired or malicious session. Make a new one.
# See https://github.com/cherrypy/cherrypy/issues/709.
self.id = None
self.missing = True
self._regenerate()
def now(self):
"""Generate the session specific concept of 'now'.
Other session providers can override this to use alternative,
possibly timezone aware, versions of 'now'.
"""
return datetime.datetime.now()
def regenerate(self):
"""Replace the current session (with a new id)."""
self.regenerated = True
self._regenerate()
def _regenerate(self):
if self.id is not None:
if self.debug:
cherrypy.log(
'Deleting the existing session %r before '
'regeneration.' % self.id,
'TOOLS.SESSIONS')
self.delete()
old_session_was_locked = self.locked
if old_session_was_locked:
self.release_lock()
if self.debug:
cherrypy.log('Old lock released.', 'TOOLS.SESSIONS')
self.id = None
while self.id is None:
self.id = self.generate_id()
# Assert that the generated id is not already stored.
if self._exists():
self.id = None
if self.debug:
cherrypy.log('Set id to generated %s.' % self.id,
'TOOLS.SESSIONS')
if old_session_was_locked:
self.acquire_lock()
if self.debug:
cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS')
def clean_up(self):
"""Clean up expired sessions."""
pass
def generate_id(self):
"""Return a new session id."""
return random20()
def save(self):
"""Save session data."""
try:
# If session data has never been loaded then it's never been
# accessed: no need to save it
if self.loaded:
t = datetime.timedelta(seconds=self.timeout * 60)
expiration_time = self.now() + t
if self.debug:
cherrypy.log('Saving session %r with expiry %s' %
(self.id, expiration_time),
'TOOLS.SESSIONS')
self._save(expiration_time)
else:
if self.debug:
cherrypy.log(
'Skipping save of session %r (no session loaded).' %
self.id, 'TOOLS.SESSIONS')
finally:
if self.locked:
# Always release the lock if the user didn't release it
self.release_lock()
if self.debug:
cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS')
def load(self):
"""Copy stored session data into this session instance."""
data = self._load()
# data is either None or a tuple (session_data, expiration_time)
if data is None or data[1] < self.now():
if self.debug:
cherrypy.log('Expired session %r, flushing data.' % self.id,
'TOOLS.SESSIONS')
self._data = {}
else:
if self.debug:
cherrypy.log('Data loaded for session %r.' % self.id,
'TOOLS.SESSIONS')
self._data = data[0]
self.loaded = True
# Stick the clean_thread in the class, not the instance.
# The instances are created and destroyed per-request.
cls = self.__class__
if self.clean_freq and not cls.clean_thread:
# clean_up is an instancemethod and not a classmethod,
# so that tool config can be accessed inside the method.
t = cherrypy.process.plugins.Monitor(
cherrypy.engine, self.clean_up, self.clean_freq * 60,
name='Session cleanup')
t.subscribe()
cls.clean_thread = t
t.start()
if self.debug:
cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS')
def delete(self):
"""Delete stored session data."""
self._delete()
if self.debug:
cherrypy.log('Deleted session %s.' % self.id,
'TOOLS.SESSIONS')
# -------------------- Application accessor methods -------------------- #
def __getitem__(self, key):
if not self.loaded:
self.load()
return self._data[key]
def __setitem__(self, key, value):
if not self.loaded:
self.load()
self._data[key] = value
def __delitem__(self, key):
if not self.loaded:
self.load()
del self._data[key]
def pop(self, key, default=missing):
"""Remove the specified key and return the corresponding value.
If key is not found, default is returned if given,
otherwise KeyError is raised.
"""
if not self.loaded:
self.load()
if default is missing:
return self._data.pop(key)
else:
return self._data.pop(key, default)
def __contains__(self, key):
if not self.loaded:
self.load()
return key in self._data
if hasattr({}, 'has_key'):
def has_key(self, key):
"""D.has_key(k) -> True if D has a key k, else False."""
if not self.loaded:
self.load()
return key in self._data
def get(self, key, default=None):
"""D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
if not self.loaded:
self.load()
return self._data.get(key, default)
def update(self, d):
"""D.update(E) -> None. Update D from E: for k in E: D[k] = E[k]."""
if not self.loaded:
self.load()
self._data.update(d)
def setdefault(self, key, default=None):
"""D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
if not self.loaded:
self.load()
return self._data.setdefault(key, default)
def clear(self):
"""D.clear() -> None. Remove all items from D."""
if not self.loaded:
self.load()
self._data.clear()
def keys(self):
"""D.keys() -> list of D's keys."""
if not self.loaded:
self.load()
return self._data.keys()
def items(self):
"""D.items() -> list of D's (key, value) pairs, as 2-tuples."""
if not self.loaded:
self.load()
return self._data.items()
def values(self):
"""D.values() -> list of D's values."""
if not self.loaded:
self.load()
return self._data.values()
class RamSession(Session):
# Class-level objects. Don't rebind these!
cache = {}
locks = {}
def clean_up(self):
"""Clean up expired sessions."""
now = self.now()
for _id, (data, expiration_time) in copyitems(self.cache):
if expiration_time <= now:
try:
del self.cache[_id]
except KeyError:
pass
try:
if self.locks[_id].acquire(blocking=False):
lock = self.locks.pop(_id)
lock.release()
except KeyError:
pass
# added to remove obsolete lock objects
for _id in list(self.locks):
if _id not in self.cache and self.locks[_id].acquire(blocking=False):
lock = self.locks.pop(_id)
lock.release()
def _exists(self):
return self.id in self.cache
def _load(self):
return self.cache.get(self.id)
def _save(self, expiration_time):
self.cache[self.id] = (self._data, expiration_time)
def _delete(self):
self.cache.pop(self.id, None)
def acquire_lock(self):
"""Acquire an exclusive lock on the currently-loaded session data."""
self.locked = True
self.locks.setdefault(self.id, threading.RLock()).acquire()
def release_lock(self):
"""Release the lock on the currently-loaded session data."""
self.locks[self.id].release()
self.locked = False
def __len__(self):
"""Return the number of active sessions."""
return len(self.cache)
class FileSession(Session):
"""Implementation of the File backend for sessions
storage_path
The folder where session data will be saved. Each session
will be saved as pickle.dump(data, expiration_time) in its own file;
the filename will be self.SESSION_PREFIX + self.id.
lock_timeout
A timedelta or numeric seconds indicating how long
to block acquiring a lock. If None (default), acquiring a lock
will block indefinitely.
"""
SESSION_PREFIX = 'session-'
LOCK_SUFFIX = '.lock'
pickle_protocol = pickle.HIGHEST_PROTOCOL
def __init__(self, id=None, **kwargs):
# The 'storage_path' arg is required for file-based sessions.
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
kwargs.setdefault('lock_timeout', None)
Session.__init__(self, id=id, **kwargs)
# validate self.lock_timeout
if isinstance(self.lock_timeout, (int, float)):
self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout)
if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))):
raise ValueError('Lock timeout must be numeric seconds or '
'a timedelta instance.')
@classmethod
def setup(cls, **kwargs):
"""Set up the storage system for file-based sessions.
This should only be called once per process; this will be done
automatically when using sessions.init (as the built-in Tool does).
"""
# The 'storage_path' arg is required for file-based sessions.
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
for k, v in kwargs.items():
setattr(cls, k, v)
def _get_file_path(self):
f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
if not os.path.abspath(f).startswith(self.storage_path):
raise cherrypy.HTTPError(400, 'Invalid session id in cookie.')
return f
def _exists(self):
path = self._get_file_path()
return os.path.exists(path)
def _load(self, path=None):
assert self.locked, ('The session load without being locked. '
"Check your tools' priority levels.")
if path is None:
path = self._get_file_path()
try:
f = open(path, 'rb')
try:
return pickle.load(f)
finally:
f.close()
except (IOError, EOFError):
e = sys.exc_info()[1]
if self.debug:
cherrypy.log('Error loading the session pickle: %s' %
e, 'TOOLS.SESSIONS')
return None
def _save(self, expiration_time):
assert self.locked, ('The session was saved without being locked. '
"Check your tools' priority levels.")
f = open(self._get_file_path(), 'wb')
try:
pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
finally:
f.close()
def _delete(self):
assert self.locked, ('The session deletion without being locked. '
"Check your tools' priority levels.")
try:
os.unlink(self._get_file_path())
except OSError:
pass
def acquire_lock(self, path=None):
"""Acquire an exclusive lock on the currently-loaded session data."""
if path is None:
path = self._get_file_path()
path += self.LOCK_SUFFIX
checker = locking.LockChecker(self.id, self.lock_timeout)
while not checker.expired():
try:
self.lock = lockfile.LockFile(path)
except lockfile.LockError:
time.sleep(0.1)
else:
break
self.locked = True
if self.debug:
cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
def release_lock(self, path=None):
"""Release the lock on the currently-loaded session data."""
self.lock.release()
self.lock.remove()
self.locked = False
def clean_up(self):
"""Clean up expired sessions."""
now = self.now()
# Iterate over all session files in self.storage_path
for fname in os.listdir(self.storage_path):
if (fname.startswith(self.SESSION_PREFIX)
and not fname.endswith(self.LOCK_SUFFIX)):
# We have a session file: lock and load it and check
# if it's expired. If it fails, nevermind.
path = os.path.join(self.storage_path, fname)
self.acquire_lock(path)
if self.debug:
# This is a bit of a hack, since we're calling clean_up
# on the first instance rather than the entire class,
# so depending on whether you have "debug" set on the
# path of the first session called, this may not run.
cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS')
try:
contents = self._load(path)
# _load returns None on IOError
if contents is not None:
data, expiration_time = contents
if expiration_time < now:
# Session expired: deleting it
os.unlink(path)
finally:
self.release_lock(path)
def __len__(self):
"""Return the number of active sessions."""
return len([fname for fname in os.listdir(self.storage_path)
if (fname.startswith(self.SESSION_PREFIX)
and not fname.endswith(self.LOCK_SUFFIX))])
class MemcachedSession(Session):
# The most popular memcached client for Python isn't thread-safe.
# Wrap all .get and .set operations in a single lock.
mc_lock = threading.RLock()
# This is a seperate set of locks per session id.
locks = {}
servers = ['127.0.0.1:11211']
@classmethod
def setup(cls, **kwargs):
"""Set up the storage system for memcached-based sessions.
This should only be called once per process; this will be done
automatically when using sessions.init (as the built-in Tool does).
"""
for k, v in kwargs.items():
setattr(cls, k, v)
import memcache
cls.cache = memcache.Client(cls.servers)
def _exists(self):
self.mc_lock.acquire()
try:
return bool(self.cache.get(self.id))
finally:
self.mc_lock.release()
def _load(self):
self.mc_lock.acquire()
try:
return self.cache.get(self.id)
finally:
self.mc_lock.release()
def _save(self, expiration_time):
# Send the expiration time as "Unix time" (seconds since 1/1/1970)
td = int(time.mktime(expiration_time.timetuple()))
self.mc_lock.acquire()
try:
if not self.cache.set(self.id, (self._data, expiration_time), td):
raise AssertionError(
'Session data for id %r not set.' % self.id)
finally:
self.mc_lock.release()
def _delete(self):
self.cache.delete(self.id)
def acquire_lock(self):
"""Acquire an exclusive lock on the currently-loaded session data."""
self.locked = True
self.locks.setdefault(self.id, threading.RLock()).acquire()
if self.debug:
cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
def release_lock(self):
"""Release the lock on the currently-loaded session data."""
self.locks[self.id].release()
self.locked = False
def __len__(self):
"""Return the number of active sessions."""
raise NotImplementedError
# Hook functions (for CherryPy tools)
def save():
"""Save any changed session data."""
if not hasattr(cherrypy.serving, 'session'):
return
request = cherrypy.serving.request
response = cherrypy.serving.response
# Guard against running twice
if hasattr(request, '_sessionsaved'):
return
request._sessionsaved = True
if response.stream:
# If the body is being streamed, we have to save the data
# *after* the response has been written out
request.hooks.attach('on_end_request', cherrypy.session.save)
else:
# If the body is not being streamed, we save the data now
# (so we can release the lock).
if is_iterator(response.body):
response.collapse_body()
cherrypy.session.save()
save.failsafe = True
def close():
"""Close the session object for this request."""
sess = getattr(cherrypy.serving, 'session', None)
if getattr(sess, 'locked', False):
# If the session is still locked we release the lock
sess.release_lock()
if sess.debug:
cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS')
close.failsafe = True
close.priority = 90
def init(storage_type=None, path=None, path_header=None, name='session_id',
timeout=60, domain=None, secure=False, clean_freq=5,
persistent=True, httponly=False, debug=False,
# Py27 compat
# *, storage_class=RamSession,
**kwargs):
"""Initialize session object (using cookies).
storage_class
The Session subclass to use. Defaults to RamSession.
storage_type
(deprecated)
One of 'ram', 'file', memcached'. This will be
used to look up the corresponding class in cherrypy.lib.sessions
globals. For example, 'file' will use the FileSession class.
path
The 'path' value to stick in the response cookie metadata.
path_header
If 'path' is None (the default), then the response
cookie 'path' will be pulled from request.headers[path_header].
name
The name of the cookie.
timeout
The expiration timeout (in minutes) for the stored session data.
If 'persistent' is True (the default), this is also the timeout
for the cookie.
domain
The cookie domain.
secure
If False (the default) the cookie 'secure' value will not
be set. If True, the cookie 'secure' value will be set (to 1).
clean_freq (minutes)
The poll rate for expired session cleanup.
persistent
If True (the default), the 'timeout' argument will be used
to expire the cookie. If False, the cookie will not have an expiry,
and the cookie will be a "session cookie" which expires when the
browser is closed.
httponly
If False (the default) the cookie 'httponly' value will not be set.
If True, the cookie 'httponly' value will be set (to 1).
Any additional kwargs will be bound to the new Session instance,
and may be specific to the storage type. See the subclass of Session
you're using for more information.
"""
# Py27 compat
storage_class = kwargs.pop('storage_class', RamSession)
request = cherrypy.serving.request
# Guard against running twice
if hasattr(request, '_session_init_flag'):
return
request._session_init_flag = True
# Check if request came with a session ID
id = None
if name in request.cookie:
id = request.cookie[name].value
if debug:
cherrypy.log('ID obtained from request.cookie: %r' % id,
'TOOLS.SESSIONS')
first_time = not hasattr(cherrypy, 'session')
if storage_type:
if first_time:
msg = 'storage_type is deprecated. Supply storage_class instead'
cherrypy.log(msg)
storage_class = storage_type.title() + 'Session'
storage_class = globals()[storage_class]
# call setup first time only
if first_time:
if hasattr(storage_class, 'setup'):
storage_class.setup(**kwargs)
# Create and attach a new Session instance to cherrypy.serving.
# It will possess a reference to (and lock, and lazily load)
# the requested session data.
kwargs['timeout'] = timeout
kwargs['clean_freq'] = clean_freq
cherrypy.serving.session = sess = storage_class(id, **kwargs)
sess.debug = debug
def update_cookie(id):
"""Update the cookie every time the session id changes."""
cherrypy.serving.response.cookie[name] = id
sess.id_observers.append(update_cookie)
# Create cherrypy.session which will proxy to cherrypy.serving.session
if not hasattr(cherrypy, 'session'):
cherrypy.session = cherrypy._ThreadLocalProxy('session')
if persistent:
cookie_timeout = timeout
else:
# See http://support.microsoft.com/kb/223799/EN-US/
# and http://support.mozilla.com/en-US/kb/Cookies
cookie_timeout = None
set_response_cookie(path=path, path_header=path_header, name=name,
timeout=cookie_timeout, domain=domain, secure=secure,
httponly=httponly)
def set_response_cookie(path=None, path_header=None, name='session_id',
timeout=60, domain=None, secure=False, httponly=False):
"""Set a response cookie for the client.
path
the 'path' value to stick in the response cookie metadata.
path_header
if 'path' is None (the default), then the response
cookie 'path' will be pulled from request.headers[path_header].
name
the name of the cookie.
timeout
the expiration timeout for the cookie. If 0 or other boolean
False, no 'expires' param will be set, and the cookie will be a
"session cookie" which expires when the browser is closed.
domain
the cookie domain.
secure
if False (the default) the cookie 'secure' value will not
be set. If True, the cookie 'secure' value will be set (to 1).
httponly
If False (the default) the cookie 'httponly' value will not be set.
If True, the cookie 'httponly' value will be set (to 1).
"""
# Set response cookie
cookie = cherrypy.serving.response.cookie
cookie[name] = cherrypy.serving.session.id
cookie[name]['path'] = (
path or
cherrypy.serving.request.headers.get(path_header) or
'/'
)
# We'd like to use the "max-age" param as indicated in
# http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
# save it to disk and the session is lost if people close
# the browser. So we have to use the old "expires" ... sigh ...
## cookie[name]['max-age'] = timeout * 60
if timeout:
e = time.time() + (timeout * 60)
cookie[name]['expires'] = httputil.HTTPDate(e)
if domain is not None:
cookie[name]['domain'] = domain
if secure:
cookie[name]['secure'] = 1
if httponly:
if not cookie[name].isReservedKey('httponly'):
raise ValueError('The httponly cookie token is not supported.')
cookie[name]['httponly'] = 1
def expire():
"""Expire the current session cookie."""
name = cherrypy.serving.request.config.get(
'tools.sessions.name', 'session_id')
one_year = 60 * 60 * 24 * 365
e = time.time() - one_year
cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)

381
cherrypy/lib/static.py Normal file
View File

@@ -0,0 +1,381 @@
import os
import re
import stat
import mimetypes
try:
from io import UnsupportedOperation
except ImportError:
UnsupportedOperation = object()
import cherrypy
from cherrypy._cpcompat import ntob, unquote
from cherrypy.lib import cptools, httputil, file_generator_limited
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.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):
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, 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'] = httputil.HTTPDate(st.st_mtime)
cptools.validate_since()
if content_type is None:
# Set content-type based on filename extension
ext = ''
i = path.rfind('.')
if i != -1:
ext = path[i:].lower()
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
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
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
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' % 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 > content_length:
stop = content_length
r_len = stop - start
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
fileobj.seek(start)
response.body = file_generator_limited(fileobj, r_len)
else:
# Return a multipart/byteranges response.
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 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers['Content-Length']
def file_ranges():
# Apache compatibility:
yield ntob('\r\n')
for start, stop in r:
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 ntob('\r\n')
# Final boundary
yield ntob('--' + boundary + '--', 'ascii')
# Apache compatibility:
yield ntob('\r\n')
response.body = file_ranges()
return response.body
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)
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
content_type = None
if content_types:
r, ext = os.path.splitext(filename)
content_type = content_types.get(ext[1:], None)
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='',
debug=False):
"""Serve a static resource from the given (root +) dir.
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.
"""
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).'
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 = 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
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:
request.is_index = filename[-1] in (r'\/')
return handled
def staticfile(filename, root=None, match='', content_types=None, debug=False):
"""Serve a static resource from the given (root +) filename.
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").
"""
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,)
if debug:
cherrypy.log(msg, 'TOOLS.STATICFILE')
raise ValueError(msg)
filename = os.path.join(root, filename)
return _attempt(filename, content_types, debug=debug)

View File

@@ -0,0 +1,57 @@
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:
return get_xmlrpclib().loads(cherrypy.request.body.read())
except Exception:
return ('ERROR PARAMS', ), 'ERRORMETHOD'
def patched_path(path):
"""Return 'path', doctored for RPC."""
if not path.endswith('/'):
path += '/'
if path.startswith('/RPC2/'):
# strip the first /rpc2
path = path[5:]
return path
def _set_response(body):
# The XML-RPC spec (http://www.xmlrpc.com/spec) says:
# "Unless there's a lower-level error, always return 200 OK."
# Since Python's xmlrpclib interprets a non-200 response
# as a "Protocol Error", we'll just return 200 every time.
response = cherrypy.response
response.status = '200 OK'
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):
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])
xmlrpclib = get_xmlrpclib()
_set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))

View File

@@ -0,0 +1,14 @@
"""Site container for an HTTP server.
A Web Site Process Bus object is used to connect applications, servers,
and frameworks with site-wide services such as daemonization, process
reload, signal handling, drop privileges, PID file management, logging
for all of these, and many more.
The 'plugins' module defines a few abstract and concrete services for
use with the bus. Some use tool-specific channels; see the documentation
for each class.
"""
from cherrypy.process.wspbus import bus # noqa
from cherrypy.process import plugins, servers # noqa

740
cherrypy/process/plugins.py Normal file
View File

@@ -0,0 +1,740 @@
"""Site services for use with a Web Site Process Bus."""
import os
import re
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:
# Subscribe self.start, self.exit, etc. if present.
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:
# Unsubscribe self.start, self.exit, etc. if present.
method = getattr(self, channel, None)
if method is not None:
self.bus.unsubscribe(channel, method)
class SignalHandler(object):
"""Register bus channels (and listeners) for system signals.
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.
"""
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
self.handlers = {'SIGTERM': self.bus.exit,
'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):
"""Subscribe self.handlers to signals."""
for sig, func in self.handlers.items():
try:
self.set_handler(sig, func)
except ValueError:
pass
def unsubscribe(self):
"""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)
handler = _signal.SIG_DFL
else:
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.' %
(signame, handler), level=30)
except ValueError:
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, text_or_bytes):
signum = getattr(_signal, signal, None)
if signum is None:
raise ValueError('No such signal: %r' % signal)
signame = signal
else:
try:
signame = self.signals[signal]
except KeyError:
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.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.publish(signame)
def handle_SIGHUP(self):
"""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
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/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.',
level=30)
val = None
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. 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.',
level=30)
val = None
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. 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.',
level=30)
val = None
self._umask = val
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():
"""Return the current (uid, gid) if available."""
name, group = None, None
if pwd:
name = pwd.getpwuid(os.getuid())[0]
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' %
current_ids())
else:
if self.uid is None and self.gid is None:
if pwd or grp:
self.bus.log('uid/gid not set', level=30)
else:
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:
self.bus.log('umask already set to: %03o' % self.umask)
else:
if self.umask is None:
self.bus.log('umask not set', level=30)
else:
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
# port (which requires root) and then dropping to another user.
start.priority = 77
class Daemonizer(SimplePlugin):
"""Daemonize the running script.
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
error occurs in the forked children. Errors in the initial daemonizing
process still return proper exit codes. Therefore, if you use this
plugin to daemonize, don't use the return code as an accurate indicator
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)
self.stdin = stdin
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
# world is what to do with all of the threads..."
# So we check for active threads:
if threading.activeCount() != 1:
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()
if pid == 0:
# This is the child process. Continue.
pass
else:
# This is the first parent. Exit, now that we've forked.
self.bus.log('Forking once.')
os._exit(0)
except OSError:
# Python raises OSError rather than returning negative numbers.
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.exc_info()[1]
sys.exit('%s: fork #2 failed: (%d) %s\n'
% (sys.argv[0], exc.errno, exc.strerror))
os.chdir('/')
os.umask(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.
# See http://docs.python.org/lib/os-fd-ops.html
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(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)
self.bus.log('PID file removed: %r.' % self.pidfile)
except (KeyboardInterrupt, SystemExit):
raise
except:
pass
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
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."""
callback = None
"""The function to call at intervals."""
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 background thread."""
if self.frequency > 0:
threadname = self.name or self.__class__.__name__
if self.thread is None:
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)
else:
self.bus.log('Monitor thread %r already started.' % threadname)
start.priority = 70
def stop(self):
"""Stop our callback's background task thread."""
if self.thread is None:
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()
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 background task thread and restart it."""
self.stop()
self.start()
class Autoreloader(Monitor):
"""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 background task thread for self.run."""
if self.thread is None:
self.mtimes = {}
Monitor.start(self)
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."""
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.thread.cancel()
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
'release_thread' in this case, since you do not know whether
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 = get_thread_ident()
if thread_ident not in self.threads:
# 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 = 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.items():
self.bus.publish('stop_thread', i)
self.threads.clear()
graceful = stop

470
cherrypy/process/servers.py Normal file
View File

@@ -0,0 +1,470 @@
"""
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::
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?)'
elif isinstance(self.bind_addr, tuple):
on_what = self._get_base()
else:
on_what = 'socket file: %s' % self.bind_addr
if self.running:
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.')
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.start()
self.wait()
self.running = True
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
exception is raised in the httpserver's thread then it's
trapped here, and the bus (and therefore our httpserver)
are shut down.
"""
try:
self.httpserver.start()
except KeyboardInterrupt:
self.bus.log('<Ctrl-C> hit: shutting down HTTP server')
self.interrupt = sys.exc_info()[1]
self.bus.exit()
except SystemExit:
self.bus.log('SystemExit raised: shutting down HTTP server')
self.interrupt = sys.exc_info()[1]
self.bus.exit()
raise
except:
self.interrupt = sys.exc_info()[1]
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):
if self.interrupt:
raise self.interrupt
time.sleep(.1)
# Wait for port to be occupied
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:
# stop() MUST block until the server is *truly* stopped.
self.httpserver.stop()
# Wait for the socket to be truly freed.
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)
else:
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, 'fromfd'):
raise ValueError(
'Dynamic FCGI server not available on this platform. '
'You must use a static or external one by providing a '
'legal bindAddress.')
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__
# starts a threadpool. If we do it too early, daemonize won't work.
from flup.server.fcgi import WSGIServer
self.fcgiserver = WSGIServer(*self.args, **self.kwargs)
# TODO: report this bug upstream to flup.
# If we don't set _oldSIGs on Windows, we get:
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
# line 108, in run
# self._restoreSignalHandlers()
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
# line 156, in _restoreSignalHandlers
# for signum,handler in self._oldSIGs:
# AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
self.fcgiserver._installSignalHandlers = lambda: None
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.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__
# starts a threadpool. If we do it too early, daemonize won't work.
from flup.server.scgi import WSGIServer
self.scgiserver = WSGIServer(*self.args, **self.kwargs)
# TODO: report this bug upstream to flup.
# If we don't set _oldSIGs on Windows, we get:
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
# line 108, in run
# self._restoreSignalHandlers()
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
# line 156, in _restoreSignalHandlers
# for signum,handler in self._oldSIGs:
# AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
self.scgiserver._installSignalHandlers = lambda: None
self.scgiserver._oldSIGs = []
self.ready = True
self.scgiserver.run()
def stop(self):
"""Stop the HTTP server."""
self.ready = False
# Forcibly stop the scgi server main event loop.
self.scgiserver._keepGoing = False
# Force all worker threads to die off.
self.scgiserver._threadPool.maxSpare = 0
def client_host(server_host):
"""Return the host on which a client can connect to the given listener."""
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 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)
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:
s = socket.socket(af, socktype, proto)
# See http://groups.google.com/group/cherrypy-users/
# browse_frm/thread/bbfe5eb39c904fe0
s.settimeout(timeout)
s.connect((host, port))
s.close()
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)))
# 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.")
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=timeout)
except IOError:
# Give the old server thread time to free the port.
time.sleep(timeout)
else:
return
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.")
if timeout is None:
timeout = occupied_port_timeout
for trial in range(50):
try:
check_port(host, port, timeout=timeout)
except IOError:
# port is occupied
return
else:
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)

180
cherrypy/process/win32.py Normal file
View File

@@ -0,0 +1,180 @@
"""Windows service. Requires pywin32."""
import os
import win32api
import win32con
import win32event
import win32service
import win32serviceutil
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)' %
win32api.GetLastError(), level=40)
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
return 0
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,
'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, channel=None):
"""Wait for the given state(s), KeyboardInterrupt or SystemExit.
Since this class uses native win32event objects, the interval
argument is ignored.
"""
if isinstance(state, (tuple, list)):
# 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)
else:
# Don't wait for an event that beat us to the punch ;)
if self.state != state:
event = self._get_state_event(state)
win32event.WaitForSingleObject(event, win32event.INFINITE)
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.items():
if val is obj:
return key
raise ValueError('The given object could not be found: %r' % obj)
control_codes = _ControlCodes({'graceful': 138})
def signal_child(service, command):
if command == 'stop':
win32serviceutil.StopService(service)
elif command == 'restart':
win32serviceutil.RestartService(service)
else:
win32serviceutil.ControlService(service, control_codes[command])
class PyWebService(win32serviceutil.ServiceFramework):
"""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_args_ = None # Default to no arguments
# Only exists on Windows 2000 or later, ignored on windows NT
_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))
if __name__ == '__main__':
win32serviceutil.HandleCommandLine(PyWebService)

519
cherrypy/process/wspbus.py Normal file
View File

@@ -0,0 +1,519 @@
"""An implementation of the Web Site Process Bus.
This module is completely standalone, depending only on the stdlib.
Web Site Process Bus
--------------------
A Bus object is used to contain and manage site-wide behavior:
daemonization, HTTP server start/stop, process reload, signal handling,
drop privileges, PID file management, logging for all of these,
and many more.
In addition, a Bus object provides a place for each web framework
to register code that runs in response to site-wide events (like
process start and stop), or which controls or otherwise interacts with
the site-wide components mentioned above. For example, a framework which
uses file-based templates would add known template filenames to an
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.
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.
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.
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', '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
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.::
O
|
V
STOPPING --> STOPPED --> EXITING -> X
A A |
| \___ |
| \ |
| V V
STARTED <-- STARTING
"""
import atexit
import ctypes
import operator
import os
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
def __setattr__(self, key, value):
if isinstance(value, self.State):
value.name = key
object.__setattr__(self, key, value)
states = _StateEnum()
states.STOPPED = states.State()
states.STARTING = states.State()
states.STARTED = states.State()
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 channels
)
self._priorities = {}
def subscribe(self, channel, callback, priority=None):
"""Add the given callback at the given channel (if not present)."""
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 = ChannelFailures()
output = []
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 = 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.handle_exception()
if channel == 'log':
# Assume any further messages to 'log' will fail.
pass
else:
self.log('Error in %r listener %r' % (channel, listener),
level=40, traceback=True)
if exc:
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)
self.exit()
def start(self):
"""Start all services."""
atexit.register(self._clean_exit)
self.state = states.STARTING
self.log('Bus STARTING')
try:
self.publish('start')
self.state = states.STARTED
self.log('Bus STARTED')
except (KeyboardInterrupt, SystemExit):
raise
except:
self.log('Shutting down due to error in start listener:',
level=40, traceback=True)
e_info = sys.exc_info()[1]
try:
self.exit()
except:
# Any stop/exit errors will be logged inside publish().
pass
# 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')
# This isn't strictly necessary, but it's better than seeing
# "Waiting for child threads to terminate..." and then nothing.
self.log('Bus EXITED')
except:
# This method is often called asynchronously (whether thread,
# 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
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
design allows another thread to call bus.restart, yet have the main
thread perform the actual execv call (required on some platforms).
"""
try:
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.
self.log('Keyboard Interrupt: shutting down bus')
self.exit()
except SystemExit:
self.log('SystemExit raised: shutting down bus')
self.exit()
raise
# Waiting for ALL child threads to finish is necessary on OS X.
# 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 https://github.com/cherrypy/cherrypy/issues/751.
self.log('Waiting for child threads to terminate...')
for t in threading.enumerate():
# 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'):
# 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, 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
# detected before execution comes back to the regular Python
# interpreter. Your program cannot be interrupted if caught
# into an infinite Psyco-compiled loop."
try:
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.
"""
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))
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
self.log('Bus STOPPING')
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:
args = ()
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):
"""Log the given message. Append the last traceback if requested."""
if traceback:
# 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)

22
email/badfetch-en.tmpl Normal file
View File

@@ -0,0 +1,22 @@
##
## Bad URL Fetch Email template for SABnzbd
## This a Cheetah template
## Documentation: http://sabnzbd.wikidot.com/email-templates
##
## Newlines and whitespace are significant!
##
## These are the email headers
To: $to
From: $from
Date: $date
Subject: SABnzbd failed to fetch an NZB
X-priority: 5
X-MS-priority: 5
## After this comes the body, the empty line is required!
Hi,
SABnzbd has failed to retrieve the NZB from $url.
The error message was: $msg
Bye

509
gntp/__init__.py Normal file
View File

@@ -0,0 +1,509 @@
import re
import hashlib
import time
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'
class BaseError(Exception):
def gntp_error(self):
error = GNTPError(self.errorcode, self.errordesc)
return error.encode()
class ParseError(BaseError):
errorcode = 500
errordesc = 'Error parsing the message'
class AuthError(BaseError):
errorcode = 400
errordesc = 'Error with authorization'
class UnsupportedError(BaseError):
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):
"""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 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 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
return
if not self.encryptAlgo in hash.keys():
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
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 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)
result += chr(tmp)
return result
def _decode_binary(self, rawIdentifier, identifier):
rawIdentifier += '\r\n\r\n'
dataLength = int(identifier['Length'])
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)))
return data
def _validate_password(self, password):
"""Validate GNTP Message against stored password"""
self.password = password
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
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"""
for header in self._requiredHeaders:
if not self.headers.get(header, False):
raise ParseError('Missing Notification Header: ' + header)
def _format_info(self):
"""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' % (
self.info.get('encryptionAlgorithmID'),
self.info.get('ivValue'),
)
else:
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 string data:
:return dict:
"""
dict = {}
for line in data.split('\r\n'):
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):
if isinstance(value, unicode):
self.headers[key] = value
else:
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 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():
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
: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):
_GNTPBase.__init__(self, 'REGISTER')
self.notifications = []
if data:
self.decode(data, password)
else:
self.set_password(password)
self.add_header('Application-Name', 'pygntp')
self.add_header('Notifications-Count', 0)
def validate(self):
'''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)
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 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
notice = self._parse_dict(part)
if notice.get('Notification-Name', False):
self.notifications.append(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 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
self.notifications.append(notice)
self.add_header('Notifications-Count', len(self.notifications))
def encode(self):
"""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():
buffer.writefmt('%s: %s', k, v)
buffer.writefmt()
#Notifications
if len(self.notifications) > 0:
for notice in self.notifications:
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
: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):
_GNTPBase.__init__(self, 'NOTIFY')
if data:
self.decode(data, password)
else:
self.set_password(password)
if app:
self.add_header('Application-Name', app)
if name:
self.add_header('Notification-Name', name)
if title:
self.add_header('Notification-Title', title)
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
notice = self._parse_dict(part)
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
class GNTPSubscribe(_GNTPBase):
"""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)
else:
self.set_password(password)
class GNTPOK(_GNTPBase):
"""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):
_GNTPBase.__init__(self, '-OK')
if data:
self.decode(data)
if action:
self.add_header('Response-Action', action)
class GNTPError(_GNTPBase):
"""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)
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)
elif info['messagetype'] == 'NOTIFY':
return GNTPNotice(data, password=password)
elif info['messagetype'] == 'SUBSCRIBE':
return GNTPSubscribe(data, password=password)
elif info['messagetype'] == '-OK':
return GNTPOK(data)
elif info['messagetype'] == '-ERROR':
return GNTPError(data)
raise ParseError('INVALID_GNTP_MESSAGE')

247
gntp/notifier.py Normal file
View File

@@ -0,0 +1,247 @@
"""
The gntp.notifier module is provided as a simple way to send notifications
using GNTP
.. note::
This class is intended to mostly mirror the older Python bindings such
that you should be able to replace instances of the old bindings with
this class.
`Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_
"""
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
:param string applicationName: Sending application name
:param list notification: List of valid notifications
:param list defaultNotifications: List of notifications that should be enabled
by default
:param string applicationIcon: Icon URL
:param string hostname: Remote host
:param integer port: Remote port
"""
passwordHash = 'MD5'
socketTimeout = 3
def __init__(self, applicationName='Python GNTP', notifications=[],
defaultNotifications=None, applicationIcon=None, hostname='localhost',
password=None, port=23053):
self.applicationName = applicationName
self.notifications = list(notifications)
if defaultNotifications:
self.defaultNotifications = list(defaultNotifications)
else:
self.defaultNotifications = self.notifications
self.applicationIcon = applicationIcon
self.password = password
self.hostname = hostname
self.port = int(port)
def _checkIcon(self, data):
'''
Check the icon to see if it's valid
If it's a simple URL icon, then we return True. If it's a data icon
then we return False
'''
logger.debug('Checking icon')
return data.startswith('http')
def register(self):
"""Send GNTP Registration
.. warning::
Before sending notifications to Growl, you need to have
sent a registration message at least once
"""
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:
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)
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, callback=None, identifier=None):
"""Send a GNTP notifications
.. warning::
Must have registered with growl beforehand or messages will be ignored
:param string noteType: One of the notification names registered earlier
:param string title: Notification title (usually displayed on the notification)
:param string description: The main content of the notification
: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.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)
notice.add_header('Notification-Name', noteType)
notice.add_header('Notification-Title', title)
if self.password:
notice.set_password(self.password, self.passwordHash)
if sticky:
notice.add_header('Notification-Sticky', sticky)
if priority:
notice.add_header('Notification-Priority', priority)
if 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)
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"""
sub = gntp.GNTPSubscribe()
sub.add_header('Subscriber-ID', id)
sub.add_header('Subscriber-Name', name)
sub.add_header('Subscriber-Port', port)
if self.password:
sub.set_password(self.password, self.passwordHash)
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"""
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)
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
logger.debug('From : %s:%s <%s>', self.hostname, self.port, response.__class__)
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')

BIN
icons/nzb.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
icons/sabnzbd.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 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

@@ -1,22 +0,0 @@
#
# Copyright 2008-2011 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"#-->
<!--#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,102 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="configure-categories"#-->
<!--#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-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>
</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+Directories+V2"#-->
<!--#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,66 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+Email"#-->
<!--#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">
<input type="hidden" name="session" value="$session">
</fieldset>
</div>
<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')"/>
</p>
</form>
<!--#if $lastmail#-->
$T('emailResult') = <b>$lastmail</b>
<!--#end if#-->
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,145 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+General+V2"#-->
<!--#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+Index+Sites"#-->
<!--#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://www.newzbin.com/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,374 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+RSS+V2"#-->
<!--#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')"/>
</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+V2"#-->
<!--#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+V2"#-->
<!--#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"#-->
<!--#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,150 +0,0 @@
<!--#set global $topmenu="config"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="Configure+Switches+V3"#-->
<!--#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="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>
<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><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>
</div>
<p><input type="submit" value="$T('button-saveChanges')"></p>
</form>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,45 +0,0 @@
<!--#set global $have_refresh=5#-->
<!--#set global $topmenu="connections"#-->
<!--#set global $statpath=".."#-->
<!--#set global $helpsubject="GUI+Connections"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<span class="SubMenu">
<!--#set $msg=$T('askTestEmail')#-->
<a href="./disconnect?session=$session">$T('link-forceDisc')</a> |
<a href="./showlog?session=$session">$T('link-showLog')</a>
$T('logging'):
<select onChange="location = './change_loglevel?session=$session&loglevel='+this.options[this.selectedIndex].value">
<option value="0" <!--#if $loglevel == "0" then "selected" else ""#-->>$T('log-errWarn')</option>
<option value="1" <!--#if $loglevel == "1" then "selected" else ""#-->>$T('log-info')</option>
<option value="2" <!--#if $loglevel == "2" then "selected" else ""#-->>$T('log-debug')</option>
</select>
</span>
<h2>$T('connections')</h2>
<!--#if $servers#-->
<ul>
<!--#for $server in $servers#-->
<li>
$server[0]: $server[2]
<!--#if not $server[5]#-->
<a href="./unblock_server?server=$server[0]&session=$session">($T('server-blocked'))</a>
&nbsp;&nbsp;$server[6]
<!--#end if#-->
<ul>
<!--#for $thrd in $server[3]#-->
<li>$T('thread') #$thrd[0] -&gt; $thrd[1] -&gt; $thrd[2] -&gt; $thrd[3]</li>
<!--#end for#-->
</ul>
</li>
<!--#end for#-->
</ul>
<!--#end if#-->
<!--#if $warnings#-->
<h2>$T('lastWarnings') (<a href="./clearwarnings?session=$session">$T('clearWarnings')</a>)</h2>
<!--#for $warn in $warnings#-->
$warn<br/>
<!--#end for#-->
<!--#end if#-->
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,99 +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> |
<!--#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,32 +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<br>
<!--#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/connections/">$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=="directories"#-->
<a class="current" href="./">$T('cmenu-folders')</a> |
<!--#else#-->
<a href="$cpath/directories/">$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=="email"#-->
<a class="current" href="./">$T('cmenu-email')</a> |
<!--#else#-->
<a href="$cpath/email/">$T('cmenu-email')</a> |
<!--#end if#-->
<!--#if $submenu=="newzbin"#-->
<a class="current" href="./">$T('cmenu-newzbin')</a> |
<!--#else#-->
<a href="$cpath/newzbin/">$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/connections/">$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"#-->

View File

@@ -1,150 +0,0 @@
<!--#set global $topmenu="queue"#-->
<!--#set global $statpath="../.."#-->
<!--#set global $helpsubject="GUI+Queue"#-->
<!--#include $webdir + "/inc_top.tmpl"#-->
<script type='text/javascript'>
function submit_marked(nzo_id, action_key)
{
extra = 'action_key='+action_key
var s;
var i=0;
while(s = document.getElementsByTagName("input")[i++])
{
if(s.type=='checkbox' && s.checked == true)
{
extra = extra + '&' + s.name + '=on'
}
}
//submitconfig(extra, '', '', 1)
url = 'nzb/'+nzo_id+'/bulk_operation'
lr(url, extra)
}
function mark_all()
{
var s;
var i=0;
while(s = document.getElementsByTagName("input")[i++])
{
if(s.type=='checkbox')
{
s.checked=true;
}
}
}
</script>
<script type='text/javascript'>
function unmark_all()
{
var s;
var i=0;
while(s = document.getElementsByTagName("input")[i++])
{
if(s.type=='checkbox')
{
s.checked=false;
}
}
}
</script>
<script type='text/javascript'>
function invert()
{
var s;
var i=0;
while(s = document.getElementsByTagName("input")[i++])
{
if(s.type=='checkbox')
{
s.checked=!s.checked;
}
}
}
</script>
<span class="SubMenu">
&nbsp;
</span>
<!--#if $varExists('slot')#-->
<h3>$T('nzoDetails')</h3>
<form action="save" method="post">
<label class="label">$T('nzoName'):</label><br />
<input type="text" name="name" style="width:400px" size="80" value="$slot.filename">
<br />
<label class="label">$T('pp'):</label><br />
<select name="pp">
<option value="0" <!--#if $slot.unpackopts == "0" then "selected" else ""#-->>$T('pp-n')</option>
<option value="1" <!--#if $slot.unpackopts == "1" then "selected" else ""#-->>$T('pp-r')</option>
<option value="2" <!--#if $slot.unpackopts == "2" then "selected" else ""#-->>$T('pp-u')</option>
<option value="3" <!--#if $slot.unpackopts == "3" then "selected" else ""#-->>$T('pp-d')</option>
</select>
<!--#if $slot.priority != '2'#-->
<br />
<label class="label">$T('priority'):</label><br />
<select name="priority">
<option value="-100" <!--#if $slot.priority == "-100" then "selected" else ""#-->>$T('default')</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>
</select>
<!--#end if#-->
<!--#if $script_list#-->
<br />
<label class="label">$T('script'):</label><br />
<select name="script">
<!--#for $sc in $script_list#-->
<option value="$sc" <!--#if $slot.script.lower() == $sc.lower() then "selected" else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</select>
<!--#end if#-->
<!--#if $cat_list#-->
<br />
<label class="label">$T('category'):</label><br />
<select name="cat">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $slot.cat.lower() == $ct.lower() then "selected" else ""#-->>$ct</option>
<!--#end for#-->
</select>
<!--#end if#-->
<br /><br />
<input type="submit" size="40" value="$T('button-saveChanges')">
</form>
<br />
<br /><br /><br />
<!--#end if#-->
<!--#if $varExists('active_files')#-->
<form action="bulk_operation">
<input type="hidden" name="session" value="$session">
<input type="submit" name="action_key" value="Delete">
<input type="submit" name="action_key" value="Top">
<input type="submit" name="action_key" value="Up">
<input type="submit" name="action_key" value="Down">
<input type="submit" name="action_key" value="Bottom">
<br>
$T('nzo-selection'):<a href='#' onclick='mark_all();' target='_self'>$T('nzo-all')</a>
<a href='#' onclick='unmark_all();' target='_self'>$T('nzo-none')</a>
<a href='#' onclick='invert();' target='_self'>$T('nzo-invert')</a>
<table id="historyTable">
<tr><th></th><th>$T('nzo-filename')/$T('nzo-subject')</th><th>$T('size')</th><th>$T('nzo-age')</th></tr>
<!--#set $oddLine = False#-->
<!--#for $file in $active_files#-->
<!--#set $oddLine = not $oddLine#-->
<tr class="<!--#if $oddLine then "oddLine" else "evenLine"#--> active">
<td><input type="checkbox" name="$file.nzf_id" <!--#if $file.checked then "checked" else ""#-->></td>
<td>$file.filename</td>
<td>$file.size</td>
<td>$file.age</td>
</tr>
<!--#end for#-->
</table>
</form>
<!--#end if#-->
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

@@ -1,141 +0,0 @@
<!--#set global $topmenu="queue"#-->
<!--#set global $have_refresh=$refresh_rate#-->
<!--#set global $statpath=".."#-->
<!--#set global $helpsubject="GUI+Queue"#-->
<!--#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#-->|
<a href="./sort_by_name?session=$session">$T('link-sortByName')</a> |
<a href="./sort_by_avg_age?session=$session">$T('link-sortByAge')</a> |
<a href="./sort_by_size?session=$session">$T('link-sortBySize')</a> |
<!--#if $isverbose#-->
<a href="./tog_verbose?session=$session">$T('link-hideFiles')</a>
<!--#else#-->
<a href="./tog_verbose?session=$session">$T('link-showFiles')</a>|
<!--#end if#-->
$T('onQueueFinish'):
<select onChange="location = './change_queue_complete_action?action='+this.options[this.selectedIndex].value+'&session=$session'">
<option value=""></option>
<optgroup label="$T('eoq-actions')">
<!--#if $power_options#-->
<option value="shutdown_pc" <!--#if $finishaction == 'shutdown_pc' then 'selected' else ''#-->>$T('shutdownPc')</option>
<option value="standby_pc" <!--#if $finishaction == 'standby_pc' then 'selected' else ''#-->>$T('standbyPc')</option>
<option value="hibernate_pc" <!--#if $finishaction == 'hibernate_pc' then 'selected' else ''#-->>$T('hibernatePc')</option>
<!--#end if#-->
<option value="shutdown_program" <!--#if $finishaction == 'shutdown_program' then 'selected' else ''#-->>$T('shutdownSab')</option>
</optgroup>
<!--#if $script_list#-->
<optgroup label="$T('eoq-scripts')">
<!--#for $sc in $script_list#-->
<!--#if $sc != 'None'#-->
<option value="script_$sc" <!--#if $finishaction == 'script_'+$sc then 'selected' else ''#-->>$sc</option>
<!--#end if#-->
<!--#end for#-->
</optgroup>
<!--#end if#-->
</select>
| $T('speedLimit'):
<input id="speedbox" type="text" value="$speedlimit" onChange="location = './set_speedlimit?session=$session&value='+this.value">&nbsp;$T('KBs')
| $T('pauseFor'):
<input id="speedbox" type="text" size=5 value="" onChange="location = './set_pause?session=$session&value='+this.value">&nbsp;$T('minute')
|
<a href="./purge?session=$session" onclick="return confirm('$T('purgeQueueConf').replace("'","`") ');">$T('purgeQueue')</a>
</span>
<br>
<br>
<!--#if $varExists('slots')#-->
<table id="queueTable">
<tr> <th></th><th></th><th>$T('order')</th>
<!--#if $cat_list#--><th>$T('category')</th><!--#end if#-->
<th>$T('mode')</th><th>$T('priority')</th>
<!--#if $script_list#--><th>$T('script')</th><!--#end if#-->
<th>$T('name')</th><th>$T('remainTotal')</th><th>$T('eta')</th><th>$T('age')</th>
</tr>
<!--#set $odd = False#-->
<!--#for $slot in $slots#-->
<!--#set $odd = not $odd#-->
<tr class="<!--#if $odd then "odd" else "even"#--> $slot.priority $slot.status">
<td><a class="verbosity_link" href="./tog_uid_verbose?session=$session&uid=$slot.nzo_id"> <!--#if $slot.active then "-" else "+" #--> </a></td>
<td>
<input type="submit" onclick="if (confirm('$T('delFiles').replace("'","`") ')) window.location='delete?uid=$slot.nzo_id&del_files=1&session=$session'; else window.location='delete?uid=$slot.nzo_id&del_files=0&session=$session'; return false;" value="$T('button-del')">
<!--#if $slot.status == 'Paused'#-->
<form action="resume_nzo" method="get">
<input type="hidden" value="$slot.nzo_id" name="uid">
<input type="hidden" value="$session" name="session">
<input type="submit" value="$T('button-resume')"></form>
<!--#else#-->
<form action="pause_nzo" method="get">
<input type="hidden" value="$slot.nzo_id" name="uid">
<input type="hidden" value="$session" name="session">
<input type="submit" value="$T('button-pause')"></form>
<!--#end if#-->
<td><form><select onchange="location = this.options[this.selectedIndex].value;">
<!--#for $i in xrange($noofslots)#-->
<option value="switch?session=$session&uid1=$slot.nzo_id&uid2=$slots[i].nzo_id" <!--#if $i == $slot.index then "selected" else ""#-->>$i</option>
<!--#end for#-->
</select></form></td>
<!--#if $cat_list#-->
<td><form><select onchange="location = this.options[this.selectedIndex].value;">
<!--#for $ct in $cat_list#-->
<option value="change_cat?session=$session&nzo_id=$slot.nzo_id&cat=$ct" <!--#if $slot.cat.lower() == $ct.lower() then "selected" else ""#-->>$Tspec($ct)</option>
<!--#end for#-->
</select></form>
</td>
<!--#end if#-->
<td><form><select onchange="location = this.options[this.selectedIndex].value;">
<option value="change_opts?session=$session&nzo_id=$slot.nzo_id&pp=0" <!--#if $slot.unpackopts == "0" then "selected" else ""#-->></option>
<option value="change_opts?session=$session&nzo_id=$slot.nzo_id&pp=1" <!--#if $slot.unpackopts == "1" then "selected" else ""#-->>$T('pp-r')</option>
<option value="change_opts?session=$session&nzo_id=$slot.nzo_id&pp=2" <!--#if $slot.unpackopts == "2" then "selected" else ""#-->>$T('pp-u')</option>
<option value="change_opts?session=$session&nzo_id=$slot.nzo_id&pp=3" <!--#if $slot.unpackopts == "3" then "selected" else ""#-->>$T('pp-d')</option>
</select></form>
</td>
<td>
<!--#if $slot.priority#-->
<form>
<select onchange="location = this.options[this.selectedIndex].value;">
<option value="set_priority?session=$session&nzo_id=$slot.nzo_id&priority=2" <!--#if $slot.priority == "Force" then "selected" else ""#-->>$T('pr-force')</option>
<option value="set_priority?session=$session&nzo_id=$slot.nzo_id&priority=1" <!--#if $slot.priority == "High" then "selected" else ""#-->>$T('pr-high')</option>
<option value="set_priority?session=$session&nzo_id=$slot.nzo_id&priority=0" <!--#if $slot.priority == "Normal" then "selected" else ""#-->>$T('pr-normal')</option>
<option value="set_priority?session=$session&nzo_id=$slot.nzo_id&priority=-1" <!--#if $slot.priority == "Low" then "selected" else ""#-->>$T('pr-low')</option>
</select></form>
<!--#end if#-->
</td>
<!--#if $script_list#-->
<td><form><select onchange="location = this.options[this.selectedIndex].value;">
<!--#for $sc in $script_list#-->
<option value="change_script?session=$session&nzo_id=$slot.nzo_id&script=$sc" <!--#if $slot.script == $sc then "selected" else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</select></form>
</td>
<!--#end if#-->
<td><a href="../nzb/$slot.nzo_id/">$slot.filename</a></td>
<td class="pre">$slot.sizeleft/$slot.size</td>
<td>$slot.eta</td>
<td>$slot.avg_age</td>
</tr>
<!--#set $oddLine = False#-->
<!--#for $line in $slot.finished#-->
<!--#set $oddLine = not $oddLine#-->
<tr class="<!--#if $oddLine then "oddLine" else "evenLine"#--> finished"><td></td><td></td><td>Finished</td><td></td><!--#if $cat_list#--><td></td><!--#end if#--><td></td><!--#if $script_list#--><td></td><!--#end if#--><td>$line.filename</td><td class="pre">$line.mbleft/$line.mb MB<td></td><td>$line.age</td></tr>
<!--#end for#-->
<!--#set $oddLine = False#-->
<!--#for $line in $slot.active#-->
<!--#set $oddLine = not $oddLine#-->
<tr class="<!--#if $oddLine then "oddLine" else "evenLine"#--> active"><td></td><td><a class="remove" href="./removeNzf?session=$session&nzo_id=$slot.nzo_id&nzf_id=$line.nzf_id">Remove</a></td><td>Active</td><td></td><!--#if $cat_list#--><td></td><!--#end if#--><td></td><!--#if $script_list#--><td></td><!--#end if#--><td>$line.filename</td><td class="pre">$line.mbleft/$line.mb MB</td><td></td><td>$line.age</td></tr>
<!--#end for#-->
<!--#set $oddLine = False#-->
<!--#for $line in $slot.queued#-->
<!--#set $oddLine = not $oddLine#-->
<tr class="<!--#if $oddLine then "oddLine" else "evenLine"#--> waiting"><td></td></td><td><td>Waiting</td><td></td><!--#if $cat_list#--><td></td><!--#end if#--><td></td><!--#if $script_list#--><td></td><!--#end if#--><td>$line.filename (set: $line.set)</td><td class="pre">$line.mbleft/$line.mb MB<td></td><td>$line.age</td></tr>
<!--#end for#-->
<!--#end for#-->
</table>
<!--#end if#-->
<!--#include $webdir + "/inc_bottom.tmpl"#-->

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,12 +0,0 @@
sfHover = function() {
var sfEls = document.getElementById("nav").getElementsByTagName("LI");
for (var i=0; i<sfEls.length; i++) {
sfEls[i].onmouseover=function() {
this.className+=" sfhover";
}
sfEls[i].onmouseout=function() {
this.className=this.className.replace(new RegExp(" sfhover\\b"), "");
}
}
}
if (window.attachEvent) window.attachEvent("onload", sfHover);

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