Compare commits

...

1001 Commits
1.0.2 ... 2.0.x

Author SHA1 Message Date
Safihre
e69eeebdd8 Merge branch 'develop' - Update to 2.0.1 2017-05-24 12:14:56 +02:00
Safihre
be5bebb574 Update text files 2.0.1 - Nr2 2017-05-23 13:12:02 +02:00
Safihre
4ca2a7a65e Also match 'proof' when ignoring samples 2017-05-22 22:07:33 +02:00
Safihre
eed8c9bf50 Update text files for 2.0.1 2017-05-22 21:14:17 +02:00
Safihre
11d5855430 Always show Script box on Categories config page
Otherwise people might not be aware of it
2017-05-22 21:10:17 +02:00
Safihre
b13413b1e5 Only perform end-of-queue action when one is set 2017-05-22 10:38:52 +02:00
Safihre
9809474615 Remove cache from first Config page
It's a dynamic thing, not static like the Config. It can be observed live from the Status and Interface window.
2017-05-21 21:23:10 +02:00
Safihre
9ee3a61ae9 Convert HTML " in Repair confirmation 2017-05-21 21:05:12 +02:00
Safihre
c71ffa02f8 Remove ending \ in extraction path after long-path unrar retry
So, turns out that when not using the \\?\ notation, Unrar does not like the \ at the end of the path (which it does need otherwise). Linked #771
2017-05-21 13:53:38 +02:00
Safihre
2e862da292 Remember all unpack/repair info, no more overwrite
Keeps forgetting all the previous stages, no idea why that's usefull
2017-05-21 13:00:20 +02:00
Safihre
08d762c6c9 No longer try to verify using SFV and/or RAR-check when Par2 failed 2017-05-21 12:23:06 +02:00
Safihre
2ef6f9e0e7 Repair would be skipped if first par2-file was not downloaded 2017-05-20 19:14:04 +02:00
Safihre
b8f84cf18d Renames of QuickCheck were not saved if following files failed
Now we do as much as possible in QuickCheck before moving on, much faster then letting par2 do it
2017-05-20 15:52:36 +02:00
Safihre
7e88af7047 Small cleanup of par2repair code 2017-05-20 14:52:18 +02:00
Safihre
2fc365dd57 Update text-files for 2.0.1RC2 2017-05-20 11:45:59 +02:00
Jonathon Saine
434170862c Upgrade moment 2.10.6 -> 2.18.1 (inc locales). Corrected filename to reflect its minified.
> https://github.com/moment/moment/blob/develop/CHANGELOG.md
2017-05-20 09:51:18 +02:00
Safihre
1eb6c426fd Correctly handle already bound port with proper command-line message
Closes #921 
Closes #923
2017-05-20 00:03:57 +02:00
Safihre
e2c46d73e4 Remove unused panic message 2017-05-19 23:44:05 +02:00
Safihre
eef02ac7ce Properly handle Queue-finish-action in Glitter
Could get lost, now it sticks of first try. Closes #924
2017-05-19 23:27:20 +02:00
Safihre
0d9614755e Make DateJS bugfix more general for m/h/d etc notations 2017-05-19 23:07:42 +02:00
Safihre
aa0557656c Optional tag on dark theme not visible
Closes #905
2017-05-19 23:03:28 +02:00
Safihre
2c750f98cb Do not remove folder on re-use
This can cause issues on some file-systems, resulting in infinite loops: https://forums.sabnzbd.org/viewtopic.php?f=2&t=22637#p111875
2017-05-19 15:14:52 +02:00
Alishan Ladhani
82cf2b33cf Fix bug with DateJS
Fix DateJS bug where sometimes >12 minutes/hours/days converts into the wrong timestamp. Removing the space makes DateJS work properly.
2017-05-19 14:36:10 +02:00
Phil R
b9cfe0d6f0 URL Grabber crashes on unhandled socket exceptions
URL grabber handles `httplib.IncompleteRequest` exceptions.
This exception is not returned when the HTTP responses closes in the body, rather than the headers.
2017-05-19 13:57:33 +02:00
Safihre
a0166a4011 Correct command-line parameters 2017-04-21 15:53:24 +02:00
Safihre
c9f765813c Exit more cleanly when ports are occupied 2017-04-21 15:35:10 +02:00
Safihre
2f52590587 Update translations 2017-04-19 16:09:15 +02:00
Safihre
fd581fffa5 Prepare text-files for 2.0.1RC1 2017-04-19 15:58:43 +02:00
Safihre
fcdac1cb32 On retry, make sure we also do a check for failed recursive unpacks
Closes #900
2017-04-19 11:26:00 +02:00
Safihre
a824fa617b Proper messages in case of 7zip encryption 2017-04-19 11:05:24 +02:00
Safihre
1fd0c23e55 Apply overwrite_files() setting also to 7zip 2017-04-19 11:04:27 +02:00
Safihre
884d4ee91b Remove Special-switch prospective_par_download
It works, no need to disable anymore.

Possibly also fixes #903
2017-04-18 21:28:29 +02:00
Safihre
0c7d303568 Stop further Assembler processing after IOError 2017-04-18 21:15:49 +02:00
Safihre
a8ac8609d6 Revert "Don't use decoder for missing articles"
Turns out that it is really slow for some people

https://forums.sabnzbd.org/viewtopic.php?f=2&t=22592
2017-04-18 15:43:54 +02:00
Safihre
0d98a24c67 Only search for free port when SAB is started for first time
Closes #875. Removed 'fixed_ports' from Specials because now it's just an internal variable.
2017-04-18 13:56:21 +02:00
Safihre
31eeb5e539 Filter out tab-char (\t) for filenames on Windows
Closes #897
2017-04-18 13:29:46 +02:00
Safihre
82dacda359 Need to wait longer after restart is triggered in Config 2017-04-18 13:15:55 +02:00
Safihre
c3f82e49bf Log X-Forwarded-For in API calls and logins
Closes #884. But we don't overwrite the other found IP otherwise it might be used to remove correct logging.
2017-04-18 13:00:17 +02:00
Safihre
86a0e734b1 Avoid ZeroDivisionError in PP-script call
See #900
2017-04-18 09:00:17 +02:00
Safihre
934db752bf Restore Downloader-slowdown to more conservative value
More slowdown was causing problems for some users
2017-04-14 17:07:54 +02:00
Safihre
ed379da657 Update versioning file
On to 2.1.0!
2017-04-09 12:39:36 +02:00
Safihre
5da5f1adc1 Set version to 2.0.0 2017-04-09 12:25:05 +02:00
Safihre
f47e92dec0 Merge branch 'develop' 2017-04-09 11:59:13 +02:00
shypike
55c4bef524 Update translations 2017-04-09 10:22:25 +02:00
Safihre
ccb329160d Update text files for 2.0.0 Final (again) 2017-04-08 07:53:24 +02:00
Safihre
424626d64b Correct the SABYenc version matching 2017-04-07 08:56:45 +02:00
SanderJo
026893e10d SABYenc: explicitly define "SABYENC_VERSION = None" if none installed 2017-04-07 07:45:42 +02:00
Safihre
840b03c875 Update text files for 2.0.0RC3 2017-04-06 09:49:56 +02:00
Safihre
325f876010 Bump SABYenc to 3.0.2
Because the source-distribution on PyPi failed (missing .h file), on pypi are not allowed to replace a file for a release.
2017-04-06 09:35:22 +02:00
Safihre
91606a24b8 Correct translatable texts formatting 2017-04-06 08:55:21 +02:00
SanderJo
f001d8b749 SABYenc: Reporting of Required and Found version. Plus better var naming. 2017-04-06 08:47:20 +02:00
SanderJo
31c0c239f9 getformance.getcpu(): if no cpu info, fallback to platform.platform() 2017-04-05 17:34:34 +02:00
Safihre
918c4dbfce Save renames on join and move the join
It needs to happen first, so it doesn't get canceled
2017-04-05 14:41:56 +02:00
Safihre
f587319ef0 Only retry once after joining files 2017-04-05 13:59:01 +02:00
Safihre
b4dd942899 Fix problems with joinable files and par2cmdline
Closes #885. For some magical crazy reason, par2cmdline just doesn't scan .001 files, only all the other ones. Causing unjust verification failures.
2017-04-05 13:39:04 +02:00
Safihre
97a6720fba SABYenc bump to 3.0.1
Clumsy
2017-04-05 10:08:32 +02:00
Safihre
f05c1ef9e8 Bump SABYenc to 3.0.0
Important changes.
2017-04-05 09:35:24 +02:00
Safihre
91c1ea97fd Update display of Certificate Validation
Unfortunately requires new translations. "Default" is just not correct anymore.
2017-04-03 15:24:58 +02:00
Safihre
74faa159e7 Rename 'Default' Certificate Validation setting to 'Minimal' 2017-04-03 12:52:10 +02:00
Safihre
7e9892bb8d Prepare text files for 2.0.0 Final
@sanderjo @thezoggy @shypike: Let me know if things are missing or should be formulated better!
2017-04-02 17:43:54 +02:00
Safihre
0a9e54e5c5 Update some general text-files 2017-04-02 17:16:51 +02:00
Safihre
0f0d16a104 Update translatable texts
@shypike, I updated, the pot-file, can you send it to LP?
2017-04-02 13:25:01 +02:00
Safihre
7d7ee6ca6a When detecting cloaked job, tell this to the user 2017-04-02 12:47:46 +02:00
Safihre
0d96cd3fe8 Better handeling of filetypes-to-ignore during QuickCheck
@thezoggy Closes #880
2017-04-02 12:05:33 +02:00
Safihre
596244543c Config - Specials did not show default-values for lists
Linked to #880
2017-04-02 11:20:50 +02:00
Safihre
237d6b9414 Revert "Bump SABYenc to 2.9.0"
Staying with 2.8.0 for compatibility untill we can do another SABnzbd release.
2017-03-31 15:12:43 +02:00
Safihre
a7c42779f8 Bump SABYenc to 2.9.0 2017-03-31 13:50:04 +02:00
Safihre
c49e5f2054 Also remove IPv4/6 address from logs 2017-03-31 10:54:10 +02:00
Safihre
6ca6037aa0 Don't sort when loading Queue from disk on start-up 2017-03-31 09:57:22 +02:00
Safihre
cc7f360c04 Only trigger Script change in Glitter when there are scripts 2017-03-31 09:39:54 +02:00
Safihre
75991bffea Stray unicode in CRC should not crash the decoder
This actually happened..
2017-03-30 15:11:59 +02:00
Safihre
afd1b1968c Update text files for 2.0.0RC2 2017-03-29 17:22:27 +02:00
Safihre
746e9d2a6d Bump required SABYenc 2017-03-29 16:46:40 +02:00
Safihre
36b5b5d0f3 RarFile needs path to unrar before starting 2017-03-28 21:34:39 +02:00
Safihre
dd603cfcc8 Non-SABYenc situations could result in crashes 2017-03-28 17:00:35 +02:00
Safihre
cefce9913a QuickCheck would fail renaming when file does not exist
For example if par2 already renamed it before
2017-03-28 16:44:33 +02:00
Safihre
0b29f27fcd Use popen instead of system for diskspeed initialization on Windows
In the binary otherwise a command window pops up
2017-03-23 16:18:47 +01:00
Safihre
f3f3e27bfe Move dot-stripping to last step of sanatizing 2017-03-22 17:20:33 +01:00
Safihre
519c44a72a skip_dashboard=1 default for fullstatus API-call 2017-03-22 11:35:24 +01:00
Safihre
7e28da0530 Set a develop-version to distinguish which develop users are on
Closes #872
2017-03-22 09:13:30 +01:00
Jonathon Saine
af70d98b50 Fixes: #860
Handle: `SSLError: [SSL: HTTPS_PROXY_REQUEST] https proxy request (_ssl.c:661)`
2017-03-21 12:53:16 +01:00
Safihre
3f0b84ea22 Update text files for 2.0.0RC1 2017-03-20 22:33:01 +01:00
sanderjo
e58abd45ec newsunpack: unrar says "too large" => give error 2017-03-20 21:55:49 +01:00
Safihre
abeee263f0 Use constants.REC_RAR_VERSION in all checks 2017-03-20 15:10:44 +01:00
Safihre
30f68bd7b9 Warning about bad Unrar was never shown in Glitter
Now it's a proper warning
2017-03-20 15:04:03 +01:00
Safihre
f13394d27f Change Error to Warning for missing SABYenc
In preperation of RC1
2017-03-19 16:59:36 +01:00
shypike
88e0617429 Update translatable texts 2017-03-18 22:34:29 +01:00
shypike
ab94ffc055 Update translatable texts 2017-03-18 22:13:56 +01:00
shypike
265ab99cc7 Remove unused languges 2017-03-18 21:12:53 +01:00
shypike
738adbe38e Update translation 2017-03-18 21:09:36 +01:00
Safihre
a894ca5171 Set version to 1.2.3 2017-03-18 13:05:04 +01:00
Safihre
5abe1140ae Merge branch '1.2.x' 2017-03-18 13:03:36 +01:00
Safihre
4e6862cef9 Chain certificates not loaded in all situations of extra servers 2017-03-18 12:23:48 +01:00
Safihre
7aff60b24d CherryPy would fallback to (non-existing) pyopenssl for extra servers
Causing failed starts
2017-03-18 12:23:12 +01:00
Safihre
d34e14370c Chain certificates not loaded in all situations of extra servers 2017-03-18 11:43:46 +01:00
Safihre
c4f4a3131c Update 1.2.3 text files for extra bugfix 2017-03-18 11:40:57 +01:00
Safihre
dcbd9b57f3 CherryPy would fallback to (non-existing) pyopenssl for extra servers
Causing failed starts
2017-03-18 11:39:40 +01:00
Jonathon Saine
8819e38073 We only used 1 line from listquote, just copied over that line to eliminate the need for the superfluously import. 2017-03-18 11:10:44 +01:00
Safihre
aad3b54a17 Update Text Files for 1.2.3
These patches were already tested within 2.0.0Alpha/Beta, so considerd safe.
2017-03-18 11:04:57 +01:00
Safihre
cde142a371 Windows paths can't end in a dot 2017-03-18 11:02:14 +01:00
Safihre
8bfc98ffc6 Glitter server unblock button wasn't working
Closes #864
2017-03-18 11:01:51 +01:00
Jonathon Saine
e46f21d566 Fix -w option as it no longer needs arguments. 2017-03-18 11:00:46 +01:00
Safihre
0e45fdcdfd Fix BPS manager breaking the downloader due to Quota 2017-03-18 10:28:31 +01:00
Safihre
eec7af16d7 Improve log obfuscation
It needs to match any charachters not just alpha-numeric
2017-03-18 10:28:03 +01:00
Safihre
6532425902 Kill the UnRar after a Windows failure
On Windows Server it seems unrar otherwise won't stop: https://forums.sabnzbd.org/viewtopic.php?f=3&t=22492
2017-03-18 10:27:57 +01:00
Safihre
44b896522c Unzip needs clipped paths
(7zip doesn't)
2017-03-18 10:26:32 +01:00
Safihre
2bbac91436 Fix BPS manager breaking the downloader due to Quota 2017-03-18 09:41:01 +01:00
Safihre
cf4dea432b Fix SMPL after reverting qstatus 2017-03-17 22:47:55 +01:00
Safihre
bd70df1f05 Remove non queue/history info from those API calls 2017-03-17 22:29:10 +01:00
Safihre
b2638c1fac Make API call qstatus identical to queue call 2017-03-17 21:39:24 +01:00
Safihre
4aa9409f5d Revert "Remove API-call 'qstatus'"
CouchPotato also uses it and doesn't get updated.
2017-03-17 21:35:38 +01:00
Safihre
dfa863a54a Reame cat_list > categories and script_list > scripts 2017-03-17 20:40:40 +01:00
Safihre
626c04df48 Don't need password parameter 2017-03-17 20:19:17 +01:00
Safihre
6026fa57f0 Remember password in history
Closes #855
2017-03-17 20:17:45 +01:00
shypike
0db28fb5e2 Add new column to database.
Bump version to 2.
Add "password" column in new database.
Insert "password" column in existing databases.
2017-03-17 19:34:13 +01:00
Safihre
c9bf8ced99 Remove unused function format_history_for_queue 2017-03-17 16:58:11 +01:00
Safihre
18a55db245 Remove undocumented 'sort' and 'trans' options from queue API call 2017-03-17 16:30:44 +01:00
Jonathon Saine
efb9664761 Update windows section to use win32api 2017-03-17 15:09:09 +01:00
Safihre
3ee412c7a5 Update translatable texts 2017-03-17 13:22:18 +01:00
Safihre
5afc00a502 Remove API-call 'qstatus'
SMPL specifc thing that can also be done from data in regular queue call. To keep SMPL working.
2017-03-16 22:09:33 +01:00
Safihre
e5ca0e6415 Remove legacy detection when not to show API warnings 2017-03-16 22:09:33 +01:00
SanderJo
98f121258c Everything in a try/except as we're dealing with OS calls. 2017-03-16 22:06:47 +01:00
Safihre
b2474c51fd Glitter server unblock button wasn't working
Closes #864
2017-03-16 14:19:37 +01:00
Jonathon Saine
1bd6ebdb41 Fix -w option as it no longer needs arguments. 2017-03-16 08:24:36 +01:00
SanderJo
20ef99326d Check if Completed Download Folder is on FAT. If so, give a warning 2017-03-15 08:58:58 +01:00
Safihre
171a1b9ae3 Do not handle first article seperate
Was build to detect filename early on, but we can also do this later. This way not everything gets loaded into memory right away. Part of #535
2017-03-14 16:48:27 +01:00
Safihre
cd2c9d151a Correctly check for NOT is_gone when deleting
Oops
2017-03-14 16:41:26 +01:00
Safihre
8fbfe9a76a Always provide nzf_id to get_files
Otherwise we can loose track of which file we are talking about. Especially because nowadays files are obfuscated on article-level.
2017-03-14 16:16:56 +01:00
Safihre
63cf0d4f97 Top-only now really really only downloads the top job 2017-03-14 13:55:20 +01:00
Safihre
faa98126f8 More general multicore-par2 detection 2017-03-11 20:24:02 +01:00
Safihre
0dd70249b9 Windows paths can't end in a dot 2017-03-10 16:48:09 +01:00
Safihre
aadd99cac3 Update text files for 2.0.0Beta1 2017-03-10 15:16:11 +01:00
Safihre
5bede842ba Avoid double checks for saving SABnzbd password 2017-03-09 17:37:45 +01:00
Safihre
1f2ac77b5e SABnzbd password should show stars
Closes #851
2017-03-09 17:29:27 +01:00
Jonathon Saine
3f4f35c6d1 More PEP8 cleanup, spelling fixes in comments and reserved word changes, and make IDE ignore debug asserts. 2017-03-09 16:22:30 +01:00
Jonathon Saine
9575ddbdb4 PEP8 cleanup -- compare instance not type 2017-03-09 16:22:25 +01:00
Jonathon Saine
3ed918d98d PEP8 related cleanup (most whitespace) 2017-03-09 16:22:18 +01:00
Safihre
89d5af3372 Correct typo (Successful) 2017-03-09 15:55:24 +01:00
Safihre
fa8f40eee3 Detect multicore par2 and show link how to install
Now also available in @jcfp's new PPA (both MT and TBB) versions. Installing MT is also very easy on other platforms. For example SynoCommunity also uses it for their packages.
2017-03-08 18:24:48 +01:00
Safihre
8f06035500 Log succes and failure of logins
Might consider adding a special later to stop the warnings... https://forums.sabnzbd.org/viewtopic.php?f=2&t=22494
2017-03-08 16:59:21 +01:00
Safihre
8f7d969099 Do the Restart async to allow finishing of request to browser
For Shutdown this does not seem to be required
#843
2017-03-08 16:19:13 +01:00
Safihre
5422785feb Improve log obfuscation
It needs to match any charachters not just alpha-numeric
2017-03-08 15:26:08 +01:00
Safihre
911f82c00b Kill the UnRar after a Windows failure
On Windows Server it seems unrar otherwise won't stop: https://forums.sabnzbd.org/viewtopic.php?f=3&t=22492
2017-03-08 15:16:16 +01:00
Safihre
fbff8c991b Update translatable texts
@shypike Can you push to Launchpad? :)
2017-03-07 17:29:29 +01:00
Safihre
f1b139d55d Make extract_pot also work on Windows 2017-03-07 17:25:03 +01:00
Safihre
9c7f196e20 Incorrect detection of servers with same priority 2017-03-07 16:13:28 +01:00
Safihre
4d8a37006e CRC error is not the same as unknown encoding
With SABYenc it would report single-article files with CRC errors as 'Warning: unknown encoding'
2017-03-07 15:45:16 +01:00
Safihre
bc9d3d561f Missed to convert one Raiser() in interface 2017-03-07 15:02:54 +01:00
Safihre
fd9e80bdf5 CherryPy 8.1.2 - Also catch 'unknown error' from SSL
@sanderjo I forgot!
2017-03-04 16:50:12 +01:00
Safihre
aee2f71170 Log where a restart was triggered 2017-03-04 15:51:42 +01:00
Safihre
102160d651 Don't add prospective par2 on Pre-check 2017-03-03 23:08:34 +01:00
Safihre
e33f26d33c Log which server has the article
(in the hopes to debug the Check before download-feature)
2017-03-03 22:43:45 +01:00
Safihre
57113fa02f Check before download were decoded twice 2017-03-03 22:24:25 +01:00
Safihre
2be7575f98 Only show Multi-edit-Search when there is a Queue 2017-03-03 14:01:38 +01:00
Safihre
c5647b46e1 Show Queue search when Multi-editing
#842
2017-03-03 13:51:08 +01:00
Safihre
3450cff92f Unzip needs clipped paths
(7zip doesn't)
2017-03-02 15:41:40 +01:00
Safihre
1b16ee44cb Set version to 1.2.2 2017-02-28 11:17:24 +01:00
Safihre
d5f608c28c Merged branch 1.2.x into master 2017-02-28 11:16:59 +01:00
Safihre
13a11411a9 Update README 2017-02-26 20:20:44 +01:00
Safihre
6e7eb9dec4 Small Changelog typos 2017-02-26 12:37:52 +01:00
Safihre
c74eed7c0e Add new PP-script feature to Changelog
Url only for the Alpha, later information will be on Wiki
2017-02-26 12:31:50 +01:00
Safihre
2fc6811495 Remove enabled_sabyenc
Too much was intertwined. If users want to experiment with/without SAByenc they can download seperate releases.
2017-02-26 12:23:25 +01:00
Safihre
57e0dac45b If no SABYenc or _yenc, give ERROR
Performance of Python-yEnc is lower in 2.0.0, so people need to fix things
2017-02-26 12:23:24 +01:00
Safihre
537e31000e Python-yEnc cannot handle \n or \r in data
_yenc and SABYenc ignores them
2017-02-26 12:23:24 +01:00
Safihre
555d8418e7 Update text files for 1.2.2 2017-02-25 22:12:34 +01:00
Safihre
8c22e35da4 Script paths were not clipped correctly 2017-02-25 22:07:25 +01:00
Safihre
e0872f4536 Convert python types in environment fields
True/False=1/0 and None=''
2017-02-25 21:46:26 +01:00
Safihre
9dddf6dd2e Make more information available to PP-scripts via enviroment variables
Closes #785
2017-02-25 21:22:11 +01:00
Safihre
7a0c5feed3 Update text files for 2.0.0Alpha1 2017-02-25 14:21:33 +01:00
Safihre
42d154f0b7 Accidentally deleted StringIO import 2017-02-25 14:02:21 +01:00
Safihre
44b0ab2203 Remove macOS IOPolicy throttling
#830. Currently it's set to IOPOL_THROTTLE, meaning lowest priority. 
Have to await feedback from users, potentially we can create a Special option for it.
2017-02-25 13:39:48 +01:00
Safihre
4af59b50ad Update translatable texts 2017-02-25 13:28:45 +01:00
SanderJo
b309099f0b Warning in case of non-UTF-8 setting on Linux/Unix 2017-02-25 13:27:49 +01:00
Safihre
affea99cb1 Retry pickle-saving 3x
Especially on Windows the file sometimes gets locked, and users have reported 'dictionary changed size during iteration' errors, so somewhere a Lock is missing.
2017-02-25 13:11:45 +01:00
Safihre
9bc35a3026 Always use hashlib
Part of Python standard library now
2017-02-25 13:02:54 +01:00
Safihre
57e9d499fb Slowdown downloader more
Decoder doesn't use GIL, so all power now goes to the Downloader that loops too much
2017-02-24 17:36:06 +01:00
Safihre
95a7924b31 Set version to 1.2.1 and fix small discrepancies 2017-02-24 11:22:55 +01:00
Safihre
5830bebd95 Merge branch '1.2.x' 2017-02-24 11:16:18 +01:00
shypike
d32cf57c75 Update translations 2017-02-24 10:03:07 +01:00
shypike
d53cf598a4 Update translations 2017-02-23 19:55:51 +01:00
Safihre
6d9242ebc5 Update text files for 1.2.1 Final 2017-02-23 11:53:56 +01:00
Safihre
bdaca2bd37 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:22 +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
53e3af9b30 Update changelog for Script changes 2017-02-21 13:31:18 +01:00
Safihre
9cfef895f8 Don't shorten, but clip paths given to external scripts
Windows-only: Either we give long-path notation paths (\\?\) or we just give the path. Either way, user scripts have to be modified.
2017-02-20 16:12:11 +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
56fa9644a5 Forced diskspace check seperate from cached one
Closes #826. Otherwise the complete/incomplete get out of sync.
2017-02-20 15:35:16 +01:00
Safihre
53a219f12b Tooltip for Download/Incomplete in Status Windows
Closes #827
2017-02-20 09:03:45 +01:00
Safihre
8eff51a96b Tooltip for Download/Incomplete in Status Windows
Closes #827
2017-02-20 09:03:24 +01:00
Safihre
d130a1d44a Update Wiki help links for 2.0.0 2017-02-19 17:17:04 +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
98316fd282 Correct mistake in Diskspace calculation for Unix 2017-02-19 12:36:36 +01:00
Safihre
59f1ea3073 Diskspace for Complete folder was not calculated 2017-02-19 11:47:11 +01:00
Safihre
270757f3bd Consistently check for cfg.use_pickle() 2017-02-19 10:20:13 +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
843c6b36a8 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:45:03 +01:00
Safihre
f71a2a8fc2 Use pythonic-style for data reading/writing 2017-02-18 23:38:39 +01:00
Safihre
63fc763958 Don't use decoder for missing articles
Don't occupy the decoder-queue with all these empty articles, but handle them directly.
Requires less CPU.
2017-02-18 22:52:42 +01:00
Safihre
9bc0aac63d CherryPy 8.1.2 - Use relative HTTPredirects instead
CherryPy is using the oudated redirects with full URL, but that's not needed anymore and is much more flexable when used behind a proxy.
https://github.com/cherrypy/cherrypy/issues/1355#issuecomment-270669958
2017-02-18 22:52:42 +01:00
Safihre
ff529da874 Update Readme for changeset 2017-02-18 22:52:42 +01:00
Safihre
ff40944f00 Disable schedule items without deleting them
Requires conversion of the schedules!
2017-02-18 22:52:42 +01:00
Safihre
1d0ac46c7e More correct handeling of CherryPy logging
Use CherryPy's options to handle the logging.
2017-02-18 22:52:42 +01:00
Safihre
d62bb1e5b6 Move overwrite_files to Specials
Very special option, only usuefull if you dump all output in 1 directory. But this rarely happens.
2017-02-18 22:52:42 +01:00
Safihre
89f91e46b7 Add Certificate Validation option to Wizard
Otherwise a user would be stuck.
2017-02-18 22:52:42 +01:00
Safihre
bc2daa5f8b Enabled Hostname verification by Default
We will try this and see the feedback if users understand. We have improved the linked page with information for users: https://sabnzbd.org/certificate-errors
2017-02-18 22:52:42 +01:00
Safihre
0fbf240a58 Simplify CherryPy logging
No filter required anymore, CherryPy provides it's own config switches for it.
2017-02-18 22:52:42 +01:00
Safihre
83d57b33f7 Rewrite Raisers in Interface.py
Raisers don't need extra information anymore, there is only 1 type of redirect to be used.
2017-02-18 22:52:42 +01:00
Safihre
59ae23e315 Correct redirect for each situation
Some redirects were not performed correctly in the new situation.
2017-02-18 22:52:42 +01:00
Safihre
df26adfe89 Accidentally removed SSL-choice from Wizard 2017-02-18 22:52:42 +01:00
Safihre
d1a92aeb36 Restructure interface.py
We now only have 1 directory that has all the template files, so the directory does not have to be a variable for each page anymore.
There should be caution when doing redirects, to make sure the correct /sabnzbd/ or just / is used.
2017-02-18 22:52:42 +01:00
Safihre
c09f1b2f1c Move rating_host back to Specials
Possibly to be deprecated later on. Indexers should supply this info via the headers: https://sabnzbd.org/wiki/extra/indexer-feedback
Indexers like to use an URL, instead of just providen a host.
2017-02-18 22:52:42 +01:00
Safihre
a70a1e6290 Assume Rating feedback if Indexer integration enabled
With the changes in 1.1.0 we now have more indexers in the wild actually use the system, by sending the headers or using the fields in the NZB.
But nowadays very few files have actual audio/video ratings of users, so the main purpose of this feature has evolved to automatically report back if the download failed on the server.
The text is also updated as such, that enabling it will send to the indexer if a download failed, only if it failed.
2017-02-18 22:52:42 +01:00
Safihre
dde5258b59 Move 'Use tags from indexer' to Specials
The user should always use this, there is no bad thing about it.
This cleans up the Switches page a bit. Eventually the option can just be completly removed.
2017-02-18 22:52:42 +01:00
Safihre
dc438e6eb7 Remove Secondary Skin option
Major cleanup, now doesn't need any special links to file locations anymore.
Closes #778
2017-02-18 22:52:42 +01:00
Safihre
37a9a97f4f Move QuickCheck to Specials
Closes #788. It's been tested, it works, nobody should not use it.
2017-02-18 22:52:42 +01:00
Safihre
4fb6e3fe7b Remove check for ssl module
Any Python setup has it, it was only introduced to check for PyOpenSSL which is now dropped.
2017-02-18 22:52:42 +01:00
Safihre
7884848c78 Don't run PyStone for every Status Window update
Only diagnostic info, no need to refresh every time and blocks CPU when window is refreshed during downloading.
2017-02-18 17:54:12 +01:00
Safihre
2ece328e50 Update Cache-info in Status-window with Queue
Info is already in the API-call for queue, so can be updated with same frequency
2017-02-18 17:30:06 +01:00
Safihre
b8ac3cd22f Catch bad article from SABYenc 2017-02-18 00:56:40 +01:00
Safihre
9b4bd7a3f0 Only warn about _yenc when no SABYenc 2017-02-18 00:56:40 +01:00
Safihre
facefc5c58 Option to disable SABYenc didn't actually work
But now it does!
2017-02-18 00:56:40 +01:00
Safihre
5d6a6b1af7 Show SABYencversion in log 2017-02-18 00:56:40 +01:00
Safihre
916e0ead99 Ignore non-essential files during QuickCheck 2017-02-18 00:56:40 +01:00
Safihre
aff7b07f33 Make sure the error-code is logged 2017-02-18 00:56:40 +01:00
Safihre
8267b429ca Some cleanup of SABYenc code 2017-02-18 00:56:40 +01:00
Safihre
5759bee1df Small changes to Decoder 2017-02-18 00:56:40 +01:00
Safihre
f2648ec85c Require SABYenc 2.7.0 2017-02-18 00:56:40 +01:00
Safihre
e083722f0b Make sure to do DMCA/Pre-Check checks in right data 2017-02-18 00:56:40 +01:00
Safihre
f03e63fa54 Make sure Decoder's get shutdown 2017-02-18 00:56:40 +01:00
Safihre
f33c3e30eb Robust detection of end-of-article 2017-02-18 00:56:40 +01:00
Safihre
c9ee0b0fcb Check for specific SABYenc version 2017-02-18 00:56:40 +01:00
Safihre
7aaa8036bc Setup multiple Decoders 2017-02-18 00:56:40 +01:00
Safihre
00f2410d2d Show SABYenc detection at startup and in Config 2017-02-18 00:56:40 +01:00
Safihre
be77a494db Correctly handle end-of-article 2017-02-18 00:56:40 +01:00
Safihre
787a95bdd2 Reduce timeouts for decoder/assembler switches 2017-02-18 00:56:40 +01:00
Safihre
720ce591b7 First work on SABYenc integration 2017-02-18 00:56:40 +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
494e72a996 Update to 1.2.0 and set version 2017-01-13 17:00:51 +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
Safihre
84cc86f1d3 Update to 1.1.1 and set version (#731) 2016-11-11 13:58:07 +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
64479e2e5d Correct the merge of release 1.1.0 to the "master" branch. 2016-09-14 08:26:02 +02:00
shypike
13b523d9bd Merge pull request #681 from Safihre/master
Master to 1.1.0
2016-09-13 16:25:05 +02:00
Safihre
181881a21b Merge branch 'develop' 2016-09-11 22:16:42 +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
86d11095ac Merge pull request #592 from sabnzbd/1.0.x
Update master to 1.0.3
2016-06-10 19:01:03 +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
927ba3cd9d Update text files for 1.0.3 2016-06-04 13:34:11 +02:00
Safihre
6296fc1762 #568 Add code 482 to check for too-many-connections 2016-06-03 23:24:07 +02:00
shypike
60fbe44724 Support X-DNZB-PASSWORD header. 2016-06-03 22:18:56 +02:00
shypike
29e45da431 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 22:07:44 +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
d82e69eef4 Handle checksum error reports from unrar. 2016-06-02 23:19:57 +02:00
shypike
8c7d557252 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.
2016-06-02 22:11:59 +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
shypike
a56d6e5517 Merge pull request #577 from sabnzbd/1.0.x
1.0.x to master
2016-05-23 18:11:23 +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
shypike
7548d9e975 Correct base version number for 1.0.x releases. 2016-05-10 14:43:14 +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
fc7e87f0df Merge branch '1.0.x' 2016-03-19 11:49:11 +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
e51f4fc45a Merge pull request #181 from discordianfish/add-dockerfile
Add Dockerfile
2014-10-08 21:38:15 +02:00
Johannes 'fish' Ziemke
65278120e2 Add Dockerfile
Usage:

        docker build -t sabnzbd .
        docker run -p 127.0.0.1:8080:8080 sabnzbd
2014-09-24 13:46:43 +02:00
shypike
2eed355e9c Revert "newsunpack.py: also handle output of unrar5 with password protected files"
This reverts commit 12fd63c1cf.
2013-11-11 20:24:11 +01:00
shypike
018955f4d5 Merge pull request #122 from sanderjo/patch-4
newsunpack.py: also handle output of unrar5 with password protected file...
2013-11-11 11:19:20 -08:00
sanderjo
12fd63c1cf newsunpack.py: also handle output of unrar5 with password protected files
as discussed http://forums.sabnzbd.org/viewtopic.php?f=3&t=16166&p=89054#p89054
2013-11-07 17:02:24 +01:00
301 changed files with 47993 additions and 43845 deletions

9
.gitignore vendored
View File

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

View File

@@ -1,5 +1,5 @@
*******************************************
*** This is SABnzbd 1.0.2 ***
*** This is SABnzbd 2.0.1 ***
*******************************************
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
@@ -10,33 +10,19 @@ 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 1.0.0:
http://wiki.sabnzbd.org/introducing-1-0-0
https://sabnzbd.org/wiki/
Please also read the file "ISSUES.txt"
*******************************************
*** Upgrading from 0.7.x or 0.6.x ***
*******************************************
Empty your current queue
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.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,5 +1,5 @@
(c) Copyright 2007-2016 by "The SABnzbd-team" <team@sabnzbd.org>
(c) Copyright 2007-2017 by "The SABnzbd-team" <team@sabnzbd.org>
The SABnzbd-team is:

View File

@@ -1,18 +0,0 @@
FROM ubuntu:14.04
MAINTAINER Johannes 'fish' Ziemke <docker@freigeist.org> @discordianfish
RUN echo deb http://archive.ubuntu.com/ubuntu/ trusty multiverse >> \
/etc/apt/sources.list
RUN apt-get -qy update && apt-get -qy install python python-cheetah unrar \
unzip python-yenc par2
RUN useradd sabnzbd -d /sab -m && chown -R sabnzbd:sabnzbd /sab
VOLUME /sab
ADD . /sabnzbd
EXPOSE 8080
USER sabnzbd
ENV HOME /sab
ENTRYPOINT [ "python", "/sabnzbd/SABnzbd.py", "-s", "0.0.0.0:8080" ]

View File

@@ -1,10 +1,10 @@
SABnzbd 1.0.2
SABnzbd 2.0.1
-------------------------------------------------------------------------------
0) LICENSE
-------------------------------------------------------------------------------
(c) Copyright 2007-2016 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
@@ -21,7 +21,7 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-------------------------------------------------------------------------------
1) INSTALL with the Win32 installer
1) INSTALL with the Windows installer
-------------------------------------------------------------------------------
Just run the downloaded EXE file and the installer will start.
It's just a simple standard installer.
@@ -32,19 +32,20 @@ Use the "Help" button in the web-interface to be directed to the Help Wiki.
-------------------------------------------------------------------------------
2) INSTALL pre-built Win32 binaries
2) INSTALL pre-built Windows binaries
-------------------------------------------------------------------------------
Unzip pre-built version to any folder of your liking.
Start the SABnzbd.exe program.
Within 5-10 seconds your web browser will start and show the user interface.
Use the "Help" button in the web-interface to be directed to the Help Wiki.
-------------------------------------------------------------------------------
3) INSTALL pre-built OSX binaries
3) INSTALL pre-built macOS 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.
Make sure you pick the right folder, depending on your macOS version.
-------------------------------------------------------------------------------
@@ -54,45 +55,36 @@ Make sure you pick the right folder, depending on your OSX version.
You need to have Python installed plus some non-standard Python modules
and a few tools.
Unix/Linux/OSX
Python-2.6 or 2.7 http://www.python.org
OSX Leopard/SnowLeopard
Python 2.6 http://www.activestate.com
OSX Lion/MountainLion
Apple Python 2.7 Included in OSX (default)
Unix/Linux/macOS
Python-2.7.latest http://www.python.org (2.7.9+ recommended)
Windows
Python-2.7.latest http://www.python.com
Most versions will work: 2.7.10 recommended
Python-2.7.latest http://www.python.org (2.7.9+ recommended)
PyWin32 use "pip install pypiwin32"
Essential modules
cheetah-2.0.1+ use "pip install cheetah"
par2cmdline >= 0.4 http://parchive.sourceforge.net/
Note: https://sabnzbd.org/wiki/configuration/2.0/switches#par2cmdline
And: https://sabnzbd.org/wiki/installation/multicore-par2
unrar >= 5.00+ http://www.rarlab.com/rar_add.htm
Optional modules
unzip >= 5.52 http://www.info-zip.org/
unzip >= 6.00 http://www.info-zip.org/
7zip >= 9.20 http://www.7zip.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)
sabyenc == 3.0.2 use "pip install sabyenc" - https://sabnzbd.org/sabyenc
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 Windows
pyopenssl >= 0.13 "pip install pyopenssl"
or http://www.egenix.com/products/python/pyOpenSSL/
(Binaries, including the OpenSSL libraries)
Optional modules Unix/Linux/OSX
pyopenssl >= 0.15 http://pypi.python.org/pypi/pyOpenSSL
openssl => ? http://www.openssl.org/
Make sure the OpenSSL libraries match with PyOpenSSL
Optional modules Unix/Linux/macOS
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 (preferably use the included version)
CherryPy-3.8.0 with patches http://www.cherrypy.org
CherryPy-8.1.2 with patches http://www.cherrypy.org
Unpack the ZIP-file containing the SABnzbd sources to any folder of your liking.
@@ -102,6 +94,7 @@ Start this from a shell terminal (or command prompt):
Start this from a shell terminal (or command prompt):
python SABnzbd.py
Within 5-10 seconds your web browser will start and show the user interface.
Use the "Help" button in the web-interface to be directed to the Help Wiki.
@@ -129,7 +122,7 @@ may help you solve problems easier.
-------------------------------------------------------------------------------
Visit the WIKI site:
http://wiki.sabnzbd.org/
https://sabnzbd.org/wiki/
-------------------------------------------------------------------------------
@@ -138,4 +131,4 @@ Visit the WIKI site:
Several parts of SABnzbd were built by other people, illustrating the
wonderful world of Free Open Source Software.
See the licenses folder of the main program and of the skin folders.
See the licenses folder of the main program and of the skin folders.

View File

@@ -12,7 +12,7 @@
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 firewalls.
@@ -24,13 +24,13 @@
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-1-0
See: https://sabnzbd.org/wiki/configuration/2.0/special
- Some third-party utilties try to probe SABnzbd API in such a way that you will
often see warnings about unauthenticated access.
If you are sure these probes are harmless, you can suppress the warnings by
setting the option "api_warnings" to 0.
See: http://wiki.sabnzbd.org/configure-special-1-0
See: https://sabnzbd.org/wiki/configuration/2.0/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.
@@ -41,7 +41,7 @@
You will see this only when downloaded files contain accented characters.
You need to fix it yourself by running the convmv utility (available for most Linux platforms).
Possible the file system override setting 'fsys_type' might be solve things:
See: http://wiki.sabnzbd.org/configure-special-1-0
See: https://sabnzbd.org/wiki/configuration/2.0/special
- The "Watched Folder" sometimes fails to delete the NZB files it has
processed. This happens when other software still accesses these files.
@@ -81,4 +81,4 @@
- Squeeze Linux
There is a "special" option that will allow you to select an alternative library.
use_pickle = 1
See: http://wiki.sabnzbd.org/configure-special-1-0
See: https://sabnzbd.org/wiki/configuration/2.0/special

View File

@@ -1,4 +1,4 @@
(c) Copyright 2007-2016 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,7 +1,7 @@
Metadata-Version: 1.0
Name: SABnzbd
Version: 1.0.2
Summary: SABnzbd-1.0.2
Version: 2.0.1
Summary: SABnzbd-2.0.1
Home-page: http://sabnzbd.org
Author: The SABnzbd Team
Author-email: team@sabnzbd.org

View File

@@ -1,37 +1,28 @@
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.
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:
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, then you likely already have all the needed dependencies. If not, here's what you're looking for:
- `python` (We support Python 2.6 and 2.7)
- `python` (only 2.7.x and higher, but not 3.x.x)
- `python-cheetah`
- `python-dbus`
- `python-openssl`
- `python-support`
- `python-yenc`
- `par2` (Multi-threaded par2 installation guide can be found [here](https://forums.sabnzbd.org/viewtopic.php?f=16&t=18793#p99702) )
- `par2` (Multi-threaded par2 installation guide can be found [here](https://sabnzbd.org/wiki/installation/multicore-par2))
- `unrar` (Make sure you get the "official" non-free version of unrar)
- `unzip`
- `sabyenc` (installation guide can be found [here](https://sabnzbd.org/sabyenc))
Optional:
- `python-cryptography` (enables certificate generation and detection of encrypted RAR-files during download)
- `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).
@@ -40,13 +31,13 @@ Your package manager should supply these. If not, we've got links in our more in
Once you've sorted out all the dependencies, simply run:
```
python SABnzbd.py
python -OO SABnzbd.py
```
Or, if you want to run in the background:
```
python SABnzbd.py -d -f /path/to/sabnzbd.ini
python -OO SABnzbd.py -d -f /path/to/sabnzbd.ini
```
If you want multi-language support, run:
@@ -55,8 +46,23 @@ If you want multi-language support, run:
python tools/make_mo.py
```
Our many other command line options are explained in depth [here](http://wiki.sabnzbd.org/command-line-parameters).
Our many other command line options are explained in depth [here](https://sabnzbd.org/wiki/advanced/command-line-parameters).
## About Our Repo
We're going to be attempting to follow the [gitflow model](http://nvie.com/posts/a-successful-git-branching-model/), so you can consider "master" to be whatever our present stable release build is (presently 0.6.x) and "develop" to be whatever our next build will be (presently 0.7.x). Once we transition from unstable to stable dev builds we'll create release branches, and encourage you to follow along and help us test.
The workflow we use, is a simplified form of "GitFlow".
Basically:
- `master` contains only stable releases (which have been merged to `master`) and is intended for end-users.
- `develop` is the target for integration and is **not** intended for end-users.
- `1.1.x` is a release and maintenance branch for 1.1.x (1.1.0 -> 1.1.1 -> 1.1.2) and is **not** intended for end-users.
- `feature/my_feature` is a temporary feature branch based on `develop`.
- `bugfix/my_bugfix` 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.

View File

@@ -1,78 +1,56 @@
Release Notes - SABnzbd 1.0.2
===============================
Release Notes - SABnzbd 2.0.1
=========================================================
## Bugfixes in 1.0.2
- Fix hangups at 100% when QuickCheck is off and "all-pars" is on
- Fix handling of "too many connections" for some Usenet servers
## Changes since 2.0.0
- No longer change ports if the configured port is not available during startup.
- 'Proof' files also ignored when Ignore Samples is enabled.
- Redundant SFV and RAR checks no longer performed if par2 verification failed.
- More repair/unpack info is retained in the History information.
- Windows: remove option to start SABnzbd from installer, it would start the
program as administrator.
## Bugfixes since 2.0.0
- Some users experienced slower download speeds.
- Revert changes to handling of missing articles introduced in 2.0.0 that
turned out to be slow for some users.
- Missing first par2-file would cause repair to be skipped.
- Better handling of 7zip unpacking.
- Log X-Forwarded-For of API calls and logins.
- Handle more URLGrabber exceptions.
- Command-line parameters were not listed correctly.
- Queue-finish-action picker in Glitter more stable.
- Custom Pause interpreter in Glitter more reliable.
- Pre-check would fail if download was on SMBv3 share.
- Windows: NZB-icon association lost sometimes.
- Windows: Unrar could fail on some archives containing very long paths.
## What's new in 1.0.1
- Prevent creating orphan items in "incomplete" when deleting downloading jobs.
- Forced item with missing articles caused overflow into paused jobs
- Do QuickCheck even on files that would be removed by the Cleanup-list (problematic for RAR files).
- Fix "Download all par2 files" behavior
- Treat ambiguous numeric values as number of minutes for custom pause time.
- Accept MIME records that have only LF line endings (error in some third-party utilities)
- Fix PushOver support.
- Fix breaking Glitter bug with large script_log
- Fix issues with deleting jobs via the API
- Fix issue where Sonarr could not read using the History-API
- Increase default cache to 450M
- The pre-queue script can now return an accept value of 2, meaning immediate failure. (Useful for Sonarr.)
- Add start script for portable Windows installations
## What's new in 1.0.0
- Full Unicode support with Chinese and Russian translations
- New default UI: Glitter
- Server priorities instead of primary/backup ==> REVIEW YOUR SERVER SETTINGS!
- Newsserver IPv6 load balancing aka Happy Eyeballs / RFC 6555
- Duplicate detection for series
- More filters in RSS
- 7zip support
- Option to save repair time by downloading all par2 files
- Support for long paths in Windows (above 260)
- Improved security for external access
- Lots of small improvements and bug fixes
- Redesign of notifications classes
- More notification services supported
- Diagnostic dashboard tab for "Status" page
- Bonjour/ZeroConfig support
## Remarks
- SABnzbd's webserver now doesn't listen to IPv6 addresses by default.
- Use Config->Special->ipv6_hosting if you want this enabled.
- "localhost" will be replaced with "127.0.0.1", check any browser bookmark and third-party tool
- Classic skin has been removed
- Support extra parameters for par2 on other platforms than Windows
- Option to verify HTTPS connections (default off)
- Auto-negotiates best Usenet ssl protocol (override possible)
- When upgrading from 0.7.x, a backup server will get priority 1
## 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-2016 by "The SABnzbd-team" \<team@sabnzbd.org\>
### IMPORTANT INFORMATION about release 1.0.0
<http://wiki.sabnzbd.org/introducing-1-0-0>
### Known problems and solutions
- Read the file "ISSUES.txt"
### Upgrading from 0.7.x and older
## 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.0.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
## Upgrade notices
- Windows: When starting the Post-Processing script, the path to the job folder
is no longer in short-path notation but includes the full path. To support
long paths (>255), you might need to alter them to long-path notation (\\?\).
- Schedule items are converted when upgrading to 2.x.x and will break when
reverted back to pre-2.x.x releases.
- The organization of the download queue is different from 0.7.x releases.
So 2.x.x will not see the existing queue, but you can go to Status->Queue Repair
and "Repair" the old queue.
## Known problems and solutions
- Read the file "ISSUES.txt"
## Translations
- Numerous 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\>

View File

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

View File

@@ -1,5 +1,5 @@
#!/usr/bin/python -OO
# Copyright 2008-2016 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,8 +16,8 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import sys
if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
print "Sorry, requires Python 2.6 or 2.7."
if sys.version_info[:2] < (2, 7) or sys.version_info[:2] >= (3, 0):
print "Sorry, requires Python 2.7."
sys.exit(1)
# Make sure UTF-8 is default 8bit encoding
@@ -27,7 +27,7 @@ try:
sys.setdefaultencoding('utf-8')
except:
print 'Sorry, you MUST add the SABnzbd folder to the PYTHONPATH environment variable'
print 'or find another way to force Python to use UTF-8 for string encoding.'
print 'or find another way to force Python to use UTF-8 for text encoding.'
sys.exit(1)
import logging
@@ -52,12 +52,11 @@ except:
sys.exit(1)
import cherrypy
if [int(n) for n in cherrypy.__version__.split('.')] < [3, 8, 0]:
print 'Sorry, requires Python module Cherrypy 3.8.0+ (use the included version)'
if [int(n) for n in cherrypy.__version__.split('.')] < [8, 1, 2]:
print 'Sorry, requires Python module Cherrypy 8.1.2+ (use the included version)'
sys.exit(1)
from cherrypy import _cpserver
from cherrypy import _cpwsgi_server
SQLITE_DLL = True
try:
@@ -92,32 +91,20 @@ from sabnzbd.misc import real_path, \
split_host, get_ext, create_https_certificates, \
windows_variant, ip_extract, set_serv_parms, get_serv_parms, globber_full
from sabnzbd.panic import panic_tmpl, panic_port, panic_host, panic_fwall, \
panic_sqlite, panic, launch_a_browser, panic_xport
panic_sqlite, panic, launch_a_browser
import sabnzbd.scheduler as scheduler
import sabnzbd.config as config
import sabnzbd.cfg
import sabnzbd.downloader
from sabnzbd.encoding import unicoder, deunicode
import sabnzbd.growler as growler
import sabnzbd.notifier as notifier
import sabnzbd.zconfig
import sabnzbd.utils.sslinfo
from threading import Thread
LOG_FLAG = False # Global for this module, signaling loglevel change
_first_log = True
def FORCELOG(txt):
global _first_log
if _first_log:
os.remove('d:/temp/debug.txt')
_first_log = False
ff = open('d:/temp/debug.txt', 'a+')
ff.write(txt)
ff.write('\n')
ff.close()
try:
import win32api
@@ -143,62 +130,6 @@ def guard_loglevel():
LOG_FLAG = True
# Improved RotatingFileHandler
# See: http://www.mail-archive.com/python-bugs-list@python.org/msg53913.html
# http://bugs.python.org/file14420/NTSafeLogging.py
# Thanks Erik Antelman
#
if sabnzbd.WIN32:
import msvcrt
import _subprocess
import codecs
def duplicate(handle, inheritable=False):
target_process = _subprocess.GetCurrentProcess()
return _subprocess.DuplicateHandle(
_subprocess.GetCurrentProcess(), handle, target_process,
0, inheritable, _subprocess.DUPLICATE_SAME_ACCESS).Detach()
class NewRotatingFileHandler(logging.handlers.RotatingFileHandler):
def _open(self):
""" Open the current base file with the (original) mode and encoding.
Return the resulting stream.
"""
if self.encoding is None:
stream = open(self.baseFilename, self.mode)
newosf = duplicate(msvcrt.get_osfhandle(stream.fileno()), inheritable=False)
newFD = msvcrt.open_osfhandle(newosf, os.O_APPEND)
newstream = os.fdopen(newFD, self.mode)
stream.close()
return newstream
else:
stream = codecs.open(self.baseFilename, self.mode, self.encoding)
return stream
else:
NewRotatingFileHandler = logging.handlers.RotatingFileHandler
class FilterCP3:
# Filter out all CherryPy3-Access logging that we receive,
# because we have the root logger
def __init__(self):
pass
def filter(self, record):
_cplogging = record.module == '_cplogging'
# Python2.4 fix
# record has no attribute called funcName under python 2.4
if hasattr(record, 'funcName'):
access = record.funcName == 'access'
else:
access = True
return not (_cplogging and access)
class guiHandler(logging.Handler):
""" Logging handler collects the last warnings/errors/exceptions
to be displayed in the web-gui
@@ -253,10 +184,9 @@ def print_help():
print " -f --config-file <ini> Location of config file"
print " -s --server <srv:port> Listen on server:port [*]"
print " -t --templates <templ> Template directory [*]"
print " -2 --template2 <templ> Secondary template dir [*]"
print
print " -l --logging <0..2> Set logging level (-1=off, 0= least, 2= most) [*]"
print " -w --weblogging <0..2> Set cherrypy logging (0= off, 1= on, 2= file-only) [*]"
print " -w --weblogging Enable cherrypy access logging"
print
print " -b --browser <0..1> Auto browser launch (0= off, 1= on) [*]"
if sabnzbd.WIN32:
@@ -266,7 +196,6 @@ def print_help():
print " --pid <path> Create a PID file in the given folder (full path)"
print " --pidfile <path> Create a PID file with the given name (full path)"
print
print " --force Discard web-port timeout (see Wiki!)"
print " -h --help Print this message"
print " -v --version Print version information"
print " -c --clean Remove queue, cache and logs"
@@ -275,6 +204,7 @@ def print_help():
print " --repair-all Try to reconstruct the queue from the incomplete folder"
print " with full data reconstruction"
print " --https <port> Port to use for HTTPS server"
print " --no-login Start with username and password reset"
print " --log-all Log all article handling (for developers)"
print " --console Force console logging for OSX app"
print " --new Run a new instance of SABnzbd"
@@ -285,7 +215,7 @@ def print_version():
print """
%s-%s
Copyright (C) 2008-2016, The SABnzbd-Team <team@sabnzbd.org>
Copyright (C) 2008-2017, The SABnzbd-Team <team@sabnzbd.org>
SABnzbd comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions. It is licensed under the
@@ -325,9 +255,7 @@ def Bail_Out(browserhost, cherryport, err=''):
""" Abort program because of CherryPy troubles """
logging.error(T('Failed to start web-interface') + ' : ' + str(err))
if not sabnzbd.DAEMON:
if '13' in err:
panic_xport(browserhost, cherryport)
elif '49' in err:
if '49' in err:
panic_host(browserhost, cherryport)
else:
panic_port(browserhost, cherryport)
@@ -355,9 +283,6 @@ def Web_Template(key, defweb, wdir):
logging.info("Web dir is %s", full_dir)
if not os.path.exists(full_main):
# Temporarily fix that allows missing Config
if defweb == DEF_STDCONFIG:
return ''
# end temp fix
logging.warning(T('Cannot find web template: %s, trying standard template'), full_main)
full_dir = real_path(sabnzbd.DIR_INTERFACES, DEF_STDINTF)
@@ -367,8 +292,6 @@ def Web_Template(key, defweb, wdir):
panic_tmpl(full_dir)
exit_sab(1)
# sabnzbd.lang.install_language(real_path(full_dir, DEF_INT_LANGUAGE), sabnzbd.cfg.language(), wdir)
return real_path(full_dir, "templates")
@@ -479,13 +402,28 @@ def GetProfileInfo(vista_plus):
def print_modules():
""" Log all detected optional or external modules """
if sabnzbd.decoder.HAVE_YENC:
logging.info("_yenc module... found!")
if sabnzbd.decoder.SABYENC_ENABLED:
# Yes, we have SABYenc, and it's the correct version, so it's enabled
logging.info("SABYenc module (v%s)... found!", sabnzbd.constants.SABYENC_VERSION_REQUIRED)
else:
if hasattr(sys, "frozen"):
logging.error(T('_yenc module... NOT found!'))
# Something wrong with SABYenc, so let's determine and print what:
if sabnzbd.decoder.SABYENC_VERSION:
# We have a VERSION, thus a SABYenc module, but it's not the correct version
logging.warning(T("SABYenc disabled: no correct version found! (Found v%s, expecting v%s)") % (sabnzbd.decoder.SABYENC_VERSION, sabnzbd.constants.SABYENC_VERSION_REQUIRED))
else:
logging.info("_yenc module... NOT found!")
# No SABYenc module at all
logging.warning(T("SABYenc module... NOT found! Expecting v%s - https://sabnzbd.org/sabyenc") % sabnzbd.constants.SABYENC_VERSION_REQUIRED)
# No correct SABYenc version or no SABYenc at all, so now we care about old-yEnc
if sabnzbd.decoder.HAVE_YENC:
logging.info("_yenc module... found!")
else:
logging.error(T('_yenc module... NOT found!'))
if sabnzbd.HAVE_CRYPTOGRAPHY:
logging.info('Cryptography module (v%s)... found!', sabnzbd.HAVE_CRYPTOGRAPHY)
else:
logging.info('Cryptography module... NOT found!')
if sabnzbd.newsunpack.PAR2_COMMAND:
logging.info("par2 binary... found (%s)", sabnzbd.newsunpack.PAR2_COMMAND)
@@ -493,25 +431,30 @@ def print_modules():
logging.error(T('par2 binary... NOT found!'))
if sabnzbd.newsunpack.PAR2C_COMMAND:
logging.info("par2-classic binary... found (%s)", sabnzbd.newsunpack.PAR2C_COMMAND)
logging.info("par2cmdline binary... found (%s)", sabnzbd.newsunpack.PAR2C_COMMAND)
if sabnzbd.newsunpack.RAR_COMMAND:
logging.info("UNRAR binary... found (%s)", sabnzbd.newsunpack.RAR_COMMAND)
# Report problematic unrar
if sabnzbd.newsunpack.RAR_PROBLEM and not sabnzbd.cfg.ignore_wrong_unrar():
have_str = '%.2f' % (float(sabnzbd.newsunpack.RAR_VERSION) / 100)
want_str = '%.2f' % (float(sabnzbd.constants.REC_RAR_VERSION) / 100)
logging.warning(T('Your UNRAR version is %s, we recommend version %s or higher.<br />') % (have_str, want_str))
elif not (sabnzbd.WIN32 or sabnzbd.DARWIN):
logging.debug('UNRAR binary version %.2f', (float(sabnzbd.newsunpack.RAR_VERSION) / 100))
else:
logging.warning(T('unrar binary... NOT found'))
logging.error(T('unrar binary... NOT found'))
if sabnzbd.newsunpack.ZIP_COMMAND:
logging.info("unzip binary... found (%s)", sabnzbd.newsunpack.ZIP_COMMAND)
else:
if sabnzbd.cfg.enable_unzip():
logging.warning(T('unzip binary... NOT found!'))
logging.info(T('unzip binary... NOT found!'))
if sabnzbd.newsunpack.SEVEN_COMMAND:
logging.info("7za binary... found (%s)", sabnzbd.newsunpack.SEVEN_COMMAND)
else:
if sabnzbd.cfg.enable_7zip():
logging.info(T('7za binary... NOT found!'))
logging.info(T('7za binary... NOT found!'))
if not sabnzbd.WIN32:
if sabnzbd.newsunpack.NICE_COMMAND:
@@ -523,11 +466,6 @@ def print_modules():
else:
logging.info("ionice binary... NOT found!")
if sabnzbd.newswrapper.HAVE_SSL:
logging.info("pyOpenSSL... found (%s)", sabnzbd.newswrapper.HAVE_SSL)
else:
logging.info("pyOpenSSL... NOT found! - Try apt-get install python-openssl (SSL is optional)")
def all_localhosts():
""" Return all unique values of localhost in order of preference """
@@ -548,7 +486,11 @@ def all_localhosts():
ips = []
for item in info:
item = item[4][0]
if item not in ips:
# Avoid problems on strange Linux settings
if not isinstance(item, basestring):
continue
# Only return IPv6 when enabled
if item not in ips and ('::1' not in item or sabnzbd.cfg.ipv6_hosting()):
ips.append(item)
return ips
@@ -691,7 +633,7 @@ def get_webhost(cherryhost, cherryport, https_port):
if cherryport == https_port and sabnzbd.cfg.enable_https():
sabnzbd.cfg.enable_https.set(False)
# TODO: Should have a translated message, but that's not available yet
# Should have a translated message, but that's not available yet
logging.error(T('HTTP and HTTPS ports cannot be the same'))
return cherryhost, cherryport, browserhost, https_port
@@ -700,25 +642,25 @@ def get_webhost(cherryhost, cherryport, https_port):
def attach_server(host, port, cert=None, key=None, chain=None):
""" Define and attach server, optionally HTTPS """
if sabnzbd.cfg.ipv6_hosting() or '::1' not in host:
http_server = _cpwsgi_server.CPWSGIServer()
http_server = cherrypy._cpserver.Server()
http_server.bind_addr = (host, port)
if cert and key:
http_server.ssl_module = 'builtin'
http_server.ssl_certificate = cert
http_server.ssl_private_key = key
http_server.ssl_certificate_chain = chain
adapter = _cpserver.ServerAdapter(cherrypy.engine, http_server, http_server.bind_addr)
adapter.subscribe()
http_server.subscribe()
def is_sabnzbd_running(url, timeout=None):
def is_sabnzbd_running(url):
""" Return True when there's already a SABnzbd instance running. """
try:
url = '%s&mode=version' % (url)
# Do this without certificate verification, few installations will have that
prev = sabnzbd.set_https_verification(False)
ver = sabnzbd.newsunpack.get_from_url(url, timeout=timeout)
ver = sabnzbd.newsunpack.get_from_url(url)
sabnzbd.set_https_verification(prev)
return bool(ver and re.search(r'\d+\.\d+\.', ver))
return (ver and (re.search(r'\d+\.\d+\.', ver) or ver.strip() == sabnzbd.__version__))
except:
return False
@@ -728,7 +670,7 @@ def find_free_port(host, currentport):
n = 0
while n < 10 and currentport <= 49151:
try:
cherrypy.process.servers.check_port(host, currentport)
cherrypy.process.servers.check_port(host, currentport, timeout=0.025)
return currentport
except:
currentport += 5
@@ -830,12 +772,12 @@ def commandline_handler(frozen=True):
info.extend(sys.argv[slice:])
try:
opts, args = getopt.getopt(info, "phdvncw:l:s:f:t:b:2:",
opts, args = getopt.getopt(info, "phdvncwl:s:f:t:b:2:",
['pause', 'help', 'daemon', 'nobrowser', 'clean', 'logging=',
'weblogging=', 'server=', 'templates', 'ipv6_hosting=',
'weblogging', 'server=', 'templates', 'ipv6_hosting=',
'template2', 'browser=', 'config-file=', 'force',
'version', 'https=', 'autorestarted', 'repair', 'repair-all',
'log-all', 'no-login', 'pid=', 'new', 'sessions', 'console', 'pidfile=',
'log-all', 'no-login', 'pid=', 'new', 'console', 'pidfile=',
# Below Win32 Service options
'password=', 'username=', 'startup=', 'perfmonini=', 'perfmondll=',
'interactive', 'wait=',
@@ -897,18 +839,15 @@ def main():
clean_up = False
logging_level = None
web_dir = None
web_dir2 = None
vista_plus = False
vista64 = False
force_web = False
repair = 0
api_url = None
no_login = False
re_argv = [sys.argv[0]]
sabnzbd.RESTART_ARGS = [sys.argv[0]]
pid_path = None
pid_file = None
new_instance = False
force_sessions = False
osx_console = False
ipv6_hosting = None
@@ -922,18 +861,16 @@ def main():
fork = True
autobrowser = False
sabnzbd.DAEMON = True
re_argv.append(opt)
sabnzbd.RESTART_ARGS.append(opt)
elif opt in ('-f', '--config-file'):
inifile = arg
re_argv.append(opt)
re_argv.append(arg)
sabnzbd.RESTART_ARGS.append(opt)
sabnzbd.RESTART_ARGS.append(arg)
elif opt in ('-h', '--help'):
print_help()
exit_sab(0)
elif opt in ('-t', '--templates'):
web_dir = arg
elif opt in ('-2', '--template2'):
web_dir2 = arg
elif opt in ('-s', '--server'):
(cherryhost, cherryport) = split_host(arg)
elif opt in ('-n', '--nobrowser'):
@@ -943,18 +880,12 @@ def main():
autobrowser = bool(int(arg))
except:
autobrowser = True
elif opt in ('--autorestarted'):
elif opt in ('--autorestarted', ):
autorestarted = True
elif opt in ('-c', '--clean'):
clean_up = True
elif opt in ('-w', '--weblogging'):
try:
cherrypylogging = int(arg)
except:
cherrypylogging = -1
if cherrypylogging < 0 or cherrypylogging > 2:
print_help()
exit_sab(1)
cherrypylogging = True
elif opt in ('-l', '--logging'):
try:
logging_level = int(arg)
@@ -968,13 +899,10 @@ def main():
exit_sab(0)
elif opt in ('-p', '--pause'):
pause = True
elif opt in ('--force',):
force_web = True
re_argv.append(opt)
elif opt in ('--https',):
https_port = int(arg)
re_argv.append(opt)
re_argv.append(arg)
sabnzbd.RESTART_ARGS.append(opt)
sabnzbd.RESTART_ARGS.append(arg)
elif opt in ('--repair',):
repair = 1
pause = True
@@ -987,23 +915,20 @@ def main():
no_login = True
elif opt in ('--pid',):
pid_path = arg
re_argv.append(opt)
re_argv.append(arg)
sabnzbd.RESTART_ARGS.append(opt)
sabnzbd.RESTART_ARGS.append(arg)
elif opt in ('--pidfile',):
pid_file = arg
re_argv.append(opt)
re_argv.append(arg)
sabnzbd.RESTART_ARGS.append(opt)
sabnzbd.RESTART_ARGS.append(arg)
elif opt in ('--new',):
new_instance = True
elif opt in ('--sessions',):
re_argv.append(opt)
force_sessions = True
elif opt in ('--console',):
re_argv.append(opt)
sabnzbd.RESTART_ARGS.append(opt)
osx_console = True
elif opt in ('--ipv6_hosting',):
ipv6_hosting = arg
sabnzbd.MY_FULLNAME = os.path.normpath(os.path.abspath(sabnzbd.MY_FULLNAME))
sabnzbd.MY_NAME = os.path.basename(sabnzbd.MY_FULLNAME)
sabnzbd.DIR_PROG = os.path.dirname(sabnzbd.MY_FULLNAME)
@@ -1091,13 +1016,13 @@ def main():
if sabnzbd.DAEMON:
if enable_https and https_port:
try:
cherrypy.process.servers.check_port(cherryhost, https_port)
cherrypy.process.servers.check_port(cherryhost, https_port, timeout=0.05)
except IOError, error:
Bail_Out(browserhost, cherryport)
except:
Bail_Out(browserhost, cherryport, '49')
try:
cherrypy.process.servers.check_port(cherryhost, cherryport)
cherrypy.process.servers.check_port(cherryhost, cherryport, timeout=0.05)
except IOError, error:
Bail_Out(browserhost, cherryport)
except:
@@ -1110,15 +1035,11 @@ def main():
if url and check_for_sabnzbd(url, upload_nzbs, autobrowser):
exit_sab(0)
# If an instance of sabnzbd(same version) is already running on this port, launch the browser
# If another program or sabnzbd version is on this port, try 10 other ports going up in a step of 5
# If 'Port is not bound' (firewall) do not do anything (let the script further down deal with that).
# SSL
if enable_https:
port = https_port or cherryport
try:
cherrypy.process.servers.check_port(browserhost, port)
cherrypy.process.servers.check_port(browserhost, port, timeout=0.05)
except IOError, error:
if str(error) == 'Port not bound.':
pass
@@ -1126,37 +1047,49 @@ def main():
if not url:
url = 'https://%s:%s/sabnzbd/api?' % (browserhost, port)
if new_instance or not check_for_sabnzbd(url, upload_nzbs, autobrowser):
# Bail out if we have fixed our ports after first start-up
if sabnzbd.cfg.fixed_ports():
Bail_Out(browserhost, cherryport)
# Find free port to bind
newport = find_free_port(browserhost, port)
if newport > 0:
sabnzbd.cfg.https_port.set(newport)
# Save the new port
if https_port:
https_port = newport
sabnzbd.cfg.https_port.set(newport)
else:
# In case HTTPS == HTTP port
http_port = newport
sabnzbd.cfg.port.set(newport)
except:
# Something else wrong, probably badly specified host
Bail_Out(browserhost, cherryport, '49')
# NonSSL
try:
cherrypy.process.servers.check_port(browserhost, cherryport)
except IOError, error:
if str(error) == 'Port not bound.':
pass
else:
if not url:
url = 'http://%s:%s/sabnzbd/api?' % (browserhost, cherryport)
if new_instance or not check_for_sabnzbd(url, upload_nzbs, autobrowser):
port = find_free_port(browserhost, cherryport)
if port > 0:
sabnzbd.cfg.cherryport.set(port)
cherryport = port
except:
Bail_Out(browserhost, cherryport, '49')
# NonSSL check if there's no HTTPS or we only use 1 port
if not (enable_https and not https_port):
try:
cherrypy.process.servers.check_port(browserhost, cherryport, timeout=0.05)
except IOError, error:
if str(error) == 'Port not bound.':
pass
else:
if not url:
url = 'http://%s:%s/sabnzbd/api?' % (browserhost, cherryport)
if new_instance or not check_for_sabnzbd(url, upload_nzbs, autobrowser):
# Bail out if we have fixed our ports after first start-up
if sabnzbd.cfg.fixed_ports():
Bail_Out(browserhost, cherryport)
# Find free port to bind
port = find_free_port(browserhost, cherryport)
if port > 0:
sabnzbd.cfg.cherryport.set(port)
cherryport = port
except:
# Something else wrong, probably badly specified host
Bail_Out(browserhost, cherryport, '49')
if cherrypylogging is None:
cherrypylogging = sabnzbd.cfg.log_web()
else:
sabnzbd.cfg.log_web.set(cherrypylogging)
# We found a port, now we never check again
sabnzbd.cfg.fixed_ports.set(True)
if logging_level is None:
logging_level = sabnzbd.cfg.log_level()
@@ -1181,24 +1114,16 @@ def main():
# Prevent the logger from raising exceptions
# primarily to reduce the fallout of Python issue 4749
logging.raiseExceptions = 0
log_new = sabnzbd.cfg.log_new()
if log_new:
log_handler = NewRotatingFileHandler
else:
log_handler = logging.handlers.RotatingFileHandler
sabnzbd.LOGFILE = os.path.join(logdir, DEF_LOG_FILE)
logsize = sabnzbd.cfg.log_size.get_int()
try:
rollover_log = log_handler(
rollover_log = logging.handlers.RotatingFileHandler(
sabnzbd.LOGFILE, 'a+',
logsize,
sabnzbd.cfg.log_size.get_int(),
sabnzbd.cfg.log_backups())
logformat = '%(asctime)s::%(levelname)s::[%(module)s:%(lineno)d] %(message)s'
rollover_log.setFormatter(logging.Formatter(logformat))
rollover_log.addFilter(FilterCP3())
sabnzbd.LOGHANDLER = rollover_log
logger.addHandler(rollover_log)
logger.setLevel(LOGLEVELS[logging_level + 1])
@@ -1228,7 +1153,6 @@ def main():
if consoleLogging:
console = logging.StreamHandler()
console.addFilter(FilterCP3())
console.setLevel(LOGLEVELS[logging_level + 1])
console.setFormatter(logging.Formatter(logformat))
logger.addHandler(console)
@@ -1257,11 +1181,18 @@ def main():
logging.info('Platform = %s', os.name)
logging.info('Python-version = %s', sys.version)
logging.info('Arguments = %s', sabnzbd.CMDLINE)
# Find encoding; relevant for unrar activities
try:
logging.info('Preferred encoding = %s', locale.getpreferredencoding())
preferredencoding = locale.getpreferredencoding()
logging.info('Preferred encoding = %s', preferredencoding)
except:
logging.info('Preferred encoding = ERROR')
preferredencoding = ''
# On Linux/FreeBSD/Unix "UTF-8" is strongly, strongly adviced:
if not sabnzbd.WIN32 and not sabnzbd.DARWIN and not ('utf' in preferredencoding.lower() and '8' in preferredencoding.lower()):
logging.warning(T("SABnzbd was started with encoding %s, this should be UTF-8. Expect problems with Unicoded file and directory names in downloads.") % preferredencoding)
if sabnzbd.cfg.log_level() > 1:
from sabnzbd.getipaddress import localipv4, publicipv4, ipv6
@@ -1295,17 +1226,6 @@ def main():
if cpumodel:
logging.debug('CPU model name is %s', cpumodel)
# OSX 10.5 I/O priority setting
if sabnzbd.DARWIN:
logging.info('[osx] IO priority setting')
try:
from ctypes import cdll
libc = cdll.LoadLibrary('/usr/lib/libc.dylib')
boolSetResult = libc.setiopolicy_np(0, 1, 3) # @UnusedVariable
logging.info('[osx] IO priority set to throttle for process scope')
except:
logging.info('[osx] IO priority setting not supported')
logging.info('Read INI file %s', inifile)
if autobrowser is not None:
@@ -1317,33 +1237,16 @@ def main():
signal.signal(signal.SIGINT, sabnzbd.sig_handler)
signal.signal(signal.SIGTERM, sabnzbd.sig_handler)
init_ok = sabnzbd.initialize(pause, clean_up, evalSched=True, repair=repair)
if not init_ok:
logging.error(T('Initializing %s-%s failed, aborting'),
sabnzbd.MY_NAME, sabnzbd.__version__)
exit_sab(2)
sabnzbd.initialize(pause, clean_up, evalSched=True, repair=repair)
os.chdir(sabnzbd.DIR_PROG)
web_dir = Web_Template(sabnzbd.cfg.web_dir, DEF_STDINTF, fix_webname(web_dir))
web_dir2 = Web_Template(sabnzbd.cfg.web_dir2, '', fix_webname(web_dir2))
web_dirc = Web_Template(None, DEF_STDCONFIG, '')
sabnzbd.WEB_DIR = Web_Template(sabnzbd.cfg.web_dir, DEF_STDINTF, fix_webname(web_dir))
sabnzbd.WEB_DIR_CONFIG = Web_Template(None, DEF_STDCONFIG, '')
sabnzbd.WIZARD_DIR = os.path.join(sabnzbd.DIR_INTERFACES, 'wizard')
wizard_dir = os.path.join(sabnzbd.DIR_INTERFACES, 'wizard')
# sabnzbd.lang.install_language(os.path.join(wizard_dir, DEF_INT_LANGUAGE), sabnzbd.cfg.language(), 'wizard')
sabnzbd.WEB_DIR = web_dir
sabnzbd.WEB_DIR2 = web_dir2
sabnzbd.WEB_DIRC = web_dirc
sabnzbd.WIZARD_DIR = wizard_dir
sabnzbd.WEB_COLOR = CheckColor(sabnzbd.cfg.web_color(), web_dir)
sabnzbd.WEB_COLOR = CheckColor(sabnzbd.cfg.web_color(), sabnzbd.WEB_DIR)
sabnzbd.cfg.web_color.set(sabnzbd.WEB_COLOR)
sabnzbd.WEB_COLOR2 = CheckColor(sabnzbd.cfg.web_color2(), web_dir2)
sabnzbd.cfg.web_color2.set(sabnzbd.WEB_COLOR2)
logging.debug('Unwanted extensions are ... %s', sabnzbd.cfg.unwanted_extensions())
if fork and not sabnzbd.WIN32:
daemonize()
@@ -1368,28 +1271,8 @@ def main():
print_modules()
import sabnzbd.utils.sslinfo
logging.info("SSL version %s", sabnzbd.utils.sslinfo.ssl_version())
logging.info("pyOpenSSL version %s", sabnzbd.utils.sslinfo.pyopenssl_version())
logging.info("SSL potentially supported protocols %s", str(sabnzbd.utils.sslinfo.ssl_potential()))
logging.info("SSL actually supported protocols %s", str(sabnzbd.utils.sslinfo.ssl_protocols()))
cherrylogtoscreen = False
sabnzbd.WEBLOGFILE = None
if cherrypylogging:
if logdir:
sabnzbd.WEBLOGFILE = os.path.join(logdir, DEF_LOG_CHERRY)
# Define our custom logger for cherrypy errors
cherrypy_logging(sabnzbd.WEBLOGFILE, log_handler)
if not fork:
try:
x = sys.stderr.fileno
x = sys.stdout.fileno
if cherrypylogging == 1:
cherrylogtoscreen = True
except:
pass
logging.info("SSL supported protocols %s", str(sabnzbd.utils.sslinfo.ssl_protocols_labels()))
https_cert = sabnzbd.cfg.https_cert.get_path()
https_key = sabnzbd.cfg.https_key.get_path()
@@ -1415,7 +1298,7 @@ def main():
hosts[1] = '::1'
# The Windows binary requires numeric localhost as primary address
if multilocal and cherryhost == 'localhost':
if cherryhost == 'localhost':
cherryhost = hosts[0]
if enable_https:
@@ -1430,9 +1313,10 @@ def main():
cherryport = https_port
elif multilocal:
# Extra HTTPS port for secondary localhost
attach_server(hosts[1], cherryport, https_cert, https_key)
attach_server(hosts[1], cherryport, https_cert, https_key, https_chain)
cherrypy.config.update({'server.ssl_certificate': https_cert,
cherrypy.config.update({'server.ssl_module': 'builtin',
'server.ssl_certificate': https_cert,
'server.ssl_private_key': https_key,
'server.ssl_certificate_chain': https_chain})
elif multilocal:
@@ -1443,14 +1327,6 @@ def main():
sabnzbd.cfg.username.set('')
sabnzbd.cfg.password.set('')
# Fix leakage in memory-based CherryPy session support by using file-based.
# However, we don't really need session support.
if force_sessions:
sessions = sabnzbd.misc.create_real_path('sessions', sabnzbd.cfg.admin_dir.get_path(), 'sessions')[1]
sabnzbd.misc.remove_all(sessions, 'session-*.lock', keep_folder=True)
else:
sessions = None
mime_gzip = ('text/*',
'application/javascript',
'application/x-javascript',
@@ -1463,58 +1339,46 @@ def main():
cherrypy.config.update({'server.environment': 'production',
'server.socket_host': cherryhost,
'server.socket_port': cherryport,
'log.screen': cherrylogtoscreen,
'engine.autoreload.frequency': 100,
'server.shutdown_timeout': 0,
'log.screen': False,
'engine.autoreload.on': False,
'engine.reexec_retry': 100,
'tools.encode.on': True,
'tools.gzip.on': True,
'tools.gzip.mime_types': mime_gzip,
'tools.sessions.on': bool(sessions),
'tools.sessions.storage_type': 'file',
'tools.sessions.storage_path': sessions,
'tools.sessions.timeout': 60,
'request.show_tracebacks': True,
'checker.check_localhost': bool(consoleLogging),
'error_page.401': sabnzbd.panic.error_page_401,
'error_page.404': sabnzbd.panic.error_page_404
})
forced_mime_types = {'css': 'text/css', 'js': 'application/javascript'}
static = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(web_dir, 'static'), 'tools.staticdir.content_types': forced_mime_types}
staticcfg = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(web_dirc, 'staticcfg'), 'tools.staticdir.content_types': forced_mime_types}
wizard_static = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(wizard_dir, 'static'), 'tools.staticdir.content_types': forced_mime_types}
appconfig = {'/sabnzbd/api': {'tools.basic_auth.on': False},
'/api': {'tools.basic_auth.on': False},
'/m/api': {'tools.basic_auth.on': False},
# Do we want CherryPy Logging? Cannot be done via the config
if cherrypylogging:
sabnzbd.WEBLOGFILE = os.path.join(logdir, DEF_LOG_CHERRY)
cherrypy.log.screen = True
cherrypy.log.access_log.propagate = True
cherrypy.log.access_file = str(sabnzbd.WEBLOGFILE)
else:
cherrypy.log.access_log.propagate = False
# Force mimetypes (OS might overwrite them)
forced_mime_types = {'css': 'text/css', 'js': 'application/javascript'}
static = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(sabnzbd.WEB_DIR, 'static'), 'tools.staticdir.content_types': forced_mime_types}
staticcfg = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(sabnzbd.WEB_DIR_CONFIG, 'staticcfg'), 'tools.staticdir.content_types': forced_mime_types}
wizard_static = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(sabnzbd.WIZARD_DIR, 'static'), 'tools.staticdir.content_types': forced_mime_types}
appconfig = {'/api': {'tools.basic_auth.on': False},
'/rss': {'tools.basic_auth.on': False},
'/sabnzbd/rss': {'tools.basic_auth.on': False},
'/m/rss': {'tools.basic_auth.on': False},
'/sabnzbd/shutdown': {'streamResponse': True},
'/sabnzbd/static': static,
'/static': static,
'/sabnzbd/wizard/static': wizard_static,
'/wizard/static': wizard_static,
'/favicon.ico': {'tools.staticfile.on': True, 'tools.staticfile.filename': os.path.join(web_dirc, 'staticcfg', 'ico', 'favicon.ico')},
'/sabnzbd/staticcfg': staticcfg,
'/favicon.ico': {'tools.staticfile.on': True, 'tools.staticfile.filename': os.path.join(sabnzbd.WEB_DIR_CONFIG, 'staticcfg', 'ico', 'favicon.ico')},
'/staticcfg': staticcfg
}
if web_dir2:
static2 = {'tools.staticdir.on': True, 'tools.staticdir.dir': os.path.join(web_dir2, 'static'), 'tools.staticdir.content_types': forced_mime_types}
appconfig['/sabnzbd/m/api'] = {'tools.basic_auth.on': False}
appconfig['/sabnzbd/m/rss'] = {'tools.basic_auth.on': False}
appconfig['/sabnzbd/m/shutdown'] = {'streamResponse': True}
appconfig['/sabnzbd/m/static'] = static2
appconfig['/m/static'] = static2
appconfig['/sabnzbd/m/wizard/static'] = wizard_static
appconfig['/m/wizard/static'] = wizard_static
appconfig['/sabnzbd/m/staticcfg'] = staticcfg
appconfig['/m/staticcfg'] = staticcfg
login_page = sabnzbd.interface.MainPage(web_dir, '/', web_dir2, '/m/', web_dirc, first=2)
cherrypy.tree.mount(login_page, '/', config=appconfig)
# Make available from both URLs
main_page = sabnzbd.interface.MainPage()
cherrypy.tree.mount(main_page, '/', config=appconfig)
cherrypy.tree.mount(main_page, '/sabnzbd/', config=appconfig)
# Set authentication for CherryPy
sabnzbd.interface.set_auth(cherrypy.config)
@@ -1524,21 +1388,7 @@ def main():
sabnzbd.cfg.log_level.callback(guard_loglevel)
try:
# Use internal cherrypy check first to prevent ugly tracebacks
cherrypy.process.servers.check_port(browserhost, cherryport)
cherrypy.engine.start()
except IOError, error:
if str(error) == 'Port not bound.':
if not force_web:
panic_fwall(vista_plus)
sabnzbd.halt()
exit_sab(2)
else:
logging.error(T('Failed to start web-interface: '), exc_info=True)
Bail_Out(browserhost, cherryport, str(error))
except socket.error, error:
logging.error(T('Failed to start web-interface: '), exc_info=True)
Bail_Out(browserhost, cherryport)
except:
logging.error(T('Failed to start web-interface: '), exc_info=True)
Bail_Out(browserhost, cherryport)
@@ -1547,26 +1397,6 @@ def main():
cherrypy.engine.wait(cherrypy.process.wspbus.states.STARTED)
sabnzbd.zconfig.set_bonjour(cherryhost, cherryport)
if enable_https:
browser_url = "https://%s:%s/sabnzbd" % (browserhost, cherryport)
else:
browser_url = "http://%s:%s/sabnzbd" % (browserhost, cherryport)
sabnzbd.BROWSER_URL = browser_url
if hasattr(cherrypy.wsgiserver, 'redirect_url'):
cherrypy.wsgiserver.redirect_url('https://%%s:%s/sabnzbd' % cherryport)
if not autorestarted:
launch_a_browser(browser_url)
if sabnzbd.FOUNDATION:
import sabnzbd.osxmenu
sabnzbd.osxmenu.notify("SAB_Launched", None)
growler.send_notification('SABnzbd%s' % growler.hostname(),
T('SABnzbd %s started') % sabnzbd.__version__, 'startup')
# Now's the time to check for a new version
check_latest_version()
autorestarted = False
mail = None
if sabnzbd.WIN32:
if enable_https:
@@ -1604,17 +1434,35 @@ def main():
for f in upload_nzbs:
add_local(f)
# Set URL for browser
if enable_https:
browser_url = "https://%s:%s/sabnzbd" % (browserhost, cherryport)
else:
browser_url = "http://%s:%s/sabnzbd" % (browserhost, cherryport)
sabnzbd.BROWSER_URL = browser_url
if not autorestarted:
launch_a_browser(browser_url)
if sabnzbd.FOUNDATION:
import sabnzbd.osxmenu
sabnzbd.osxmenu.notify("SAB_Launched", None)
notifier.send_notification('SABnzbd%s' % notifier.hostname(),
T('SABnzbd %s started') % sabnzbd.__version__, 'startup')
# Now's the time to check for a new version
check_latest_version()
autorestarted = False
# Have to keep this running, otherwise logging will terminate
timer = timer5 = 0
timer = 0
while not sabnzbd.SABSTOP:
if sabnzbd.LAST_WARNING:
msg = sabnzbd.LAST_WARNING
sabnzbd.LAST_WARNING = None
sabnzbd.growler.send_notification(T('Warning'), msg, 'warning')
sabnzbd.notifier.send_notification(T('Warning'), msg, 'warning')
if sabnzbd.LAST_ERROR:
msg = sabnzbd.LAST_ERROR
sabnzbd.LAST_ERROR = None
sabnzbd.growler.send_notification(T('Error'), msg, 'error')
sabnzbd.notifier.send_notification(T('Error'), msg, 'error')
if sabnzbd.WIN_SERVICE:
rc = win32event.WaitForMultipleObjects((sabnzbd.WIN_SERVICE.hWaitStop,
@@ -1622,7 +1470,7 @@ def main():
if rc == win32event.WAIT_OBJECT_0:
if mail:
mail.send('stop')
sabnzbd.save_state(flag=True)
sabnzbd.save_state()
logging.info('Leaving SABnzbd')
sabnzbd.SABSTOP = True
return
@@ -1649,58 +1497,40 @@ def main():
# Check the threads
if not sabnzbd.check_all_tasks():
autorestarted = True
cherrypy.engine.execv = True
sabnzbd.TRIGGER_RESTART = True
# Notify guardian
if sabnzbd.WIN_SERVICE and mail:
mail.send('active')
if timer5 > 9:
# 5 minute polling tasks
timer5 = 0
if sabnzbd.cfg.web_watchdog() and not is_sabnzbd_running('%s/api?tickleme=1' % sabnzbd.BROWSER_URL, 120):
autorestarted = True
cherrypy.engine.execv = True
else:
timer5 += 1
else:
timer += 1
# 3 sec polling tasks
# Check for auto-restart request
if cherrypy.engine.execv:
if sabnzbd.SCHED_RESTART:
scheduler.abort()
sabnzbd.halt()
else:
scheduler.stop()
sabnzbd.halt()
cherrypy.engine.exit()
# Or special restart cases like Mac and WindowsService
if sabnzbd.TRIGGER_RESTART:
# Shutdown
cherrypy.engine.exit()
sabnzbd.halt()
sabnzbd.SABSTOP = True
if sabnzbd.downloader.Downloader.do.paused:
re_argv.append('-p')
sabnzbd.RESTART_ARGS.append('-p')
if autorestarted:
re_argv.append('--autorestarted')
sys.argv = re_argv
sabnzbd.RESTART_ARGS.append('--autorestarted')
sys.argv = sabnzbd.RESTART_ARGS
os.chdir(org_dir)
if sabnzbd.DARWIN:
# TODO: when executing from sources on osx, after a restart, process is detached from console
# If OSX frozen restart of app instead of embedded python
if getattr(sys, 'frozen', None) == 'macosx_app':
# [[NSProcessInfo processInfo] processIdentifier]]
# logging.info("%s" % (NSProcessInfo.processInfo().processIdentifier()))
my_pid = os.getpid()
my_name = sabnzbd.MY_FULLNAME.replace('/Contents/MacOS/SABnzbd', '')
my_args = ' '.join(sys.argv[1:])
cmd = 'kill -9 %s && open "%s" --args %s' % (my_pid, my_name, my_args)
logging.info('Launching: ', cmd)
os.system(cmd)
else:
args = sys.argv[:]
args.insert(0, sys.executable)
pid = os.fork()
if pid == 0:
os.execv(sys.executable, args)
# If OSX frozen restart of app instead of embedded python
if getattr(sys, 'frozen', None) == 'macosx_app':
# [[NSProcessInfo processInfo] processIdentifier]]
# logging.info("%s" % (NSProcessInfo.processInfo().processIdentifier()))
my_pid = os.getpid()
my_name = sabnzbd.MY_FULLNAME.replace('/Contents/MacOS/SABnzbd', '')
my_args = ' '.join(sys.argv[1:])
cmd = 'kill -9 %s && open "%s" --args %s' % (my_pid, my_name, my_args)
logging.info('Launching: ', cmd)
os.system(cmd)
elif sabnzbd.WIN_SERVICE and mail:
logging.info('Asking the SABHelper service for a restart')
mail.send('restart')
@@ -1731,7 +1561,7 @@ def main():
# Failing AppHelper libary!
os._exit(0)
else:
growler.send_notification('SABnzbd', T('SABnzbd shutdown finished'), 'startup')
notifier.send_notification('SABnzbd', T('SABnzbd shutdown finished'), 'startup')
os._exit(0)

View File

@@ -1,25 +0,0 @@
Copyright (c) 2004-2015, CherryPy Team (team@cherrypy.org)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the CherryPy Team nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,76 +0,0 @@
From 0f6da83f5acff3fc9c4eda2d3111849ef1429711 Mon Sep 17 00:00:00 2001
From: shypike <shypike@sabnzbd.org>
Date: Thu, 23 Jul 2015 18:16:27 +0200
Subject: [PATCH] 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.
---
cherrypy/wsgiserver/wsgiserver2.py | 23 ++++++++++++++++++-----
1 file changed, 18 insertions(+), 5 deletions(-)
diff --git a/cherrypy/wsgiserver/wsgiserver2.py b/cherrypy/wsgiserver/wsgiserver2.py
index c0896d3..9367f7b 100644
--- a/cherrypy/wsgiserver/wsgiserver2.py
+++ b/cherrypy/wsgiserver/wsgiserver2.py
@@ -75,7 +75,7 @@ __all__ = ['HTTPRequest', 'HTTPConnection', 'HTTPServer',
'WorkerThread', 'ThreadPool', 'SSLAdapter',
'CherryPyWSGIServer',
'Gateway', 'WSGIGateway', 'WSGIGateway_10', 'WSGIGateway_u0',
- 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class']
+ 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class', 'redirect_url']
import os
try:
@@ -97,6 +97,7 @@ except ImportError:
import StringIO
DEFAULT_BUFFER_SIZE = -1
+REDIRECT_URL = None # Application can write its HTTP-->HTTPS redirection URL here
class FauxSocket(object):
@@ -167,6 +168,12 @@ quoted_slash = re.compile(ntob("(?i)%2F"))
import errno
+def redirect_url(url=None):
+ global REDIRECT_URL
+ if url and '%s' in url:
+ REDIRECT_URL = url
+ return REDIRECT_URL
+
def plat_specific_errors(*errnames):
"""Return error numbers for all errors in errnames on this platform.
@@ -881,6 +888,9 @@ class HTTPRequest(object):
"Content-Length: %s\r\n" % len(msg),
"Content-Type: text/plain\r\n"]
+ if status[:3] in ("301",):
+ buf.append("Location: %s" % msg)
+
if status[:3] in ("413", "414"):
# Request Entity Too Large / Request-URI Too Long
self.close_connection = True
@@ -1394,10 +1404,13 @@ class HTTPConnection(object):
# Unwrap our wfile
self.wfile = CP_fileobject(
self.socket._sock, "wb", self.wbufsize)
- req.simple_response(
- "400 Bad Request",
- "The client sent a plain HTTP request, but "
- "this server only speaks HTTPS on this port.")
+ if REDIRECT_URL:
+ req.simple_response("301 Moved Permanently", REDIRECT_URL % self.remote_addr)
+ else:
+ req.simple_response(
+ "400 Bad Request",
+ "The client sent a plain HTTP request, but "
+ "this server only speaks HTTPS on this port.")
self.linger = True
except Exception:
e = sys.exc_info()[1]
--
1.9.5 (Apple Git-50.3)

View File

@@ -1,5 +1,5 @@
CherryPy 3.8.0 Official distribution: https://pypi.python.org/packages/source/C/CherryPy/CherryPy-3.8.0.tar.gz
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.
A patch is required to enable proper Bonjour/Zeroconfig support.

View File

@@ -56,28 +56,31 @@ with customized or extended components. The core API's are:
These API's are described in the `CherryPy specification <https://bitbucket.org/cherrypy/cherrypy/wiki/CherryPySpec>`_.
"""
__version__ = "3.8.0"
try:
import pkg_resources
except ImportError:
pass
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
from cherrypy._cpcompat import basestring, unicodestr, set
from threading import local as _local
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect # noqa
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError # noqa
from cherrypy import _cpdispatch as dispatch
from cherrypy import _cplogging
from cherrypy import _cpdispatch as dispatch # noqa
from cherrypy import _cptools
tools = _cptools.default_toolbox
Tool = _cptools.Tool
from cherrypy._cptools import default_toolbox as tools, Tool
from cherrypy import _cprequest
from cherrypy.lib import httputil as _httputil
from cherrypy import _cptree
tree = _cptree.Tree()
from cherrypy._cptree import Application
from cherrypy import _cpwsgi as wsgi
from cherrypy._cptree import Application # noqa
from cherrypy import _cpwsgi as wsgi # noqa
from cherrypy import _cpserver
from cherrypy import process
try:
from cherrypy.process import win32
@@ -88,6 +91,12 @@ 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()
@@ -135,20 +144,19 @@ class _HandleSignalsPlugin(object):
def subscribe(self):
"""Add the handlers based on the platform"""
if hasattr(self.bus, "signal_handler"):
if hasattr(self.bus, 'signal_handler'):
self.bus.signal_handler.subscribe()
if hasattr(self.bus, "console_control_handler"):
if hasattr(self.bus, 'console_control_handler'):
self.bus.console_control_handler.subscribe()
engine.signals = _HandleSignalsPlugin(engine)
from cherrypy import _cpserver
server = _cpserver.Server()
server.subscribe()
def quickstart(root=None, script_name="", config=None):
def quickstart(root=None, script_name='', config=None):
"""Mount the given root, start the builtin server (and engine), then block.
root: an instance of a "controller class" (a collection of page handler
@@ -175,9 +183,6 @@ def quickstart(root=None, script_name="", config=None):
engine.block()
from cherrypy._cpcompat import threadlocal as _local
class _Serving(_local):
"""An interface for registering request and response objects.
@@ -190,8 +195,8 @@ class _Serving(_local):
thread-safe way.
"""
request = _cprequest.Request(_httputil.Host("127.0.0.1", 80),
_httputil.Host("127.0.0.1", 1111))
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."""
@@ -224,7 +229,7 @@ class _ThreadLocalProxy(object):
return getattr(child, name)
def __setattr__(self, name, value):
if name in ("__attrname__", ):
if name in ('__attrname__', ):
object.__setattr__(self, name, value)
else:
child = getattr(serving, self.__attrname__)
@@ -300,9 +305,6 @@ except ImportError:
pass
from cherrypy import _cplogging
class _GlobalLogManager(_cplogging.LogManager):
"""A site-wide LogManager; routes to app.log or global log as appropriate.
@@ -318,7 +320,7 @@ class _GlobalLogManager(_cplogging.LogManager):
"""Log the given message to the app.log or global log as appropriate.
"""
# Do NOT use try/except here. See
# https://bitbucket.org/cherrypy/cherrypy/issue/945
# https://github.com/cherrypy/cherrypy/issues/945
if hasattr(request, 'app') and hasattr(request.app, 'log'):
log = request.app.log
else:
@@ -346,293 +348,10 @@ def _buslog(msg, level):
log.error(msg, 'ENGINE', severity=level)
engine.subscribe('log', _buslog)
# Helper functions for CP apps #
def expose(func=None, alias=None):
"""Expose the function, optionally providing an alias or set of aliases."""
def expose_(func):
func.exposed = True
if alias is not None:
if isinstance(alias, basestring):
parents[alias.replace(".", "_")] = func
else:
for a in alias:
parents[a.replace(".", "_")] = func
return func
import sys
import types
if isinstance(func, (types.FunctionType, types.MethodType)):
if alias is None:
# @expose
func.exposed = True
return func
else:
# func = expose(func, alias)
parents = sys._getframe(1).f_locals
return expose_(func)
elif func is None:
if alias is None:
# @expose()
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose(alias="alias") or
# @expose(alias=["alias1", "alias2"])
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose("alias") or
# @expose(["alias1", "alias2"])
parents = sys._getframe(1).f_locals
alias = func
return expose_
def 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, 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:
request.params.update(parms)
return handler
request.params.update(parms)
# If we are the ultimate handler, then to prevent our _cp_dispatch
# from being called again, we will resolve remaining elements through
# getattr() directly.
if 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 request.app:
if not path.startswith("/"):
# Append/remove trailing slash from path_info as needed
# (this is to support mistyped URL's without redirecting;
# if you want to redirect, use tools.trailing_slash).
pi = request.path_info
if request.is_index is True:
if not pi.endswith('/'):
pi = pi + '/'
elif request.is_index is False:
if pi.endswith('/') and pi != '/':
pi = pi[:-1]
if path == "":
path = pi
else:
path = _urljoin(pi, path)
if script_name is None:
script_name = request.script_name
if base is None:
base = request.base
newurl = base + script_name + path + qs
else:
# No request.app (we're being called outside a request).
# We'll have to guess the base from server.* attributes.
# This will produce very different results from the above
# if you're using vhosts or tools.proxy.
if base is None:
base = server.base()
path = (script_name or "") + path
newurl = base + path + qs
if './' in newurl:
# Normalize the URL by removing ./ and ../
atoms = []
for atom in newurl.split('/'):
if atom == '.':
pass
elif atom == '..':
atoms.pop()
else:
atoms.append(atom)
newurl = '/'.join(atoms)
# At this point, we should have a fully-qualified absolute URL.
if relative is None:
relative = getattr(request.app, "relative_urls", False)
# See http://www.ietf.org/rfc/rfc2396.txt
if relative == 'server':
# "A relative reference beginning with a single slash character is
# termed an absolute-path reference, as defined by <abs_path>..."
# This is also sometimes called "server-relative".
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
elif relative:
# "A relative reference that does not begin with a scheme name
# or a slash character is termed a relative-path reference."
old = url(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
from cherrypy._helper import expose, popargs, url # noqa
# import _cpconfig last so it can reference other top-level objects
from cherrypy import _cpconfig
from cherrypy import _cpconfig # noqa
# Use _global_conf_alias so quickstart can use 'config' as an arg
# without shadowing cherrypy.config.
config = _global_conf_alias = _cpconfig.Config()
@@ -642,11 +361,11 @@ config.defaults = {
'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)
config.namespaces['log'] = lambda k, v: setattr(log, k, v)
config.namespaces['checker'] = lambda k, v: setattr(checker, k, v)
# Must reset to get our defaults applied.
config.reset()
from cherrypy import _cpchecker
from cherrypy import _cpchecker # noqa
checker = _cpchecker.Checker()
engine.subscribe('start', checker)

0
cherrypy/__main__.py Normal file → Executable file
View File

View File

@@ -33,7 +33,7 @@ class Checker(object):
warnings.formatwarning = self.formatwarning
try:
for name in dir(self):
if name.startswith("check_"):
if name.startswith('check_'):
method = getattr(self, name)
if method and hasattr(method, '__call__'):
method()
@@ -42,7 +42,7 @@ class Checker(object):
def formatwarning(self, message, category, filename, lineno, line=None):
"""Function to format a warning."""
return "CherryPy Checker:\n%s\n\n" % message
return 'CherryPy Checker:\n%s\n\n' % message
# This value should be set inside _cpconfig.
global_config_contained_paths = False
@@ -57,13 +57,13 @@ class Checker(object):
continue
if sn == '':
continue
sn_atoms = sn.strip("/").split("/")
sn_atoms = sn.strip('/').split('/')
for key in app.config.keys():
key_atoms = key.strip("/").split("/")
key_atoms = key.strip('/').split('/')
if key_atoms[:len(sn_atoms)] == sn_atoms:
warnings.warn(
"The application mounted at %r has config "
"entries that start with its script name: %r" % (sn,
'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):
@@ -76,17 +76,17 @@ class Checker(object):
for section, entries in iteritems(app.config):
if section.startswith('/'):
for key, value in iteritems(entries):
for n in ("engine.", "server.", "tree.", "checker."):
for n in ('engine.', 'server.', 'tree.', 'checker.'):
if key.startswith(n):
msg.append("[%s] %s = %s" %
msg.append('[%s] %s = %s' %
(section, key, value))
if msg:
msg.insert(0,
"The application mounted at %r contains the "
"following config entries, which are only allowed "
"in site-wide config. Move them to a [global] "
"section and pass them to cherrypy.config.update() "
"instead of tree.mount()." % sn)
'The application mounted at %r contains the '
'following config entries, which are only allowed '
'in site-wide config. Move them to a [global] '
'section and pass them to cherrypy.config.update() '
'instead of tree.mount().' % sn)
warnings.warn(os.linesep.join(msg))
def check_skipped_app_config(self):
@@ -95,13 +95,13 @@ class Checker(object):
if not isinstance(app, cherrypy.Application):
continue
if not app.config:
msg = "The Application mounted at %r has an empty config." % sn
msg = 'The Application mounted at %r has an empty config.' % sn
if self.global_config_contained_paths:
msg += (" It looks like the config you passed to "
"cherrypy.config.update() contains application-"
"specific sections. You must explicitly pass "
"application config via "
"cherrypy.tree.mount(..., config=app_config)")
msg += (' It looks like the config you passed to '
'cherrypy.config.update() contains application-'
'specific sections. You must explicitly pass '
'application config via '
'cherrypy.tree.mount(..., config=app_config)')
warnings.warn(msg)
return
@@ -115,12 +115,12 @@ class Checker(object):
if not app.config:
continue
for key in app.config.keys():
if key.startswith("[") or key.endswith("]"):
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))
'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."""
@@ -132,47 +132,47 @@ class Checker(object):
request.app = app
for section in app.config:
# get_resource will populate request.config
request.get_resource(section + "/dummy.html")
request.get_resource(section + '/dummy.html')
conf = request.config.get
if conf("tools.staticdir.on", False):
msg = ""
root = conf("tools.staticdir.root")
dir = conf("tools.staticdir.dir")
if conf('tools.staticdir.on', False):
msg = ''
root = conf('tools.staticdir.root')
dir = conf('tools.staticdir.dir')
if dir is None:
msg = "tools.staticdir.dir is not set."
msg = 'tools.staticdir.dir is not set.'
else:
fulldir = ""
fulldir = ''
if os.path.isabs(dir):
fulldir = dir
if root:
msg = ("dir is an absolute path, even "
"though a root is provided.")
msg = ('dir is an absolute path, even '
'though a root is provided.')
testdir = os.path.join(root, dir[1:])
if os.path.exists(testdir):
msg += (
"\nIf you meant to serve the "
"filesystem folder at %r, remove the "
"leading slash from dir." % (testdir,))
'\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.")
'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." % (
msg = ('%r is not an absolute path.' % (
fulldir,))
if fulldir and not os.path.exists(fulldir):
if msg:
msg += "\n"
msg += ("%r (root + dir) is not an existing "
"filesystem path." % fulldir)
msg += '\n'
msg += ('%r (root + dir) is not an existing '
'filesystem path.' % fulldir)
if msg:
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r'
% (msg, section, root, dir))
# -------------------------- Compatibility -------------------------- #
@@ -198,19 +198,19 @@ class Checker(object):
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]" %
warnings.warn('%r is obsolete. Use %r instead.\n'
'section: [%s]' %
(k, self.obsolete[k], section))
elif k in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead.\n"
"section: [%s]" %
warnings.warn('%r is deprecated. Use %r instead.\n'
'section: [%s]' %
(k, self.deprecated[k], section))
else:
if section in self.obsolete:
warnings.warn("%r is obsolete. Use %r instead."
warnings.warn('%r is obsolete. Use %r instead.'
% (section, self.obsolete[section]))
elif section in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead."
warnings.warn('%r is deprecated. Use %r instead.'
% (section, self.deprecated[section]))
def check_compatibility(self):
@@ -225,7 +225,7 @@ class Checker(object):
extra_config_namespaces = []
def _known_ns(self, app):
ns = ["wsgi"]
ns = ['wsgi']
ns.extend(copykeys(app.toolboxes))
ns.extend(copykeys(app.namespaces))
ns.extend(copykeys(app.request_class.namespaces))
@@ -233,32 +233,32 @@ class Checker(object):
ns += self.extra_config_namespaces
for section, conf in app.config.items():
is_path_section = section.startswith("/")
is_path_section = section.startswith('/')
if is_path_section and isinstance(conf, dict):
for k, v in conf.items():
atoms = k.split(".")
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:
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))
'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))
'The config entry %r is invalid, '
'because the %r config namespace '
'is unknown.\n'
'section: [%s]' % (k, atoms[0], section))
warnings.warn(msg)
elif atoms[0] == "tools":
elif atoms[0] == 'tools':
if atoms[1] not in dir(cherrypy.tools):
msg = (
"The config entry %r may be invalid, "
"because the %r tool was not found.\n"
"section: [%s]" % (k, atoms[1], section))
'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):
@@ -282,17 +282,17 @@ class Checker(object):
continue
vtype = type(getattr(obj, name, None))
if vtype in b:
self.known_config_types[namespace + "." + name] = vtype
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")
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.")
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):
@@ -326,7 +326,7 @@ class Checker(object):
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 "
'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.")

View File

@@ -1,13 +1,13 @@
"""Compatibility code for using CherryPy with various versions of Python.
CherryPy 3.2 is compatible with Python versions 2.3+. This module provides a
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 new 'bytestr', 'unicodestr', and 'nativestr' attributes, as well as
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
@@ -15,18 +15,16 @@ 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
if sys.version_info >= (3, 0):
py3k = True
bytestr = bytes
unicodestr = str
nativestr = unicodestr
basestring = (bytes, str)
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.
@@ -49,18 +47,8 @@ if sys.version_info >= (3, 0):
if isinstance(n, bytes):
return n.decode(encoding)
return n
# type("")
from io import StringIO
# bytes:
from io import BytesIO as BytesIO
else:
# Python 2
py3k = False
bytestr = str
unicodestr = unicode
nativestr = bytestr
basestring = basestring
def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given
encoding.
@@ -96,24 +84,11 @@ else:
if isinstance(n, unicode):
return n.encode(encoding)
return n
try:
# type("")
from cStringIO import StringIO
except ImportError:
# type("")
from StringIO import StringIO
# bytes:
BytesIO = StringIO
def assert_native(n):
if not isinstance(n, nativestr):
raise TypeError("n must be a native str (got %s)" % type(n).__name__)
try:
set = set
except NameError:
from sets import Set as set
if not isinstance(n, str):
raise TypeError('n must be a native str (got %s)' % type(n).__name__)
try:
# Python 3.1+
@@ -127,27 +102,16 @@ except ImportError:
def base64_decode(n, encoding='ISO-8859-1'):
"""Return the native string base64-decoded (as a native string)."""
if isinstance(n, unicodestr):
if isinstance(n, six.text_type):
b = n.encode(encoding)
else:
b = n
b = _base64_decodebytes(b)
if nativestr is unicodestr:
if str is six.text_type:
return b.decode(encoding)
else:
return b
try:
# Python 2.5+
from hashlib import md5
except ImportError:
from md5 import new as md5
try:
# Python 2.5+
from hashlib import sha1 as sha
except ImportError:
from sha import new as sha
try:
sorted = sorted
@@ -174,16 +138,11 @@ try:
from urllib.request import parse_http_list, parse_keqv_list
except ImportError:
# Python 2
from urlparse import urljoin
from urllib import urlencode, urlopen
from urllib import quote, quote_plus
from urllib import unquote
from urllib2 import parse_http_list, parse_keqv_list
try:
from threading import local as threadlocal
except ImportError:
from cherrypy._cpthreadinglocal import local as threadlocal
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
@@ -220,7 +179,7 @@ try:
import builtins
except ImportError:
# Python 2
import __builtin__ as builtins
import __builtin__ as builtins # noqa
try:
# Python 2. We try Python 2 first clients on Python 2
@@ -231,13 +190,13 @@ try:
from BaseHTTPServer import BaseHTTPRequestHandler
except ImportError:
# Python 3
from http.cookies import SimpleCookie, CookieError
from http.client import BadStatusLine, HTTPConnection, IncompleteRead
from http.client import NotConnected
from http.server import BaseHTTPRequestHandler
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 py3k:
if six.PY3:
try:
from http.client import HTTPSConnection
except ImportError:
@@ -256,29 +215,6 @@ except NameError:
# Python 3
xrange = range
import threading
if hasattr(threading.Thread, "daemon"):
# Python 2.6+
def get_daemon(t):
return t.daemon
def set_daemon(t, val):
t.daemon = val
else:
def get_daemon(t):
return t.isDaemon()
def set_daemon(t, val):
t.setDaemon(val)
try:
from email.utils import formatdate
def HTTPDate(timeval=None):
return formatdate(timeval, usegmt=True)
except ImportError:
from rfc822 import formatdate as HTTPDate
try:
# Python 3
from urllib.parse import unquote as parse_unquote
@@ -316,7 +252,7 @@ except ImportError:
def _json_encode(s):
raise ValueError('No JSON library is available')
finally:
if json and py3k:
if json and six.PY3:
# The two Python 3 implementations (simplejson/json)
# outputs str. We need bytes.
def json_encode(value):
@@ -325,31 +261,22 @@ finally:
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
import pickle # noqa
try:
os.urandom(20)
import binascii
def random20():
return binascii.hexlify(os.urandom(20)).decode('ascii')
except (AttributeError, NotImplementedError):
import random
# os.urandom not available until Python 2.4. Fall back to random.random.
def random20():
return sha('%s' % random.random()).hexdigest()
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
from thread import get_ident as get_thread_ident # noqa
try:
# Python 3
@@ -367,17 +294,41 @@ else:
Timer = threading._Timer
Event = threading._Event
# Prior to Python 2.6, the Thread class did not have a .daemon property.
# This mix-in adds that property.
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',
}
class SetDaemonProperty:
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)
def __get_daemon(self):
return self.isDaemon()
def __set_daemon(self, daemon):
self.setDaemon(daemon)
if sys.version_info < (2, 6):
daemon = property(__get_daemon, __set_daemon)
return args

View File

File diff suppressed because it is too large Load Diff

View File

@@ -46,21 +46,21 @@ To declare global configuration entries, place them in a [global] section.
You may also declare config entries directly on the classes and methods
(page handlers) that make up your CherryPy application via the ``_cp_config``
attribute. For example::
attribute, set with the ``cherrypy.config`` decorator. For example::
@cherrypy.config(**{'tools.gzip.on': True})
class Demo:
_cp_config = {'tools.gzip.on': True}
@cherrypy.expose
@cherrypy.config(**{'request.show_tracebacks': False})
def index(self):
return "Hello world"
index.exposed = True
index._cp_config = {'request.show_tracebacks': False}
.. note::
This behavior is only guaranteed for the default dispatcher.
Other dispatchers may have different restrictions on where
you can attach _cp_config attributes.
you can attach config attributes.
Namespaces
@@ -119,7 +119,7 @@ style) context manager.
"""
import cherrypy
from cherrypy._cpcompat import set, basestring
from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import reprconf
# Deprecated in CherryPy 3.2--remove in 3.3
@@ -132,16 +132,16 @@ def merge(base, other):
If the given config is a filename, it will be appended to
the list of files to monitor for "autoreload" changes.
"""
if isinstance(other, basestring):
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 "
'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 "
'Wrap your config in another dict with paths as section '
"headers, for example: {'/': config}.")
base.setdefault(section, {}).update(value_map)
@@ -152,47 +152,59 @@ class Config(reprconf.Config):
def update(self, config):
"""Update self from a dict, file or filename."""
if isinstance(config, basestring):
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 isinstance(config.get('global'), dict):
if len(config) > 1:
cherrypy.checker.global_config_contained_paths = True
config = config["global"]
config = config['global']
if 'tools.staticdir.dir' in config:
config['tools.staticdir.section'] = "global"
config['tools.staticdir.section'] = 'global'
reprconf.Config._apply(self, config)
def __call__(self, *args, **kwargs):
@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.")
'The cherrypy.config decorator does not accept positional '
'arguments; you must use keyword arguments.')
def tool_decorator(f):
if not hasattr(f, "_cp_config"):
f._cp_config = {}
for k, v in kwargs.items():
f._cp_config[k] = v
_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": {
'staging': {
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': False,
'request.show_mismatched_params': False,
},
"production": {
'production': {
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
@@ -200,7 +212,7 @@ Config.environments = environments = {
'request.show_mismatched_params': False,
'log.screen': False,
},
"embedded": {
'embedded': {
# For use with CherryPy embedded in another deployment stack.
'engine.autoreload.on': False,
'checker.on': False,
@@ -211,7 +223,7 @@ Config.environments = environments = {
'engine.SIGHUP': None,
'engine.SIGTERM': None,
},
"test_suite": {
'test_suite': {
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
@@ -225,11 +237,11 @@ Config.environments = environments = {
def _server_namespace_handler(k, v):
"""Config handler for the "server" namespace."""
atoms = k.split(".", 1)
atoms = k.split('.', 1)
if len(atoms) > 1:
# Special-case config keys of the form 'server.servername.socket_port'
# to configure additional HTTP servers.
if not hasattr(cherrypy, "servers"):
if not hasattr(cherrypy, 'servers'):
cherrypy.servers = {}
servername, k = atoms
@@ -248,45 +260,19 @@ def _server_namespace_handler(k, v):
setattr(cherrypy.servers[servername], k, v)
else:
setattr(cherrypy.server, k, v)
Config.namespaces["server"] = _server_namespace_handler
Config.namespaces['server'] = _server_namespace_handler
def _engine_namespace_handler(k, v):
"""Backward compatibility handler for the "engine" namespace."""
"""Config handler for the "engine" namespace."""
engine = cherrypy.engine
deprecated = {
'autoreload_on': 'autoreload.on',
'autoreload_frequency': 'autoreload.frequency',
'autoreload_match': 'autoreload.match',
'reload_files': 'autoreload.files',
'deadlock_poll_freq': 'timeout_monitor.frequency'
}
if k in deprecated:
engine.log(
'WARNING: Use of engine.%s is deprecated and will be removed in a '
'future version. Use engine.%s instead.' % (k, deprecated[k]))
if k == 'autoreload_on':
if v:
engine.autoreload.subscribe()
else:
engine.autoreload.unsubscribe()
elif k == 'autoreload_frequency':
engine.autoreload.frequency = v
elif k == 'autoreload_match':
engine.autoreload.match = v
elif k == 'reload_files':
engine.autoreload.files = set(v)
elif k == 'deadlock_poll_freq':
engine.timeout_monitor.frequency = v
elif k == 'SIGHUP':
engine.listeners['SIGHUP'] = set([v])
if k == 'SIGHUP':
engine.subscribe('SIGHUP', v)
elif k == 'SIGTERM':
engine.listeners['SIGTERM'] = set([v])
elif "." in k:
plugin, attrname = k.split(".", 1)
engine.subscribe('SIGTERM', v)
elif '.' in k:
plugin, attrname = k.split('.', 1)
plugin = getattr(engine, plugin)
if attrname == 'on':
if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
@@ -301,7 +287,7 @@ def _engine_namespace_handler(k, v):
setattr(plugin, attrname, v)
else:
setattr(engine, k, v)
Config.namespaces["engine"] = _engine_namespace_handler
Config.namespaces['engine'] = _engine_namespace_handler
def _tree_namespace_handler(k, v):
@@ -309,9 +295,9 @@ def _tree_namespace_handler(k, v):
if isinstance(v, dict):
for script_name, app in v.items():
cherrypy.tree.graft(app, script_name)
cherrypy.engine.log("Mounted: %s on %s" %
(app, script_name or "/"))
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
cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/'))
Config.namespaces['tree'] = _tree_namespace_handler

View File

@@ -18,7 +18,6 @@ except AttributeError:
classtype = type
import cherrypy
from cherrypy._cpcompat import set
class PageHandler(object):
@@ -40,7 +39,7 @@ class PageHandler(object):
args = property(
get_args,
set_args,
doc="The ordered args should be accessible from post dispatch hooks"
doc='The ordered args should be accessible from post dispatch hooks'
)
def get_kwargs(self):
@@ -53,7 +52,7 @@ class PageHandler(object):
kwargs = property(
get_kwargs,
set_kwargs,
doc="The named kwargs should be accessible from post dispatch hooks"
doc='The named kwargs should be accessible from post dispatch hooks'
)
def __call__(self):
@@ -154,7 +153,7 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
# arguments it's definitely a 404.
message = None
if show_mismatched_params:
message = "Missing parameters: %s" % ",".join(missing_args)
message = 'Missing parameters: %s' % ','.join(missing_args)
raise cherrypy.HTTPError(404, message=message)
# the extra positional arguments come from the path - 404 Not Found
@@ -176,8 +175,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
message = None
if show_mismatched_params:
message = "Multiple values for parameters: "\
"%s" % ",".join(multiple_args)
message = 'Multiple values for parameters: '\
'%s' % ','.join(multiple_args)
raise cherrypy.HTTPError(error, message=message)
if not varkw and varkw_usage > 0:
@@ -187,8 +186,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_qs_params:
message = None
if show_mismatched_params:
message = "Unexpected query string "\
"parameters: %s" % ", ".join(extra_qs_params)
message = 'Unexpected query string '\
'parameters: %s' % ', '.join(extra_qs_params)
raise cherrypy.HTTPError(404, message=message)
# If there were any extra body parameters, it's a 400 Not Found
@@ -196,8 +195,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_body_params:
message = None
if show_mismatched_params:
message = "Unexpected body parameters: "\
"%s" % ", ".join(extra_body_params)
message = 'Unexpected body parameters: '\
'%s' % ', '.join(extra_body_params)
raise cherrypy.HTTPError(400, message=message)
@@ -245,14 +244,14 @@ if sys.version_info < (3, 0):
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.")
'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.")
raise ValueError('The translate argument must be a dict.')
class Dispatcher(object):
@@ -290,7 +289,7 @@ class Dispatcher(object):
if func:
# Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath]
vpath = [x.replace('%2F', '/') for x in vpath]
request.handler = LateParamPageHandler(func, *vpath)
else:
request.handler = cherrypy.NotFound()
@@ -324,10 +323,10 @@ class Dispatcher(object):
fullpath_len = len(fullpath)
segleft = fullpath_len
nodeconf = {}
if hasattr(root, "_cp_config"):
if hasattr(root, '_cp_config'):
nodeconf.update(root._cp_config)
if "/" in app.config:
nodeconf.update(app.config["/"])
if '/' in app.config:
nodeconf.update(app.config['/'])
object_trail = [['root', root, nodeconf, segleft]]
node = root
@@ -362,9 +361,9 @@ class Dispatcher(object):
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)
'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
@@ -376,7 +375,7 @@ class Dispatcher(object):
if node is not None:
# Get _cp_config attached to this node.
if hasattr(node, "_cp_config"):
if hasattr(node, '_cp_config'):
nodeconf.update(node._cp_config)
# Mix in values from app.config for this path.
@@ -415,16 +414,16 @@ class Dispatcher(object):
continue
# Try a "default" method on the current leaf.
if hasattr(candidate, "default"):
if hasattr(candidate, 'default'):
defhandler = candidate.default
if getattr(defhandler, 'exposed', False):
# Insert any extra _cp_config from the default handler.
conf = getattr(defhandler, "_cp_config", {})
conf = getattr(defhandler, '_cp_config', {})
object_trail.insert(
i + 1, ["default", defhandler, conf, segleft])
i + 1, ['default', defhandler, conf, segleft])
request.config = set_conf()
# See https://bitbucket.org/cherrypy/cherrypy/issue/613
request.is_index = path.endswith("/")
# 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
@@ -471,23 +470,23 @@ class MethodDispatcher(Dispatcher):
if resource:
# Set Allow header
avail = [m for m in dir(resource) if m.isupper()]
if "GET" in avail and "HEAD" not in avail:
avail.append("HEAD")
if 'GET' in avail and 'HEAD' not in avail:
avail.append('HEAD')
avail.sort()
cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
cherrypy.serving.response.headers['Allow'] = ', '.join(avail)
# Find the subhandler
meth = request.method.upper()
func = getattr(resource, meth, None)
if func is None and meth == "HEAD":
func = getattr(resource, "GET", None)
if func is None and meth == 'HEAD':
func = getattr(resource, 'GET', None)
if func:
# Grab any _cp_config on the subhandler.
if hasattr(func, "_cp_config"):
if hasattr(func, '_cp_config'):
request.config.update(func._cp_config)
# Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath]
vpath = [x.replace('%2F', '/') for x in vpath]
request.handler = LateParamPageHandler(func, *vpath)
else:
request.handler = cherrypy.HTTPError(405)
@@ -555,28 +554,28 @@ class RoutesDispatcher(object):
# Get config for the root object/path.
request.config = base = cherrypy.config.copy()
curpath = ""
curpath = ''
def merge(nodeconf):
if 'tools.staticdir.dir' in nodeconf:
nodeconf['tools.staticdir.section'] = curpath or "/"
nodeconf['tools.staticdir.section'] = curpath or '/'
base.update(nodeconf)
app = request.app
root = app.root
if hasattr(root, "_cp_config"):
if hasattr(root, '_cp_config'):
merge(root._cp_config)
if "/" in app.config:
merge(app.config["/"])
if '/' in app.config:
merge(app.config['/'])
# Mix in values from app.config.
atoms = [x for x in path_info.split("/") if x]
atoms = [x for x in path_info.split('/') if x]
if atoms:
last = atoms.pop()
else:
last = None
for atom in atoms:
curpath = "/".join((curpath, atom))
curpath = '/'.join((curpath, atom))
if curpath in app.config:
merge(app.config[curpath])
@@ -588,14 +587,14 @@ class RoutesDispatcher(object):
if isinstance(controller, classtype):
controller = controller()
# Get config from the controller.
if hasattr(controller, "_cp_config"):
if hasattr(controller, '_cp_config'):
merge(controller._cp_config)
action = result.get('action')
if action is not None:
handler = getattr(controller, action, None)
# Get config from the handler
if hasattr(handler, "_cp_config"):
if hasattr(handler, '_cp_config'):
merge(handler._cp_config)
else:
handler = controller
@@ -603,7 +602,7 @@ class RoutesDispatcher(object):
# Do the last path atom here so it can
# override the controller's _cp_config.
if last:
curpath = "/".join((curpath, last))
curpath = '/'.join((curpath, last))
if curpath in app.config:
merge(app.config[curpath])
@@ -667,16 +666,16 @@ def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
domain = header('Host', '')
if use_x_forwarded_host:
domain = header("X-Forwarded-Host", domain)
domain = header('X-Forwarded-Host', domain)
prefix = domains.get(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://bitbucket.org/cherrypy/cherrypy/issue/614.
# https://github.com/cherrypy/cherrypy/issues/614.
section = request.config.get('tools.staticdir.section')
if section:
section = section[len(prefix):]

View File

@@ -106,19 +106,24 @@ send an e-mail containing the error::
'Error in your web app',
_cperror.format_exc())
@cherrypy.config(**{'request.error_response': handle_error})
class Root:
_cp_config = {'request.error_response': handle_error}
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 cherrypy._cpcompat import basestring, bytestr, iteritems, ntob
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
@@ -145,14 +150,14 @@ class InternalRedirect(CherryPyException):
URL.
"""
def __init__(self, path, query_string=""):
def __init__(self, path, query_string=''):
import cherrypy
self.request = cherrypy.serving.request
self.query_string = query_string
if "?" in path:
if '?' in path:
# Separate any params included in the path
path, self.query_string = path.split("?", 1)
path, self.query_string = path.split('?', 1)
# Note that urljoin will "do the right thing" whether url is:
# 1. a URL relative to root (e.g. "/dummy")
@@ -206,21 +211,10 @@ class HTTPRedirect(CherryPyException):
import cherrypy
request = cherrypy.serving.request
if isinstance(urls, basestring):
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
self.urls = [tonative(url, encoding or self.encoding) for url in urls]
# RFC 2616 indicates a 301 response code fits our goal; however,
# browser support for 301 is quite messy. Do 302/303 instead. See
@@ -233,10 +227,10 @@ class HTTPRedirect(CherryPyException):
else:
status = int(status)
if status < 300 or status > 399:
raise ValueError("status must be between 300 and 399.")
raise ValueError('status must be between 300 and 399.')
self.status = status
CherryPyException.__init__(self, abs_urls, status)
CherryPyException.__init__(self, self.urls, status)
def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent
@@ -250,7 +244,7 @@ class HTTPRedirect(CherryPyException):
response.status = status = self.status
if status in (300, 301, 302, 303, 307):
response.headers['Content-Type'] = "text/html;charset=utf-8"
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]
@@ -259,16 +253,15 @@ class HTTPRedirect(CherryPyException):
# 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 ",
300: 'This resource can be found at ',
301: 'This resource has permanently moved to ',
302: 'This resource resides temporarily at ',
303: 'This resource can be found at ',
307: 'This resource has moved temporarily to ',
}[status]
msg += '<a href=%s>%s</a>.'
from xml.sax import saxutils
msgs = [msg % (saxutils.quoteattr(u), u) for u in self.urls]
response.body = ntob("<br />\n".join(msgs), 'utf-8')
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)
@@ -293,12 +286,12 @@ class HTTPRedirect(CherryPyException):
elif status == 305:
# Use Proxy.
# self.urls[0] should be the URI of the proxy.
response.headers['Location'] = self.urls[0]
response.headers['Location'] = ntob(self.urls[0], 'utf-8')
response.body = None
# Previous code may have set C-L, so we have to reset it.
response.headers.pop('Content-Length', None)
else:
raise ValueError("The %s status code is unknown." % status)
raise ValueError('The %s status code is unknown.' % status)
def __call__(self):
"""Use this exception as a request.handler (raise self)."""
@@ -314,9 +307,9 @@ def clean_headers(status):
# 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"]:
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]
@@ -327,8 +320,8 @@ def clean_headers(status):
# specifies the current length of the selected resource.
# A response with status code 206 (Partial Content) MUST NOT
# include a Content-Range field with a byte-range- resp-spec of "*".
if "Content-Range" in respheaders:
del respheaders["Content-Range"]
if 'Content-Range' in respheaders:
del respheaders['Content-Range']
class HTTPError(CherryPyException):
@@ -368,7 +361,7 @@ class HTTPError(CherryPyException):
raise self.__class__(500, _exc_info()[1].args[0])
if self.code < 400 or self.code > 599:
raise ValueError("status must be between 400 and 599.")
raise ValueError('status must be between 400 and 599.')
# See http://www.python.org/dev/peps/pep-0352/
# self.message = message
@@ -410,6 +403,15 @@ class HTTPError(CherryPyException):
"""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):
@@ -477,7 +479,7 @@ def get_error_page(status, **kwargs):
# We can't use setdefault here, because some
# callers send None for kwarg values.
if kwargs.get('status') is None:
kwargs['status'] = "%s %s" % (code, reason)
kwargs['status'] = '%s %s' % (code, reason)
if kwargs.get('message') is None:
kwargs['message'] = message
if kwargs.get('traceback') is None:
@@ -487,7 +489,7 @@ def get_error_page(status, **kwargs):
for k, v in iteritems(kwargs):
if v is None:
kwargs[k] = ""
kwargs[k] = ''
else:
kwargs[k] = _escape(kwargs[k])
@@ -509,12 +511,12 @@ def get_error_page(status, **kwargs):
if cherrypy.lib.is_iterator(result):
from cherrypy.lib.encoding import UTF8StreamEncoder
return UTF8StreamEncoder(result)
elif isinstance(result, cherrypy._cpcompat.unicodestr):
elif isinstance(result, six.text_type):
return result.encode('utf-8')
else:
if not isinstance(result, cherrypy._cpcompat.bytestr):
if not isinstance(result, bytes):
raise ValueError('error page function did not '
'return a bytestring, unicodestring or an '
'return a bytestring, six.text_typeing or an '
'iterator - returned object of type %s.'
% (type(result).__name__))
return result
@@ -525,12 +527,12 @@ def get_error_page(status, **kwargs):
e = _format_exception(*_exc_info())[-1]
m = kwargs['message']
if m:
m += "<br />"
m += "In addition, the custom error page failed:\n<br />%s" % e
m += '<br />'
m += 'In addition, the custom error page failed:\n<br />%s' % e
kwargs['message'] = m
response = cherrypy.serving.response
response.headers['Content-Type'] = "text/html;charset=utf-8"
response.headers['Content-Type'] = 'text/html;charset=utf-8'
result = template % kwargs
return result.encode('utf-8')
@@ -562,7 +564,7 @@ def _be_ie_unfriendly(status):
if l and l < s:
# IN ADDITION: the response must be written to IE
# in one chunk or it will still get replaced! Bah.
content = content + (ntob(" ") * (s - l))
content = content + (ntob(' ') * (s - l))
response.body = content
response.headers['Content-Length'] = str(len(content))
@@ -573,9 +575,9 @@ def format_exc(exc=None):
if exc is None:
exc = _exc_info()
if exc == (None, None, None):
return ""
return ''
import traceback
return "".join(traceback.format_exception(*exc))
return ''.join(traceback.format_exception(*exc))
finally:
del exc
@@ -597,13 +599,13 @@ def bare_error(extrabody=None):
# 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.")
body = ntob('Unrecoverable error in the server.')
if extrabody is not None:
if not isinstance(extrabody, bytestr):
if not isinstance(extrabody, bytes):
extrabody = extrabody.encode('utf-8')
body += ntob("\n") + extrabody
body += ntob('\n') + extrabody
return (ntob("500 Internal Server Error"),
return (ntob('500 Internal Server Error'),
[(ntob('Content-Type'), ntob('text/plain')),
(ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))],
[body])

View File

@@ -109,15 +109,19 @@ the "log.error_file" config entry, for example).
import datetime
import logging
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
logging.Logger.manager.emittedNoHandlerWarning = 1
logfmt = logging.Formatter("%(message)s")
import os
import sys
import six
import cherrypy
from cherrypy import _cperror
from cherrypy._cpcompat import ntob, py3k
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):
@@ -151,12 +155,11 @@ class LogManager(object):
access_log = None
"""The actual :class:`logging.Logger` instance for access messages."""
if py3k:
access_log_format = \
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
else:
access_log_format = \
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
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.
@@ -169,17 +172,17 @@ class LogManager(object):
cherrypy.access.<appid>
"""
def __init__(self, appid=None, logger_root="cherrypy"):
def __init__(self, appid=None, logger_root='cherrypy'):
self.logger_root = logger_root
self.appid = appid
if appid is None:
self.error_log = logging.getLogger("%s.error" % logger_root)
self.access_log = logging.getLogger("%s.access" % logger_root)
self.error_log = logging.getLogger('%s.error' % logger_root)
self.access_log = logging.getLogger('%s.access' % logger_root)
else:
self.error_log = logging.getLogger(
"%s.error.%s" % (logger_root, appid))
'%s.error.%s' % (logger_root, appid))
self.access_log = logging.getLogger(
"%s.access.%s" % (logger_root, appid))
'%s.access.%s' % (logger_root, appid))
self.error_log.setLevel(logging.INFO)
self.access_log.setLevel(logging.INFO)
@@ -243,24 +246,24 @@ class LogManager(object):
outheaders = response.headers
inheaders = request.headers
if response.output_status is None:
status = "-"
status = '-'
else:
status = response.output_status.split(ntob(" "), 1)[0]
if py3k:
status = response.output_status.split(ntob(' '), 1)[0]
if six.PY3:
status = status.decode('ISO-8859-1')
atoms = {'h': remote.name or remote.ip,
'l': '-',
'u': getattr(request, "login", None) or "-",
'u': getattr(request, 'login', None) or '-',
't': self.time(),
'r': request.request_line,
's': status,
'b': dict.get(outheaders, 'Content-Length', '') or "-",
'b': dict.get(outheaders, 'Content-Length', '') or '-',
'f': dict.get(inheaders, 'Referer', ''),
'a': dict.get(inheaders, 'User-Agent', ''),
'o': dict.get(inheaders, 'Host', '-'),
}
if py3k:
if six.PY3:
for k, v in atoms.items():
if not isinstance(v, str):
v = str(v)
@@ -284,7 +287,7 @@ class LogManager(object):
self(traceback=True)
else:
for k, v in atoms.items():
if isinstance(v, unicode):
if isinstance(v, six.text_type):
v = v.encode('utf8')
elif not isinstance(v, str):
v = str(v)
@@ -311,26 +314,26 @@ class LogManager(object):
def _get_builtin_handler(self, log, key):
for h in log.handlers:
if getattr(h, "_cpbuiltin", None) == key:
if getattr(h, '_cpbuiltin', None) == key:
return h
# ------------------------- Screen handlers ------------------------- #
def _set_screen_handler(self, log, enable, stream=None):
h = self._get_builtin_handler(log, "screen")
h = self._get_builtin_handler(log, 'screen')
if enable:
if not h:
if stream is None:
stream = sys.stderr
h = logging.StreamHandler(stream)
h.setFormatter(logfmt)
h._cpbuiltin = "screen"
h._cpbuiltin = 'screen'
log.addHandler(h)
elif h:
log.handlers.remove(h)
def _get_screen(self):
h = self._get_builtin_handler
has_h = h(self.error_log, "screen") or h(self.access_log, "screen")
has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen')
return bool(has_h)
def _set_screen(self, newvalue):
@@ -348,11 +351,11 @@ class LogManager(object):
def _add_builtin_file_handler(self, log, fname):
h = logging.FileHandler(fname)
h.setFormatter(logfmt)
h._cpbuiltin = "file"
h._cpbuiltin = 'file'
log.addHandler(h)
def _set_file_handler(self, log, filename):
h = self._get_builtin_handler(log, "file")
h = self._get_builtin_handler(log, 'file')
if filename:
if h:
if h.baseFilename != os.path.abspath(filename):
@@ -367,7 +370,7 @@ class LogManager(object):
log.handlers.remove(h)
def _get_error_file(self):
h = self._get_builtin_handler(self.error_log, "file")
h = self._get_builtin_handler(self.error_log, 'file')
if h:
return h.baseFilename
return ''
@@ -382,7 +385,7 @@ class LogManager(object):
""")
def _get_access_file(self):
h = self._get_builtin_handler(self.access_log, "file")
h = self._get_builtin_handler(self.access_log, 'file')
if h:
return h.baseFilename
return ''
@@ -399,18 +402,18 @@ class LogManager(object):
# ------------------------- WSGI handlers ------------------------- #
def _set_wsgi_handler(self, log, enable):
h = self._get_builtin_handler(log, "wsgi")
h = self._get_builtin_handler(log, 'wsgi')
if enable:
if not h:
h = WSGIErrorHandler()
h.setFormatter(logfmt)
h._cpbuiltin = "wsgi"
h._cpbuiltin = 'wsgi'
log.addHandler(h)
elif h:
log.handlers.remove(h)
def _get_wsgi(self):
return bool(self._get_builtin_handler(self.error_log, "wsgi"))
return bool(self._get_builtin_handler(self.error_log, 'wsgi'))
def _set_wsgi(self, newvalue):
self._set_wsgi_handler(self.error_log, newvalue)
@@ -446,16 +449,16 @@ class WSGIErrorHandler(logging.Handler):
else:
try:
msg = self.format(record)
fs = "%s\n"
fs = '%s\n'
import types
# if no unicode support...
if not hasattr(types, "UnicodeType"):
if not hasattr(types, 'UnicodeType'):
stream.write(fs % msg)
else:
try:
stream.write(fs % msg)
except UnicodeError:
stream.write(fs % msg.encode("UTF-8"))
stream.write(fs % msg.encode('UTF-8'))
self.flush()
except:
self.handleError(record)

View File

@@ -55,11 +55,14 @@ 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 BytesIO, copyitems, ntob
from cherrypy._cpcompat import copyitems, ntob
from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import httputil
@@ -85,14 +88,14 @@ def setup(req):
func()
cherrypy.config.update({'log.screen': False,
"tools.ignore_headers.on": True,
"tools.ignore_headers.headers": ['Range'],
'tools.ignore_headers.on': True,
'tools.ignore_headers.headers': ['Range'],
})
engine = cherrypy.engine
if hasattr(engine, "signal_handler"):
if hasattr(engine, 'signal_handler'):
engine.signal_handler.unsubscribe()
if hasattr(engine, "console_control_handler"):
if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.unsubscribe()
engine.autoreload.unsubscribe()
cherrypy.server.unsubscribe()
@@ -146,10 +149,10 @@ def handler(req):
# Obtain a Request object from CherryPy
local = req.connection.local_addr
local = httputil.Host(
local[0], local[1], req.connection.local_host or "")
local[0], local[1], req.connection.local_host or '')
remote = req.connection.remote_addr
remote = httputil.Host(
remote[0], remote[1], req.connection.remote_host or "")
remote[0], remote[1], req.connection.remote_host or '')
scheme = req.parsed_uri[0] or 'http'
req.get_basic_auth_pw()
@@ -162,7 +165,7 @@ def handler(req):
except AttributeError:
bad_value = ("You must provide a PythonOption '%s', "
"either 'on' or 'off', when running a version "
"of mod_python < 3.1")
'of mod_python < 3.1')
threaded = options.get('multithread', '').lower()
if threaded == 'on':
@@ -170,7 +173,7 @@ def handler(req):
elif threaded == 'off':
threaded = False
else:
raise ValueError(bad_value % "multithread")
raise ValueError(bad_value % 'multithread')
forked = options.get('multiprocess', '').lower()
if forked == 'on':
@@ -178,16 +181,16 @@ def handler(req):
elif forked == 'off':
forked = False
else:
raise ValueError(bad_value % "multiprocess")
raise ValueError(bad_value % 'multiprocess')
sn = cherrypy.tree.script_name(req.uri or "/")
sn = cherrypy.tree.script_name(req.uri or '/')
if sn is None:
send_response(req, '404 Not Found', [], '')
else:
app = cherrypy.tree.apps[sn]
method = req.method
path = req.uri
qs = req.args or ""
qs = req.args or ''
reqproto = req.protocol
headers = copyitems(req.headers_in)
rfile = _ReadOnlyRequest(req)
@@ -197,7 +200,7 @@ def handler(req):
redirections = []
while True:
request, response = app.get_serving(local, remote, scheme,
"HTTP/1.1")
'HTTP/1.1')
request.login = req.user
request.multithread = bool(threaded)
request.multiprocess = bool(forked)
@@ -216,20 +219,20 @@ def handler(req):
if not recursive:
if ir.path in redirections:
raise RuntimeError(
"InternalRedirector visited the same URL "
"twice: %r" % ir.path)
'InternalRedirector visited the same URL '
'twice: %r' % ir.path)
else:
# Add the *previous* path_info + qs to
# redirections.
if qs:
qs = "?" + qs
qs = '?' + qs
redirections.append(sn + path + qs)
# Munge environment and try again.
method = "GET"
method = 'GET'
path = ir.path
qs = ir.query_string
rfile = BytesIO()
rfile = io.BytesIO()
send_response(
req, response.output_status, response.header_list,
@@ -249,7 +252,7 @@ def send_response(req, status, headers, body, stream=False):
req.status = int(status[:3])
# Set response headers
req.content_type = "text/plain"
req.content_type = 'text/plain'
for header, value in headers:
if header.lower() == 'content-type':
req.content_type = value
@@ -261,7 +264,7 @@ def send_response(req, status, headers, body, stream=False):
req.flush()
# Set response body
if isinstance(body, basestring):
if isinstance(body, text_or_bytes):
req.write(body)
else:
for seg in body:
@@ -269,8 +272,6 @@ def send_response(req, status, headers, body, stream=False):
# --------------- Startup tools for CherryPy + mod_python --------------- #
import os
import re
try:
import subprocess
@@ -285,13 +286,13 @@ except ImportError:
return pipeout
def read_process(cmd, args=""):
fullcmd = "%s %s" % (cmd, args)
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)"),
ntob('(not recognized|No such file|not found)'),
firstline,
re.IGNORECASE
)
@@ -320,8 +321,8 @@ LoadModule python_module modules/mod_python.so
</Location>
"""
def __init__(self, loc="/", port=80, opts=None, apache_path="apache",
handler="cherrypy._cpmodpy::handler"):
def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
handler='cherrypy._cpmodpy::handler'):
self.loc = loc
self.port = port
self.opts = opts
@@ -329,25 +330,25 @@ LoadModule python_module modules/mod_python.so
self.handler = handler
def start(self):
opts = "".join([" PythonOption %s %s\n" % (k, v)
opts = ''.join([' PythonOption %s %s\n' % (k, v)
for k, v in self.opts])
conf_data = self.template % {"port": self.port,
"loc": self.loc,
"opts": opts,
"handler": self.handler,
conf_data = self.template % {'port': self.port,
'loc': self.loc,
'opts': opts,
'handler': self.handler,
}
mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf')
f = open(mpconf, 'wb')
try:
f.write(conf_data)
finally:
f.close()
response = read_process(self.apache_path, "-k start -f %s" % mpconf)
response = read_process(self.apache_path, '-k start -f %s' % mpconf)
self.ready = True
return response
def stop(self):
os.popen("apache -k stop")
os.popen('apache -k stop')
self.ready = False

View File

@@ -2,9 +2,9 @@
import logging
import sys
import io
import cherrypy
from cherrypy._cpcompat import BytesIO
from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import httputil
from cherrypy import wsgiserver
@@ -19,19 +19,19 @@ class NativeGateway(wsgiserver.Gateway):
try:
# Obtain a Request object from CherryPy
local = req.server.bind_addr
local = httputil.Host(local[0], local[1], "")
local = httputil.Host(local[0], local[1], '')
remote = req.conn.remote_addr, req.conn.remote_port
remote = httputil.Host(remote[0], remote[1], "")
remote = httputil.Host(remote[0], remote[1], '')
scheme = req.scheme
sn = cherrypy.tree.script_name(req.uri or "/")
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 ""
qs = req.qs or ''
headers = req.inheaders.items()
rfile = req.rfile
prev = None
@@ -40,7 +40,7 @@ class NativeGateway(wsgiserver.Gateway):
redirections = []
while True:
request, response = app.get_serving(
local, remote, scheme, "HTTP/1.1")
local, remote, scheme, 'HTTP/1.1')
request.multithread = True
request.multiprocess = False
request.app = app
@@ -60,20 +60,20 @@ class NativeGateway(wsgiserver.Gateway):
if not self.recursive:
if ir.path in redirections:
raise RuntimeError(
"InternalRedirector visited the same "
"URL twice: %r" % ir.path)
'InternalRedirector visited the same '
'URL twice: %r' % ir.path)
else:
# Add the *previous* path_info + qs to
# redirections.
if qs:
qs = "?" + qs
qs = '?' + qs
redirections.append(sn + path + qs)
# Munge environment and try again.
method = "GET"
method = 'GET'
path = ir.path
qs = ir.query_string
rfile = BytesIO()
rfile = io.BytesIO()
self.send_response(
response.output_status, response.header_list,
@@ -91,7 +91,7 @@ class NativeGateway(wsgiserver.Gateway):
req = self.req
# Set response status
req.status = str(status or "500 Server Error")
req.status = str(status or '500 Server Error')
# Set response headers
for header, value in headers:

View File

@@ -132,7 +132,7 @@ except ImportError:
return ntob('').join(atoms)
import cherrypy
from cherrypy._cpcompat import basestring, ntob, ntou
from cherrypy._cpcompat import text_or_bytes, ntob, ntou
from cherrypy.lib import httputil
@@ -169,8 +169,8 @@ def process_urlencoded(entity):
break
else:
raise cherrypy.HTTPError(
400, "The request entity could not be decoded. The following "
"charsets were attempted: %s" % repr(entity.attempt_charsets))
400, 'The request entity could not be decoded. The following '
'charsets were attempted: %s' % repr(entity.attempt_charsets))
# Now that all values have been successfully parsed and decoded,
# apply them to the entity.params dict.
@@ -185,7 +185,7 @@ def process_urlencoded(entity):
def process_multipart(entity):
"""Read all multipart parts into entity.parts."""
ib = ""
ib = ''
if 'boundary' in entity.content_type.params:
# http://tools.ietf.org/html/rfc2046#section-5.1.1
# "The grammar for parameters on the Content-type field is such that it
@@ -193,7 +193,7 @@ def process_multipart(entity):
# on the Content-type line"
ib = entity.content_type.params['boundary'].strip('"')
if not re.match("^[ -~]{0,200}[!-~]$", ib):
if not re.match('^[ -~]{0,200}[!-~]$', ib):
raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
ib = ('--' + ib).encode('ascii')
@@ -428,7 +428,7 @@ class Entity(object):
# Copy the class 'attempt_charsets', prepending any Content-Type
# charset
dec = self.content_type.params.get("charset", None)
dec = self.content_type.params.get('charset', None)
if dec:
self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
if c != dec]
@@ -469,8 +469,8 @@ class Entity(object):
# The 'type' attribute is deprecated in 3.2; remove it in 3.3.
type = property(
lambda self: self.content_type,
doc="A deprecated alias for "
":attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`."
doc='A deprecated alias for '
':attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`.'
)
def read(self, size=None, fp_out=None):
@@ -520,8 +520,26 @@ class Entity(object):
self.file.seek(0)
else:
value = self.value
value = self.decode_entity(value)
return value
def decode_entity(self , value):
"""Return a given byte encoded value as a string"""
for charset in self.attempt_charsets:
try:
value = value.decode(charset)
except UnicodeDecodeError:
pass
else:
self.charset = charset
return value
else:
raise cherrypy.HTTPError(
400,
'The request entity could not be decoded. The following '
'charsets were attempted: %s' % repr(self.attempt_charsets)
)
def process(self):
"""Execute the best-match processor for the given media type."""
proc = None
@@ -595,18 +613,19 @@ class Part(Entity):
self.file = None
self.value = None
@classmethod
def from_fp(cls, fp, boundary):
headers = cls.read_headers(fp)
return cls(fp, headers, boundary)
from_fp = classmethod(from_fp)
@classmethod
def read_headers(cls, fp):
headers = httputil.HeaderMap()
while True:
line = fp.readline()
if not line:
# No more data--illegal end of headers
raise EOFError("Illegal end of headers.")
raise EOFError('Illegal end of headers.')
if line == ntob('\r\n') or line == ntob('\n'):
# Normal end of headers
@@ -618,17 +637,16 @@ class Part(Entity):
# It's a continuation line.
v = line.strip().decode('ISO-8859-1')
else:
k, v = line.split(ntob(":"), 1)
k, v = line.split(ntob(':'), 1)
k = k.strip().decode('ISO-8859-1')
v = v.strip().decode('ISO-8859-1')
existing = headers.get(k)
if existing:
v = ", ".join((existing, v))
v = ', '.join((existing, v))
headers[k] = v
return headers
read_headers = classmethod(read_headers)
def read_lines_to_boundary(self, fp_out=None):
"""Read bytes from self.fp and return or write them to a file.
@@ -640,16 +658,16 @@ class Part(Entity):
object that supports the 'write' method; all bytes read will be
written to the fp, and that fp is returned.
"""
endmarker = self.boundary + ntob("--")
delim = ntob("")
endmarker = self.boundary + ntob('--')
delim = ntob('')
prev_lf = True
lines = []
seen = 0
while True:
line = self.fp.readline(1 << 16)
if not line:
raise EOFError("Illegal end of multipart body.")
if line.startswith(ntob("--")) and prev_lf:
raise EOFError('Illegal end of multipart body.')
if line.startswith(ntob('--')) and prev_lf:
strippedline = line.strip()
if strippedline == self.boundary:
break
@@ -659,16 +677,16 @@ class Part(Entity):
line = delim + line
if line.endswith(ntob("\r\n")):
delim = ntob("\r\n")
if line.endswith(ntob('\r\n')):
delim = ntob('\r\n')
line = line[:-2]
prev_lf = True
elif line.endswith(ntob("\n")):
delim = ntob("\n")
elif line.endswith(ntob('\n')):
delim = ntob('\n')
line = line[:-1]
prev_lf = True
else:
delim = ntob("")
delim = ntob('')
prev_lf = False
if fp_out is None:
@@ -683,20 +701,7 @@ class Part(Entity):
if fp_out is None:
result = ntob('').join(lines)
for charset in self.attempt_charsets:
try:
result = result.decode(charset)
except UnicodeDecodeError:
pass
else:
self.charset = charset
return result
else:
raise cherrypy.HTTPError(
400,
"The request entity could not be decoded. The following "
"charsets were attempted: %s" % repr(self.attempt_charsets)
)
return result
else:
fp_out.seek(0)
return fp_out
@@ -710,7 +715,7 @@ class Part(Entity):
self.file = self.read_into_file()
else:
result = self.read_lines_to_boundary()
if isinstance(result, basestring):
if isinstance(result, text_or_bytes):
self.value = result
else:
self.file = result
@@ -727,19 +732,7 @@ class Part(Entity):
Entity.part_class = Part
try:
inf = float('inf')
except ValueError:
# Python 2.4 and lower
class Infinity(object):
def __cmp__(self, other):
return 1
def __sub__(self, other):
return self
inf = Infinity()
inf = float('inf')
comma_separated_headers = [
'Accept', 'Accept-Charset', 'Accept-Encoding',
@@ -834,7 +827,7 @@ class SizedReader:
if e.__class__.__name__ == 'MaxSizeExceeded':
# Post data is too big
raise cherrypy.HTTPError(
413, "Maximum request length: %r" % e.args[1])
413, 'Maximum request length: %r' % e.args[1])
else:
raise
if not data:
@@ -910,23 +903,23 @@ class SizedReader:
v = line.strip()
else:
try:
k, v = line.split(ntob(":"), 1)
k, v = line.split(ntob(':'), 1)
except ValueError:
raise ValueError("Illegal header line.")
raise ValueError('Illegal header line.')
k = k.strip().title()
v = v.strip()
if k in comma_separated_headers:
existing = self.trailers.get(envname)
if existing:
v = ntob(", ").join((existing, v))
v = ntob(', ').join((existing, v))
self.trailers[k] = v
except Exception:
e = sys.exc_info()[1]
if e.__class__.__name__ == 'MaxSizeExceeded':
# Post data is too big
raise cherrypy.HTTPError(
413, "Maximum request length: %r" % e.args[1])
413, 'Maximum request length: %r' % e.args[1])
else:
raise
@@ -940,7 +933,7 @@ class RequestBody(Entity):
# Don't parse the request body at all if the client didn't provide
# a Content-Type header. See
# https://bitbucket.org/cherrypy/cherrypy/issue/790
# https://github.com/cherrypy/cherrypy/issues/790
default_content_type = ''
"""This defines a default ``Content-Type`` to use if no Content-Type header
is given. The empty string is used for RequestBody, which results in the

View File

@@ -1,12 +1,12 @@
import os
import sys
import time
import warnings
import six
import cherrypy
from cherrypy._cpcompat import basestring, copykeys, ntob, unicodestr
from cherrypy._cpcompat import SimpleCookie, CookieError, py3k
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
@@ -41,11 +41,11 @@ class Hook(object):
self.callback = callback
if failsafe is None:
failsafe = getattr(callback, "failsafe", False)
failsafe = getattr(callback, 'failsafe', False)
self.failsafe = failsafe
if priority is None:
priority = getattr(callback, "priority", 50)
priority = getattr(callback, 'priority', 50)
self.priority = priority
self.kwargs = kwargs
@@ -64,10 +64,10 @@ class Hook(object):
def __repr__(self):
cls = self.__class__
return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)"
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)
', '.join(['%s=%r' % (k, v)
for k, v in self.kwargs.items()])))
@@ -124,7 +124,7 @@ class HookMap(dict):
def __repr__(self):
cls = self.__class__
return "%s.%s(points=%r)" % (
return '%s.%s(points=%r)' % (
cls.__module__,
cls.__name__,
copykeys(self)
@@ -138,8 +138,8 @@ def hooks_namespace(k, v):
# 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, basestring):
hookpoint = k.split('.', 1)[0]
if isinstance(v, text_or_bytes):
v = cherrypy.lib.attributes(v)
if not isinstance(v, Hook):
v = Hook(v)
@@ -199,23 +199,23 @@ class Request(object):
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."
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."
remote = httputil.Host('127.0.0.1', 1111)
'An httputil.Host(ip, port, hostname) object for the client socket.'
scheme = "http"
scheme = 'http'
"""
The protocol used between client and server. In most cases,
this will be either 'http' or 'https'."""
server_protocol = "HTTP/1.1"
server_protocol = 'HTTP/1.1'
"""
The HTTP version for which the HTTP server is at least
conditionally compliant."""
base = ""
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
@@ -223,13 +223,13 @@ class Request(object):
MUST NOT end in a slash."""
# Request-Line attributes
request_line = ""
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"
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
@@ -237,7 +237,7 @@ class Request(object):
servers and gateways may restrict the set of allowable methods.
CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
query_string = ""
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
@@ -312,7 +312,7 @@ class Request(object):
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")
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
@@ -341,7 +341,7 @@ class Request(object):
to a hierarchical arrangement of objects, starting at request.app.root.
See help(cherrypy.dispatch) for more information."""
script_name = ""
script_name = ''
"""
The 'mount point' of the application which is handling this request.
@@ -349,7 +349,7 @@ class Request(object):
the root of the URI, it MUST be an empty string (not "/").
"""
path_info = "/"
path_info = '/'
"""
The 'relative path' portion of the Request-URI. This is relative
to the script_name ('mount point') of the application which is
@@ -468,15 +468,15 @@ class Request(object):
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,
**{'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"):
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.
@@ -544,7 +544,7 @@ class Request(object):
self.error_response = cherrypy.HTTPError(500).set_response
self.method = method
path = path or "/"
path = path or '/'
self.query_string = query_string or ''
self.params = {}
@@ -600,11 +600,11 @@ class Request(object):
if self.show_tracebacks:
body = format_exc()
else:
body = ""
body = ''
r = bare_error(body)
response.output_status, response.header_list, response.body = r
if self.method == "HEAD":
if self.method == 'HEAD':
# HEAD requests MUST NOT return a message-body in the response.
response.body = []
@@ -696,14 +696,14 @@ class Request(object):
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." %
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 not py3k:
if six.PY2:
for key, value in p.items():
if isinstance(key, unicode):
if isinstance(key, six.text_type):
del p[key]
p[key.encode(self.query_string_encoding)] = value
self.params.update(p)
@@ -722,7 +722,7 @@ class Request(object):
# (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:
if '=?' in value:
dict.__setitem__(headers, name, httputil.decode_TEXT(value))
else:
dict.__setitem__(headers, name, value)
@@ -733,7 +733,7 @@ class Request(object):
try:
self.cookie.load(value)
except CookieError:
msg = "Illegal cookie name %s" % value.split('=')[0]
msg = 'Illegal cookie name %s' % value.split('=')[0]
raise cherrypy.HTTPError(400, msg)
if not dict.__contains__(headers, 'Host'):
@@ -746,7 +746,7 @@ class Request(object):
host = dict.get(headers, 'Host')
if not host:
host = self.local.name or self.local.ip
self.base = "%s://%s" % (self.scheme, host)
self.base = '%s://%s' % (self.scheme, host)
def get_resource(self, path):
"""Call a dispatcher (which sets self.handler and .config). (Core)"""
@@ -754,7 +754,7 @@ class Request(object):
# 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)
path, 'request.dispatch', self.dispatch)
# dispatch() should set self.handler and self.config
dispatch(path)
@@ -762,10 +762,10 @@ class Request(object):
def handle_error(self):
"""Handle the last unanticipated exception. (Core)"""
try:
self.hooks.run("before_error_response")
self.hooks.run('before_error_response')
if self.error_response:
self.error_response()
self.hooks.run("after_error_response")
self.hooks.run('after_error_response')
cherrypy.serving.response.finalize()
except cherrypy.HTTPRedirect:
inst = sys.exc_info()[1]
@@ -776,8 +776,8 @@ class Request(object):
def _get_body_params(self):
warnings.warn(
"body_params is deprecated in CherryPy 3.2, will be removed in "
"CherryPy 3.3.",
'body_params is deprecated in CherryPy 3.2, will be removed in '
'CherryPy 3.3.',
DeprecationWarning
)
return self.body.params
@@ -799,9 +799,9 @@ class ResponseBody(object):
"""The body of the HTTP response (the response entity)."""
if py3k:
unicode_err = ("Page handlers MUST return bytes. Use tools.encode "
"if you wish to return unicode.")
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:
@@ -812,10 +812,10 @@ class ResponseBody(object):
def __set__(self, obj, value):
# Convert the given value to an iterable object.
if py3k and isinstance(value, str):
if six.PY3 and isinstance(value, str):
raise ValueError(self.unicode_err)
if isinstance(value, basestring):
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.
@@ -824,7 +824,7 @@ class ResponseBody(object):
else:
# [''] doesn't evaluate to False, so replace it with [].
value = []
elif py3k and isinstance(value, list):
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):
@@ -842,7 +842,7 @@ class Response(object):
"""An HTTP Response, including status, headers, and body."""
status = ""
status = ''
"""The HTTP Status-Code and Reason-Phrase."""
header_list = []
@@ -893,20 +893,20 @@ class Response(object):
# 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),
'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, basestring):
if isinstance(self.body, text_or_bytes):
return self.body
newbody = []
for chunk in self.body:
if py3k and not isinstance(chunk, bytes):
if six.PY3 and not isinstance(chunk, bytes):
raise TypeError("Chunk %s is not of type 'bytes'." %
repr(chunk))
newbody.append(chunk)
@@ -924,9 +924,9 @@ class Response(object):
headers = self.headers
self.status = "%s %s" % (code, reason)
self.status = '%s %s' % (code, reason)
self.output_status = ntob(str(code), 'ascii') + \
ntob(" ") + headers.encode(reason)
ntob(' ') + headers.encode(reason)
if self.stream:
# The upshot: wsgiserver will chunk the response if
@@ -939,7 +939,7 @@ class Response(object):
# and 304 (not modified) responses MUST NOT
# include a message-body."
dict.pop(headers, 'Content-Length', None)
self.body = ntob("")
self.body = ntob('')
else:
# Responses which are not streamed should have a Content-Length,
# but allow user code to set Content-Length if desired.
@@ -952,14 +952,11 @@ class Response(object):
cookie = self.cookie.output()
if cookie:
for line in cookie.split("\n"):
if line.endswith("\r"):
# Python 2.4 emits cookies joined by LF but 2.5+ by CRLF.
line = line[:-1]
name, value = line.split(": ", 1)
if isinstance(name, unicodestr):
name = name.encode("ISO-8859-1")
if isinstance(value, unicodestr):
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))

View File

@@ -1,10 +1,10 @@
"""Manage HTTP servers with CherryPy."""
import warnings
import six
import cherrypy
from cherrypy.lib import attributes
from cherrypy._cpcompat import basestring, py3k
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.
@@ -35,7 +35,7 @@ class Server(ServerAdapter):
if value == '':
raise ValueError("The empty string ('') is not an allowed value. "
"Use '0.0.0.0' instead to listen on all active "
"interfaces (INADDR_ANY).")
'interfaces (INADDR_ANY).')
self._socket_host = value
socket_host = property(
_get_socket_host,
@@ -61,11 +61,11 @@ class Server(ServerAdapter):
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)."""
@@ -113,7 +113,7 @@ class Server(ServerAdapter):
ssl_private_key = None
"""The filename of the private key to use with SSL."""
if py3k:
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
@@ -156,7 +156,7 @@ class Server(ServerAdapter):
if httpserver is None:
from cherrypy import _cpwsgi_server
httpserver = _cpwsgi_server.CPWSGIServer(self)
if isinstance(httpserver, basestring):
if isinstance(httpserver, text_or_bytes):
# Is anyone using this? Can I add an arg?
httpserver = attributes(httpserver)(self)
return httpserver, self.bind_addr
@@ -180,7 +180,7 @@ class Server(ServerAdapter):
self.socket_file = None
self.socket_host = None
self.socket_port = None
elif isinstance(value, basestring):
elif isinstance(value, text_or_bytes):
self.socket_file = value
self.socket_host = None
self.socket_port = None
@@ -189,9 +189,9 @@ class Server(ServerAdapter):
self.socket_host, self.socket_port = value
self.socket_file = None
except ValueError:
raise ValueError("bind_addr must be a (host, port) tuple "
"(for TCP sockets) or a string (for Unix "
"domain sockets), not %r" % value)
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,
@@ -215,12 +215,12 @@ class Server(ServerAdapter):
port = self.socket_port
if self.ssl_certificate:
scheme = "https"
scheme = 'https'
if port != 443:
host += ":%s" % port
host += ':%s' % port
else:
scheme = "http"
scheme = 'http'
if port != 80:
host += ":%s" % port
host += ':%s' % port
return "%s://%s" % (scheme, host)
return '%s://%s' % (scheme, host)

View File

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

View File

@@ -26,6 +26,12 @@ 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):
@@ -44,8 +50,8 @@ def _getargs(func):
_attr_error = (
"CherryPy Tools cannot be turned on directly. Instead, turn them "
"on via config, or use them as decorators on your page handlers."
'CherryPy Tools cannot be turned on directly. Instead, turn them '
'on via config, or use them as decorators on your page handlers.'
)
@@ -56,7 +62,7 @@ class Tool(object):
help(tool.callable) should give you more information about this Tool.
"""
namespace = "tools"
namespace = 'tools'
def __init__(self, point, callable, name=None, priority=50):
self._point = point
@@ -79,7 +85,7 @@ class Tool(object):
for arg in _getargs(self.callable):
setattr(self, arg, None)
except (TypeError, AttributeError):
if hasattr(self.callable, "__call__"):
if hasattr(self.callable, '__call__'):
for arg in _getargs(self.callable.__call__):
setattr(self, arg, None)
# IronPython 1.0 raises NotImplementedError because
@@ -103,8 +109,8 @@ class Tool(object):
if self._name in tm:
conf.update(tm[self._name])
if "on" in conf:
del conf["on"]
if 'on' in conf:
del conf['on']
return conf
@@ -113,21 +119,21 @@ class Tool(object):
For example::
@expose
@tools.proxy()
def whats_my_base(self):
return cherrypy.request.base
whats_my_base.exposed = True
"""
if args:
raise TypeError("The %r Tool does not accept positional "
"arguments; you must use keyword arguments."
raise TypeError('The %r Tool does not accept positional '
'arguments; you must use keyword arguments.'
% self._name)
def tool_decorator(f):
if not hasattr(f, "_cp_config"):
if not hasattr(f, '_cp_config'):
f._cp_config = {}
subspace = self.namespace + "." + self._name + "."
f._cp_config[subspace + "on"] = True
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
@@ -140,9 +146,9 @@ class Tool(object):
method when the tool is "turned on" in config.
"""
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
p = getattr(self.callable, 'priority', self._priority)
cherrypy.serving.request.hooks.attach(self._point, self.callable,
priority=p, **conf)
@@ -171,12 +177,12 @@ class HandlerTool(Tool):
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
handle_func.exposed = True
return handle_func
def _wrapper(self, **kwargs):
@@ -190,9 +196,9 @@ class HandlerTool(Tool):
method when the tool is "turned on" in config.
"""
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
p = getattr(self.callable, 'priority', self._priority)
cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
priority=p, **conf)
@@ -253,11 +259,6 @@ class ErrorTool(Tool):
# Builtin tools #
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
class SessionTool(Tool):
@@ -271,7 +272,7 @@ class SessionTool(Tool):
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://bitbucket.org/cherrypy/cherrypy/issue/630>`_).
(`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
@@ -295,9 +296,9 @@ class SessionTool(Tool):
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
p = getattr(self.callable, 'priority', self._priority)
hooks.attach(self._point, self.callable, priority=p, **conf)
@@ -365,6 +366,7 @@ class XMLRPCController(object):
# 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()
@@ -372,29 +374,28 @@ class XMLRPCController(object):
for attr in str(rpcmethod).split('.'):
subhandler = getattr(subhandler, attr, None)
if subhandler and getattr(subhandler, "exposed", False):
if subhandler and getattr(subhandler, 'exposed', False):
body = subhandler(*(vpath + rpcparams), **params)
else:
# https://bitbucket.org/cherrypy/cherrypy/issue/533
# https://github.com/cherrypy/cherrypy/issues/533
# if a method is not found, an xmlrpclib.Fault should be returned
# raising an exception here will do that; see
# cherrypy.lib.xmlrpcutil.on_error
raise Exception('method "%s" is not supported' % attr)
conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {})
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
default.exposed = True
class SessionAuthTool(HandlerTool):
def _setargs(self):
for name in dir(cptools.SessionAuth):
if not name.startswith("__"):
if not name.startswith('__'):
setattr(self, name, None)
@@ -417,7 +418,7 @@ class CachingTool(Tool):
"""Hook caching into cherrypy.request."""
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
priority=p, **conf)
@@ -446,7 +447,7 @@ class Toolbox(object):
cherrypy.serving.request.toolmaps[self.namespace] = map = {}
def populate(k, v):
toolname, arg = k.split(".", 1)
toolname, arg = k.split('.', 1)
bucket = map.setdefault(toolname, {})
bucket[arg] = v
return populate
@@ -456,15 +457,22 @@ class Toolbox(object):
map = cherrypy.serving.request.toolmaps.get(self.namespace)
if map:
for name, settings in map.items():
if settings.get("on", False):
if settings.get('on', False):
tool = getattr(self, name)
tool._setup()
def register(self, point, **kwargs):
"""Return a decorator which registers the function at the given hook point."""
def decorator(func):
setattr(self, kwargs.get('name', func.__name__), Tool(point, func, **kwargs))
return func
return decorator
class DeprecatedTool(Tool):
_name = None
warnmsg = "This Tool is deprecated."
warnmsg = 'This Tool is deprecated.'
def __init__(self, point, warnmsg=None):
self.point = point
@@ -482,7 +490,7 @@ class DeprecatedTool(Tool):
warnings.warn(self.warnmsg)
default_toolbox = _d = Toolbox("tools")
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)
@@ -504,14 +512,14 @@ _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.")
'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.")
'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)
@@ -525,5 +533,6 @@ _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

View File

@@ -2,8 +2,10 @@
import os
import six
import cherrypy
from cherrypy._cpcompat import ntou, py3k
from cherrypy._cpcompat import ntou
from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
from cherrypy.lib import httputil
@@ -44,22 +46,22 @@ class Application(object):
relative_urls = False
def __init__(self, root, script_name="", config=None):
def __init__(self, root, script_name='', config=None):
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
self.root = root
self.script_name = script_name
self.wsgiapp = _cpwsgi.CPWSGIApp(self)
self.namespaces = self.namespaces.copy()
self.namespaces["log"] = lambda k, v: setattr(self.log, k, v)
self.namespaces["wsgi"] = self.wsgiapp.namespace_handler
self.namespaces['log'] = lambda k, v: setattr(self.log, k, v)
self.namespaces['wsgi'] = self.wsgiapp.namespace_handler
self.config = self.__class__.config.copy()
if config:
self.merge(config)
def __repr__(self):
return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__,
return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__,
self.root, self.script_name)
script_name_doc = """The URI "mount point" for this app. A mount point
@@ -84,11 +86,11 @@ class Application(object):
# 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("/")
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/')
def _set_script_name(self, value):
if value:
value = value.rstrip("/")
value = value.rstrip('/')
self._script_name = value
script_name = property(fget=_get_script_name, fset=_set_script_name,
doc=script_name_doc)
@@ -98,22 +100,22 @@ class Application(object):
_cpconfig.merge(self.config, config)
# Handle namespaces specified in config.
self.namespaces(self.config.get("/", {}))
self.namespaces(self.config.get('/', {}))
def find_config(self, path, key, default=None):
"""Return the most-specific value for key along path, or default."""
trail = path or "/"
trail = path or '/'
while trail:
nodeconf = self.config.get(trail, {})
if key in nodeconf:
return nodeconf[key]
lastslash = trail.rfind("/")
lastslash = trail.rfind('/')
if lastslash == -1:
break
elif lastslash == 0 and trail != "/":
trail = "/"
elif lastslash == 0 and trail != '/':
trail = '/'
else:
trail = trail[:lastslash]
@@ -170,7 +172,7 @@ class Tree(object):
def __init__(self):
self.apps = {}
def mount(self, root, script_name="", config=None):
def mount(self, root, script_name='', config=None):
"""Mount a new app from a root object, script_name, and config.
root
@@ -195,29 +197,29 @@ class Tree(object):
if script_name is None:
raise TypeError(
"The 'script_name' argument may not be None. Application "
"objects may, however, possess a script_name of None (in "
"order to inpect the WSGI environ for SCRIPT_NAME upon each "
"request). You cannot mount such Applications on this Tree; "
"you must pass them to a WSGI server interface directly.")
'objects may, however, possess a script_name of None (in '
'order to inpect the WSGI environ for SCRIPT_NAME upon each '
'request). You cannot mount such Applications on this Tree; '
'you must pass them to a WSGI server interface directly.')
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/")
script_name = script_name.rstrip('/')
if isinstance(root, Application):
app = root
if script_name != "" and script_name != app.script_name:
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")
'Cannot specify a different script name and pass an '
'Application instance to cherrypy.mount')
script_name = app.script_name
else:
app = Application(root, script_name)
# If mounted at "", add favicon.ico
if (script_name == "" and root is not None
and not hasattr(root, "favicon_ico")):
if (script_name == '' and root is not None
and not hasattr(root, 'favicon_ico')):
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
"favicon.ico")
'favicon.ico')
root.favicon_ico = tools.staticfile.handler(favicon)
if config:
@@ -227,10 +229,10 @@ class Tree(object):
return app
def graft(self, wsgi_callable, script_name=""):
def graft(self, wsgi_callable, script_name=''):
"""Mount a wsgi callable at the given script_name."""
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/")
script_name = script_name.rstrip('/')
self.apps[script_name] = wsgi_callable
def script_name(self, path=None):
@@ -250,22 +252,22 @@ class Tree(object):
if path in self.apps:
return path
if path == "":
if path == '':
return None
# Move one node up the tree and try again.
path = path[:path.rfind("/")]
path = path[:path.rfind('/')]
def __call__(self, environ, start_response):
# If you're calling this, then you're probably setting SCRIPT_NAME
# to '' (some WSGI servers always set SCRIPT_NAME to '').
# Try to look up the app using the full path.
env1x = environ
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
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 "/")
sn = self.script_name(path or '/')
if sn is None:
start_response('404 Not Found', [])
return []
@@ -274,26 +276,12 @@ class Tree(object):
# Correct the SCRIPT_NAME and PATH_INFO environ entries.
environ = environ.copy()
if not py3k:
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
# Python 2/WSGI u.0: all strings MUST be of type unicode
enc = environ[ntou('wsgi.url_encoding')]
environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
environ[ntou('PATH_INFO')] = path[
len(sn.rstrip("/")):].decode(enc)
else:
# Python 2/WSGI 1.x: all strings MUST be of type str
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
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:
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
# Python 3/WSGI u.0: all strings MUST be full unicode
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
else:
# Python 3/WSGI 1.x: all strings MUST be ISO-8859-1 str
environ['SCRIPT_NAME'] = sn.encode(
'utf-8').decode('ISO-8859-1')
environ['PATH_INFO'] = path[
len(sn.rstrip("/")):].encode('utf-8').decode('ISO-8859-1')
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
return app(environ, start_response)

View File

@@ -8,9 +8,12 @@ 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 BytesIO, bytestr, ntob, ntou, py3k, unicodestr
from cherrypy._cpcompat import ntob, ntou
from cherrypy import _cperror
from cherrypy.lib import httputil
from cherrypy.lib import is_closable_iterator
@@ -24,7 +27,7 @@ def downgrade_wsgi_ux_to_1x(environ):
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, unicodestr):
elif isinstance(v, six.text_type):
v = v.encode('ISO-8859-1')
env1x[k.encode('ISO-8859-1')] = v
@@ -43,10 +46,13 @@ class VirtualHost(object):
Domain2App = cherrypy.Application(root)
SecureApp = cherrypy.Application(Secure())
vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
domains={'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
})
vhost = cherrypy._cpwsgi.VirtualHost(
RootApp,
domains={
'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
},
)
cherrypy.tree.graft(vhost)
"""
@@ -75,7 +81,7 @@ class VirtualHost(object):
def __call__(self, environ, start_response):
domain = environ.get('HTTP_HOST', '')
if self.use_x_forwarded_host:
domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
domain = environ.get('HTTP_X_FORWARDED_HOST', domain)
nextapp = self.domains.get(domain)
if nextapp is None:
@@ -106,7 +112,7 @@ class InternalRedirector(object):
# Add the *previous* path_info + qs to redirections.
old_uri = sn + path
if qs:
old_uri += "?" + qs
old_uri += '?' + qs
redirections.append(old_uri)
if not self.recursive:
@@ -114,18 +120,20 @@ class InternalRedirector(object):
# already
new_uri = sn + ir.path
if ir.query_string:
new_uri += "?" + ir.query_string
new_uri += '?' + ir.query_string
if new_uri in redirections:
ir.request.close()
raise RuntimeError("InternalRedirector visited the "
"same URL twice: %r" % new_uri)
tmpl = (
'InternalRedirector visited the same URL twice: %r'
)
raise RuntimeError(tmpl % new_uri)
# Munge the environment and try again.
environ['REQUEST_METHOD'] = "GET"
environ['REQUEST_METHOD'] = 'GET'
environ['PATH_INFO'] = ir.path
environ['QUERY_STRING'] = ir.query_string
environ['wsgi.input'] = BytesIO()
environ['CONTENT_LENGTH'] = "0"
environ['wsgi.input'] = io.BytesIO()
environ['CONTENT_LENGTH'] = '0'
environ['cherrypy.previous_request'] = ir.request
@@ -157,19 +165,20 @@ class _TrappedResponse(object):
self.throws = throws
self.started_response = False
self.response = self.trap(
self.nextapp, self.environ, self.start_response)
self.nextapp, self.environ, self.start_response,
)
self.iter_response = iter(self.response)
def __iter__(self):
self.started_response = True
return self
if py3k:
def __next__(self):
return self.trap(next, self.iter_response)
else:
def next(self):
return self.trap(self.iter_response.next)
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'):
@@ -184,16 +193,17 @@ class _TrappedResponse(object):
raise
except:
tb = _cperror.format_exc()
#print('trapped (started %s):' % self.started_response, tb)
_cherrypy.log(tb, severity=40)
if not _cherrypy.request.show_tracebacks:
tb = ""
tb = ''
s, h, b = _cperror.bare_error(tb)
if py3k:
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]
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([])
@@ -212,7 +222,7 @@ class _TrappedResponse(object):
raise
if self.started_response:
return ntob("").join(b)
return ntob('').join(b)
else:
return b
@@ -227,7 +237,7 @@ class AppResponse(object):
def __init__(self, environ, start_response, cpapp):
self.cpapp = cpapp
try:
if not py3k:
if six.PY2:
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
environ = downgrade_wsgi_ux_to_1x(environ)
self.environ = environ
@@ -236,29 +246,31 @@ class AppResponse(object):
r = _cherrypy.serving.response
outstatus = r.output_status
if not isinstance(outstatus, bytestr):
raise TypeError("response.output_status is not a byte string.")
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, bytestr):
raise TypeError(
"response.header_list key %r is not a byte string." %
k)
if not isinstance(v, bytestr):
raise TypeError(
"response.header_list value %r is not a byte string." %
v)
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 py3k:
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]
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)
@@ -269,12 +281,12 @@ class AppResponse(object):
def __iter__(self):
return self
if py3k:
def __next__(self):
return next(self.iter_response)
else:
def next(self):
return self.iter_response.next()
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)"""
@@ -296,13 +308,18 @@ class AppResponse(object):
"""Create a Request object using environ."""
env = self.environ.get
local = httputil.Host('', int(env('SERVER_PORT', 80)),
env('SERVER_NAME', ''))
remote = httputil.Host(env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1) or -1),
env('REMOTE_HOST', ''))
local = httputil.Host(
'',
int(env('SERVER_PORT', 80) or -1),
env('SERVER_NAME', ''),
)
remote = httputil.Host(
env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1) or -1),
env('REMOTE_HOST', ''),
)
scheme = env('wsgi.url_scheme')
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1')
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
# LOGON_USER is served by IIS, and is the name of the
@@ -316,44 +333,54 @@ class AppResponse(object):
meth = self.environ['REQUEST_METHOD']
path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', ''))
path = httputil.urljoin(
self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', ''),
)
qs = self.environ.get('QUERY_STRING', '')
if py3k:
# 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():
# 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:
u_path = path.encode(old_enc).decode(new_enc)
u_qs = qs.encode(old_enc).decode(new_enc)
except (UnicodeEncodeError, UnicodeDecodeError):
# Just pass them through without transcoding and hope.
pass
else:
# Only set transcoded values if they both succeed.
path = u_path
qs = u_qs
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',
}
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."""
@@ -361,9 +388,9 @@ class AppResponse(object):
# We assume all incoming header keys are uppercase already.
if cgiName in self.headerNames:
yield self.headerNames[cgiName], environ[cgiName]
elif cgiName[:5] == "HTTP_":
elif cgiName[:5] == 'HTTP_':
# Hackish attempt at recovering original header names.
translatedHeader = cgiName[5:].replace("_", "-")
translatedHeader = cgiName[5:].replace('_', '-')
yield translatedHeader, environ[cgiName]
@@ -371,9 +398,10 @@ class CPWSGIApp(object):
"""A WSGI application object for a CherryPy Application."""
pipeline = [('ExceptionTrapper', ExceptionTrapper),
('InternalRedirector', InternalRedirector),
]
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
@@ -423,16 +451,16 @@ class CPWSGIApp(object):
def namespace_handler(self, k, v):
"""Config handler for the 'wsgi' namespace."""
if k == "pipeline":
if k == 'pipeline':
# Note this allows multiple 'wsgi.pipeline' config entries
# (but each entry will be processed in a 'random' order).
# It should also allow developers to set default middleware
# in code (passed to self.__init__) that deployers can add to
# (but not remove) via config.
self.pipeline.extend(v)
elif k == "response_class":
elif k == 'response_class':
self.response_class = v
else:
name, arg = k.split(".", 1)
name, arg = k.split('.', 1)
bucket = self.config.setdefault(name, {})
bucket[arg] = v

View File

@@ -66,5 +66,5 @@ class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
self.stats['Enabled'] = getattr(
self.server_adapter, 'statistics', False)
def error_log(self, msg="", level=20, traceback=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

0
cherrypy/cherryd Normal file → Executable file
View File

61
cherrypy/daemon.py Normal file → Executable file
View File

@@ -13,7 +13,7 @@ def start(configfiles=None, daemonize=False, environment=None,
"""Subscribe all engine plugins and start the engine."""
sys.path = [''] + sys.path
for i in imports or []:
exec("import %s" % i)
exec('import %s' % i)
for c in configfiles or []:
cherrypy.config.update(c)
@@ -37,31 +37,28 @@ def start(configfiles=None, daemonize=False, environment=None,
if pidfile:
plugins.PIDFile(engine, pidfile).subscribe()
if hasattr(engine, "signal_handler"):
if hasattr(engine, 'signal_handler'):
engine.signal_handler.subscribe()
if hasattr(engine, "console_control_handler"):
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')
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})
cherrypy.config.update({'engine.autoreload.on': False})
# Turn off the default HTTP server (which is subscribed by default).
cherrypy.server.unsubscribe()
addr = cherrypy.server.bind_addr
if fastcgi:
f = servers.FlupFCGIServer(application=cherrypy.tree,
bindAddress=addr)
elif scgi:
f = servers.FlupSCGIServer(application=cherrypy.tree,
bindAddress=addr)
else:
f = servers.FlupCGIServer(application=cherrypy.tree,
bindAddress=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()
@@ -79,25 +76,25 @@ 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('-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")
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")
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:

View File

@@ -1,7 +1,5 @@
"""CherryPy Library"""
# Deprecated in CherryPy 3.2 -- remove in CherryPy 3.3
from cherrypy.lib.reprconf import unrepr, modules, attributes
def is_iterator(obj):
'''Returns a boolean indicating if the object provided implements
@@ -16,22 +14,23 @@ def is_iterator(obj):
# 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)
@@ -40,6 +39,7 @@ def is_closable_iterator(obj):
else:
return True
class file_generator(object):
"""Yield the given input (a file object) in chunks (default 64k). (Core)"""
@@ -77,9 +77,9 @@ def file_generator_limited(fileobj, count, chunk_size=65536):
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()]
'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)
response.headers['Vary'] = ', '.join(varies)

View File

@@ -22,25 +22,25 @@ def check_auth(users, encrypt=None, realm=None):
if not isinstance(users, dict):
raise ValueError(
"Authentication users must be a dictionary")
'Authentication users must be a dictionary')
# fetch the user password
password = users.get(ah["username"], None)
password = users.get(ah['username'], None)
except TypeError:
# returns a password (encrypted or clear text)
password = users(ah["username"])
password = users(ah['username'])
else:
if not isinstance(users, dict):
raise ValueError("Authentication users must be a dictionary")
raise ValueError('Authentication users must be a dictionary')
# fetch the user password
password = users.get(ah["username"], None)
password = users.get(ah['username'], None)
# validate the authorization by re-computing it here
# and compare it with what the user-agent provided
if httpauth.checkResponse(ah, password, method=request.method,
encrypt=encrypt, realm=realm):
request.login = ah["username"]
request.login = ah['username']
return True
request.login = False
@@ -72,7 +72,7 @@ def basic_auth(realm, users, encrypt=None, debug=False):
'www-authenticate'] = httpauth.basicAuth(realm)
raise cherrypy.HTTPError(
401, "You are not authorized to access that resource")
401, 'You are not authorized to access that resource')
def digest_auth(realm, users, debug=False):
@@ -94,4 +94,4 @@ def digest_auth(realm, users, debug=False):
'www-authenticate'] = httpauth.digestAuth(realm)
raise cherrypy.HTTPError(
401, "You are not authorized to access that resource")
401, 'You are not authorized to access that resource')

View File

@@ -2,6 +2,12 @@
# -*- 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`.
@@ -22,10 +28,6 @@ as the credentials store::
__author__ = 'visteya'
__date__ = 'April 2009'
import binascii
from cherrypy._cpcompat import base64_decode
import cherrypy
def checkpassword_dict(user_password_dict):
"""Returns a checkpassword function which checks credentials
@@ -70,7 +72,8 @@ def basic_auth(realm, checkpassword, debug=False):
auth_header = request.headers.get('authorization')
if auth_header is not None:
try:
# 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)
@@ -79,12 +82,9 @@ def basic_auth(realm, checkpassword, debug=False):
cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
request.login = username
return # successful authentication
# split() error, base64.decodestring() error
except (ValueError, binascii.Error):
raise cherrypy.HTTPError(400, 'Bad Request')
# 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")
401, 'You are not authorized to access that resource')

View File

@@ -2,6 +2,13 @@
# -*- 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`.
@@ -22,11 +29,6 @@ __author__ = 'visteya'
__date__ = 'April 2009'
import time
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
import cherrypy
from cherrypy._cpcompat import md5, ntob
md5_hex = lambda s: md5(ntob(s)).hexdigest()
qop_auth = 'auth'
@@ -141,7 +143,7 @@ class HttpDigestAuthorization (object):
def __init__(self, auth_header, http_method, debug=False):
self.http_method = http_method
self.debug = debug
scheme, params = auth_header.split(" ", 1)
scheme, params = auth_header.split(' ', 1)
self.scheme = scheme.lower()
if self.scheme != 'digest':
raise ValueError('Authorization scheme is not "Digest"')
@@ -179,7 +181,7 @@ class HttpDigestAuthorization (object):
)
if not has_reqd:
raise ValueError(
self.errmsg("Not all required parameters are present."))
self.errmsg('Not all required parameters are present.'))
if self.qop:
if self.qop not in valid_qops:
@@ -187,13 +189,13 @@ class HttpDigestAuthorization (object):
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"))
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"))
self.errmsg('If qop is not sent, '
'neither cnonce nor nc can be present'))
def __str__(self):
return 'authorization : %s' % self.auth_header
@@ -238,7 +240,7 @@ class HttpDigestAuthorization (object):
except ValueError: # int() error
pass
if self.debug:
TRACE("nonce is stale")
TRACE('nonce is stale')
return True
def HA2(self, entity_body=''):
@@ -250,14 +252,14 @@ class HttpDigestAuthorization (object):
#
# 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":
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))
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!"))
raise ValueError(self.errmsg('Unrecognized value for qop!'))
return H(a2)
def request_digest(self, ha1, entity_body=''):
@@ -278,10 +280,10 @@ class HttpDigestAuthorization (object):
ha2 = self.HA2(entity_body)
# Request-Digest -- RFC 2617 3.2.2.1
if self.qop:
req = "%s:%s:%s:%s:%s" % (
req = '%s:%s:%s:%s:%s' % (
self.nonce, self.nc, self.cnonce, self.qop, ha2)
else:
req = "%s:%s" % (self.nonce, ha2)
req = '%s:%s' % (self.nonce, ha2)
# RFC 2617 3.2.2.2
#
@@ -350,12 +352,10 @@ def digest_auth(realm, get_ha1, key, debug=False):
auth_header = request.headers.get('authorization')
nonce_is_stale = False
if auth_header is not None:
try:
with cherrypy.HTTPError.handle(ValueError, 400,
'The Authorization header could not be parsed.'):
auth = HttpDigestAuthorization(
auth_header, request.method, debug=debug)
except ValueError:
raise cherrypy.HTTPError(
400, "The Authorization header could not be parsed.")
if debug:
TRACE(str(auth))
@@ -369,7 +369,7 @@ def digest_auth(realm, get_ha1, key, debug=False):
digest = auth.request_digest(ha1, entity_body=request.body)
if digest == auth.response: # authenticated
if debug:
TRACE("digest matches auth.response")
TRACE('digest matches auth.response')
# Now check if nonce is stale.
# The choice of ten minutes' lifetime for nonce is somewhat
# arbitrary
@@ -377,7 +377,7 @@ def digest_auth(realm, get_ha1, key, debug=False):
if not nonce_is_stale:
request.login = auth.username
if debug:
TRACE("authentication of %s successful" %
TRACE('authentication of %s successful' %
auth.username)
return
@@ -387,4 +387,4 @@ def digest_auth(realm, get_ha1, key, debug=False):
TRACE(header)
cherrypy.serving.response.headers['WWW-Authenticate'] = header
raise cherrypy.HTTPError(
401, "You are not authorized to access that resource")
401, 'You are not authorized to access that resource')

View File

@@ -39,7 +39,7 @@ import time
import cherrypy
from cherrypy.lib import cptools, httputil
from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted, Event
from cherrypy._cpcompat import copyitems, ntob, sorted, Event
class Cache(object):
@@ -170,7 +170,7 @@ class MemoryCache(Cache):
# Run self.expire_cache in a separate daemon thread.
t = threading.Thread(target=self.expire_cache, name='expire_cache')
self.expiration_thread = t
set_daemon(t, True)
t.daemon = True
t.start()
def clear(self):
@@ -265,7 +265,7 @@ class MemoryCache(Cache):
self.store.pop(uri, None)
def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs):
"""Try to obtain cached output. If fresh enough, raise HTTPError(304).
If POST, PUT, or DELETE:
@@ -291,9 +291,9 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request = cherrypy.serving.request
response = cherrypy.serving.response
if not hasattr(cherrypy, "_cache"):
if not hasattr(cherrypy, '_cache'):
# Make a process-wide Cache object.
cherrypy._cache = kwargs.pop("cache_class", MemoryCache)()
cherrypy._cache = kwargs.pop('cache_class', MemoryCache)()
# Take all remaining kwargs and set them on the Cache object.
for k, v in kwargs.items():
@@ -328,7 +328,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
if directive == 'max-age':
if len(atoms) != 1 or not atoms[0].isdigit():
raise cherrypy.HTTPError(
400, "Invalid Cache-Control header")
400, 'Invalid Cache-Control header')
max_age = int(atoms[0])
break
elif directive == 'no-cache':
@@ -353,13 +353,13 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
return False
# Copy the response headers. See
# https://bitbucket.org/cherrypy/cherrypy/issue/721.
# 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)
response.headers['Age'] = str(age)
try:
# Note that validate_since depends on a Last-Modified header;
@@ -457,14 +457,14 @@ def expires(secs=0, force=False, debug=False):
secs = (86400 * secs.days) + secs.seconds
if secs == 0:
if force or ("Pragma" not in headers):
headers["Pragma"] = "no-cache"
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"
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
if force or 'Expires' not in headers:
headers['Expires'] = expiry

View File

@@ -23,10 +23,14 @@ it will call ``serve()`` for you.
import re
import sys
import cgi
from cherrypy._cpcompat import quote_plus
import os
import os.path
localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
import cherrypy
from cherrypy._cpcompat import quote_plus
localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache')
the_coverage = None
try:
@@ -42,8 +46,8 @@ except ImportError:
import warnings
warnings.warn(
"No code coverage will be performed; "
"coverage.py could not be imported.")
'No code coverage will be performed; '
'coverage.py could not be imported.')
def start():
pass
@@ -193,7 +197,7 @@ def _percent(statements, missing):
return 0
def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
def _show_branch(root, base, path, pct=0, showpct=False, exclude='',
coverage=the_coverage):
# Show the directory name and any of our children
@@ -204,7 +208,7 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
if newpath.lower().startswith(base):
relpath = newpath[len(base):]
yield "| " * relpath.count(os.sep)
yield '| ' * relpath.count(os.sep)
yield (
"<a class='directory' "
"href='menu?base=%s&exclude=%s'>%s</a>\n" %
@@ -225,7 +229,7 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
for name in files:
newpath = os.path.join(path, name)
pc_str = ""
pc_str = ''
if showpct:
try:
_, statements, _, missing, _ = coverage.analysis2(newpath)
@@ -234,13 +238,13 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
pass
else:
pc = _percent(statements, missing)
pc_str = ("%3d%% " % pc).replace(' ', '&nbsp;')
pc_str = ('%3d%% ' % pc).replace(' ', '&nbsp;')
if pc < float(pct) or pc == -1:
pc_str = "<span class='fail'>%s</span>" % pc_str
else:
pc_str = "<span class='pass'>%s</span>" % pc_str
yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1),
pc_str, newpath, name)
@@ -260,8 +264,8 @@ def _graft(path, tree):
break
atoms.append(tail)
atoms.append(p)
if p != "/":
atoms.append("/")
if p != '/':
atoms.append('/')
atoms.reverse()
for node in atoms:
@@ -290,11 +294,12 @@ class CoverStats(object):
root = os.path.dirname(cherrypy.__file__)
self.root = root
@cherrypy.expose
def index(self):
return TEMPLATE_FRAMESET % self.root.lower()
index.exposed = True
def menu(self, base="/", pct="50", showpct="",
@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.
@@ -305,37 +310,36 @@ class CoverStats(object):
# Start by showing links for parent paths
yield "<div id='crumbs'>"
path = ""
path = ''
atoms = base.split(os.sep)
atoms.pop()
for atom in atoms:
path += atom + os.sep
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
% (path, quote_plus(exclude), atom, os.sep))
yield "</div>"
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>"
yield '<p>No modules covered.</p>'
else:
for chunk in _show_branch(tree, base, "/", pct,
for chunk in _show_branch(tree, base, '/', pct,
showpct == 'checked', exclude,
coverage=self.coverage):
yield chunk
yield "</div>"
yield "</body></html>"
menu.exposed = True
yield '</div>'
yield '</body></html>'
def annotated_file(self, filename, statements, excluded, missing):
source = open(filename, 'r')
buffer = []
for lineno, line in enumerate(source.readlines()):
lineno += 1
line = line.strip("\n\r")
line = line.strip('\n\r')
empty_the_buffer = True
if lineno in excluded:
template = TEMPLATE_LOC_EXCLUDED
@@ -352,6 +356,7 @@ class CoverStats(object):
buffer = []
yield template % (lineno, cgi.escape(line))
@cherrypy.expose
def report(self, name):
filename, statements, excluded, missing, _ = self.coverage.analysis2(
name)
@@ -366,12 +371,11 @@ class CoverStats(object):
yield '</table>'
yield '</body>'
yield '</html>'
report.exposed = True
def serve(path=localFile, port=8080, root=None):
if coverage is None:
raise ImportError("The coverage module could not be imported.")
raise ImportError('The coverage module could not be imported.')
from coverage import coverage
cov = coverage(data_file=path)
cov.load()
@@ -379,9 +383,9 @@ def serve(path=localFile, port=8080, root=None):
import cherrypy
cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10,
'environment': "production",
'environment': 'production',
})
cherrypy.quickstart(CoverStats(cov, root))
if __name__ == "__main__":
if __name__ == '__main__':
serve(*tuple(sys.argv[1:]))

View File

@@ -187,9 +187,17 @@ To format statistics reports::
"""
import logging
import os
import sys
import threading
import time
import cherrypy
from cherrypy._cpcompat import json
# ------------------------------- Statistics -------------------------------- #
import logging
if not hasattr(logging, 'statistics'):
logging.statistics = {}
@@ -210,11 +218,6 @@ def extrapolate_statistics(scope):
# -------------------- CherryPy Applications Statistics --------------------- #
import threading
import time
import cherrypy
appstats = logging.statistics.setdefault('CherryPy Applications', {})
appstats.update({
'Enabled': True,
@@ -294,6 +297,11 @@ class ByteCountWrapper(object):
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."""
@@ -322,7 +330,7 @@ class StatsTool(cherrypy.Tool):
appstats['Current Requests'] += 1
appstats['Total Requests'] += 1
appstats['Requests'][threading._get_ident()] = {
appstats['Requests'][_get_threading_ident()] = {
'Bytes Read': None,
'Bytes Written': None,
# Use a lambda so the ip gets updated by tools.proxy later
@@ -339,7 +347,7 @@ class StatsTool(cherrypy.Tool):
debug=False, **kwargs):
"""Record the end of a request."""
resp = cherrypy.serving.response
w = appstats['Requests'][threading._get_ident()]
w = appstats['Requests'][_get_threading_ident()]
r = cherrypy.request.rfile.bytes_read
w['Bytes Read'] = r
@@ -384,24 +392,13 @@ class StatsTool(cherrypy.Tool):
sq.pop(0)
import cherrypy
cherrypy.tools.cpstats = StatsTool()
# ---------------------- CherryPy Statistics Reporting ---------------------- #
import os
thisdir = os.path.abspath(os.path.dirname(__file__))
try:
import json
except ImportError:
try:
import simplejson as json
except ImportError:
json = None
missing = object()
locale_date = lambda v: time.strftime('%c', time.gmtime(v))
@@ -469,6 +466,7 @@ class StatsPage(object):
},
}
@cherrypy.expose
def index(self):
# Transform the raw data into pretty output for HTML
yield """
@@ -572,7 +570,6 @@ table.stats2 th {
</body>
</html>
"""
index.exposed = True
def get_namespaces(self):
"""Yield (title, scalars, collections) for each namespace."""
@@ -605,7 +602,13 @@ table.stats2 th {
"""Return ([headers], [rows]) for the given collection."""
# E.g., the 'Requests' dict.
headers = []
for record in v.itervalues():
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:
@@ -666,22 +669,22 @@ table.stats2 th {
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)
data.exposed = True
@cherrypy.expose
def pause(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = False
raise cherrypy.HTTPRedirect('./')
pause.exposed = True
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.exposed = True
resume.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']}

View File

@@ -2,9 +2,12 @@
import logging
import re
from hashlib import md5
import six
import cherrypy
from cherrypy._cpcompat import basestring, md5, set, unicodestr
from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import httputil as _httputil
from cherrypy.lib import is_iterator
@@ -30,7 +33,7 @@ def validate_etags(autotags=False, debug=False):
response = cherrypy.serving.response
# Guard against being run twice.
if hasattr(response, "ETag"):
if hasattr(response, 'ETag'):
return
status, reason, msg = _httputil.valid_status(response.status)
@@ -69,24 +72,24 @@ def validate_etags(autotags=False, debug=False):
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))
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 conditions == ['*'] or etag in conditions:
if debug:
cherrypy.log('request.method: %s' %
request.method, 'TOOLS.ETAGS')
if request.method in ("GET", "HEAD"):
if request.method in ('GET', 'HEAD'):
raise cherrypy.HTTPRedirect([], 304)
else:
raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r "
"matched %r" % (etag, conditions))
raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r '
'matched %r' % (etag, conditions))
def validate_since():
@@ -110,7 +113,7 @@ def validate_since():
since = request.headers.get('If-Modified-Since')
if since and since == lastmod:
if (status >= 200 and status <= 299) or status == 304:
if request.method in ("GET", "HEAD"):
if request.method in ('GET', 'HEAD'):
raise cherrypy.HTTPRedirect([], 304)
else:
raise cherrypy.HTTPError(412)
@@ -183,7 +186,7 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
# This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
scheme = s
if not scheme:
scheme = request.base[:request.base.find("://")]
scheme = request.base[:request.base.find('://')]
if local:
lbase = request.headers.get(local, None)
@@ -192,15 +195,14 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
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 = '127.0.0.1'
else:
base = '127.0.0.1:%s' % port
if port != 80:
base += ':%s' % port
if base.find("://") == -1:
if base.find('://') == -1:
# add http:// or https:// if needed
base = scheme + "://" + base
base = scheme + '://' + base
request.base = base
@@ -210,7 +212,7 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
if xff:
if remote == 'X-Forwarded-For':
#Bug #1268
# Bug #1268
xff = xff.split(',')[0].strip()
request.remote.ip = xff
@@ -283,7 +285,7 @@ class SessionAuth(object):
"""Assert that the user is logged in."""
session_key = "username"
session_key = 'username'
debug = False
def check_username_and_password(self, username, password):
@@ -304,7 +306,7 @@ class SessionAuth(object):
def login_screen(self, from_page='..', username='', error_msg='',
**kwargs):
return (unicodestr("""<html><body>
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" />
@@ -315,7 +317,7 @@ Message: %(error_msg)s
<br />
<input type="submit" />
</form>
</body></html>""") % vars()).encode("utf-8")
</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."""
@@ -324,15 +326,15 @@ Message: %(error_msg)s
if error_msg:
body = self.login_screen(from_page, username, error_msg)
response.body = body
if "Content-Length" in response.headers:
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
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 "/")
raise cherrypy.HTTPRedirect(from_page or '/')
def do_logout(self, from_page='..', **kwargs):
"""Logout. May raise redirect, or return True if request handled."""
@@ -362,9 +364,9 @@ Message: %(error_msg)s
locals(),
)
response.body = self.login_screen(url)
if "Content-Length" in response.headers:
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
del response.headers['Content-Length']
return True
self._debug_message('Setting request.login to %(username)r', locals())
request.login = username
@@ -386,14 +388,14 @@ Message: %(error_msg)s
return True
elif path.endswith('do_login'):
if request.method != 'POST':
response.headers['Allow'] = "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"
response.headers['Allow'] = 'POST'
raise cherrypy.HTTPError(405)
self._debug_message('routing %(path)r to do_logout', locals())
return self.do_logout(**request.params)
@@ -412,19 +414,19 @@ session_auth.__doc__ = """Session authentication hook.
Any attribute of the SessionAuth class may be overridden via a keyword arg
to this function:
""" + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__)
for k in dir(SessionAuth) if not k.startswith("__")])
""" + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__)
for k in dir(SessionAuth) if not k.startswith('__')])
def log_traceback(severity=logging.ERROR, debug=False):
"""Write the last error's traceback to the cherrypy error log."""
cherrypy.log("", "HTTP", severity=severity, traceback=True)
cherrypy.log('', 'HTTP', severity=severity, traceback=True)
def log_request_headers(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")
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):
@@ -440,13 +442,13 @@ def log_hooks(debug=False):
points.append(k)
for k in points:
msg.append(" %s:" % k)
msg.append(' %s:' % k)
v = request.hooks.get(k, [])
v.sort()
for h in v:
msg.append(" %r" % h)
msg.append(' %r' % h)
cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
':\n' + '\n'.join(msg), "HTTP")
':\n' + '\n'.join(msg), 'HTTP')
def redirect(url='', internal=True, debug=False):
@@ -531,7 +533,7 @@ def accept(media=None, debug=False):
"""
if not media:
return
if isinstance(media, basestring):
if isinstance(media, text_or_bytes):
media = [media]
request = cherrypy.serving.request
@@ -547,12 +549,12 @@ def accept(media=None, debug=False):
# Note that 'ranges' is sorted in order of preference
for element in ranges:
if element.qvalue > 0:
if element.value == "*/*":
if element.value == '*/*':
# Matches any type or subtype
if debug:
cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
return media[0]
elif element.value.endswith("/*"):
elif element.value.endswith('/*'):
# Matches any subtype
mtype = element.value[:-1] # Keep the slash
for m in media:
@@ -572,11 +574,11 @@ def accept(media=None, debug=False):
# No suitable media-range found.
ah = request.headers.get('Accept')
if ah is None:
msg = "Your client did not send an Accept header."
msg = 'Your client did not send an Accept header.'
else:
msg = "Your client sent this Accept header: %s." % ah
msg += (" But this resource only emits these media types: %s." %
", ".join(media))
msg = 'Your client sent this Accept header: %s.' % ah
msg += (' But this resource only emits these media types: %s.' %
', '.join(media))
raise cherrypy.HTTPError(406, msg)
@@ -628,3 +630,19 @@ def autovary(ignore=None, debug=False):
v.sort()
resp_h['Vary'] = ', '.join(v)
request.hooks.attach('before_finalize', set_response_header, 95)
def convert_params(exception=ValueError, error=400):
"""Convert request params based on function annotations, with error handling.
exception
Exception class to catch.
status
The HTTP error code to return to the client on failure.
"""
request = cherrypy.serving.request
types = request.handler.callable.__annotations__
with cherrypy.HTTPError.handle(exception, error):
for key in set(types).intersection(request.params):
request.params[key] = types[key](request.params[key])

View File

@@ -1,8 +1,11 @@
import struct
import time
import io
import six
import cherrypy
from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr
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
@@ -46,7 +49,7 @@ class UTF8StreamEncoder:
def __next__(self):
res = next(self._iterator)
if isinstance(res, unicodestr):
if isinstance(res, six.text_type):
res = res.encode('utf-8')
return res
@@ -63,7 +66,7 @@ class UTF8StreamEncoder:
class ResponseEncoder:
default_encoding = 'utf-8'
failmsg = "Response body could not be encoded with %r."
failmsg = 'Response body could not be encoded with %r.'
encoding = None
errors = 'strict'
text_only = True
@@ -95,7 +98,7 @@ class ResponseEncoder:
def encoder(body):
for chunk in body:
if isinstance(chunk, unicodestr):
if isinstance(chunk, six.text_type):
chunk = chunk.encode(encoding, self.errors)
yield chunk
self.body = encoder(self.body)
@@ -108,7 +111,7 @@ class ResponseEncoder:
self.attempted_charsets.add(encoding)
body = []
for chunk in self.body:
if isinstance(chunk, unicodestr):
if isinstance(chunk, six.text_type):
try:
chunk = chunk.encode(encoding, self.errors)
except (LookupError, UnicodeError):
@@ -128,7 +131,7 @@ class ResponseEncoder:
encoder = self.encode_stream
else:
encoder = self.encode_string
if "Content-Length" in response.headers:
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:
@@ -139,7 +142,7 @@ class ResponseEncoder:
# 6
# >>> len(t.encode("utf7"))
# 8
del response.headers["Content-Length"]
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).
@@ -154,7 +157,7 @@ class ResponseEncoder:
if self.debug:
cherrypy.log('Specified encoding %r' %
encoding, 'TOOLS.ENCODE')
if (not charsets) or "*" in charsets or encoding in charsets:
if (not charsets) or '*' in charsets or encoding in charsets:
if self.debug:
cherrypy.log('Attempting encoding %r' %
encoding, 'TOOLS.ENCODE')
@@ -174,7 +177,7 @@ class ResponseEncoder:
else:
for element in encs:
if element.qvalue > 0:
if element.value == "*":
if element.value == '*':
# Matches any charset. Try our default.
if self.debug:
cherrypy.log('Attempting default encoding due '
@@ -189,7 +192,7 @@ class ResponseEncoder:
if encoder(encoding):
return encoding
if "*" not in charsets:
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
@@ -205,18 +208,18 @@ class ResponseEncoder:
# No suitable encoding found.
ac = request.headers.get('Accept-Charset')
if ac is None:
msg = "Your client did not send an Accept-Charset header."
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,)
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, basestring):
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.
@@ -230,14 +233,14 @@ class ResponseEncoder:
elif self.body is None:
self.body = []
ct = response.headers.elements("Content-Type")
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 ct.value.lower().startswith('text/'):
if self.debug:
cherrypy.log(
'Content-Type %s starts with "text/"' % ct,
@@ -261,7 +264,7 @@ class ResponseEncoder:
if self.debug:
cherrypy.log('Setting Content-Type %s' % ct,
'TOOLS.ENCODE')
response.headers["Content-Type"] = str(ct)
response.headers['Content-Type'] = str(ct)
return self.body
@@ -277,11 +280,11 @@ def compress(body, compress_level):
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 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(""))
crc = zlib.crc32(ntob(''))
size = 0
zobj = zlib.compressobj(compress_level,
zlib.DEFLATED, -zlib.MAX_WBITS,
@@ -293,15 +296,15 @@ def compress(body, compress_level):
yield zobj.flush()
# CRC32: 4 bytes
yield struct.pack("<L", crc & int('FFFFFFFF', 16))
yield struct.pack('<L', crc & int('FFFFFFFF', 16))
# ISIZE: 4 bytes
yield struct.pack("<L", size & int('FFFFFFFF', 16))
yield struct.pack('<L', size & int('FFFFFFFF', 16))
def decompress(body):
import gzip
zbuf = BytesIO()
zbuf = io.BytesIO()
zbuf.write(body)
zbuf.seek(0)
zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
@@ -332,7 +335,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
request = cherrypy.serving.request
response = cherrypy.serving.response
set_vary_header(response, "Accept-Encoding")
set_vary_header(response, 'Accept-Encoding')
if not response.body:
# Response body is empty (might be a 304 for instance)
@@ -342,7 +345,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
# If returning cached content (which should already have been gzipped),
# don't re-zip.
if getattr(request, "cached", False):
if getattr(request, 'cached', False):
if debug:
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
return
@@ -410,12 +413,12 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
# 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:
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
del response.headers['Content-Length']
return
if debug:
cherrypy.log('No acceptable encoding found.', context='GZIP')
cherrypy.HTTPError(406, "identity, gzip").set_response()
cherrypy.HTTPError(406, 'identity, gzip').set_response()

View File

@@ -1,6 +1,5 @@
import gc
import inspect
import os
import sys
import time
@@ -36,7 +35,7 @@ class ReferrerTree(object):
refs = gc.get_referrers(obj)
self.ignore.append(refs)
if len(refs) > self.maxparents:
return [("[%s referrers]" % len(refs), [])]
return [('[%s referrers]' % len(refs), [])]
try:
ascendcode = self.ascend.__code__
@@ -72,20 +71,20 @@ class ReferrerTree(object):
return self.peek(repr(obj))
if isinstance(obj, dict):
return "{" + ", ".join(["%s: %s" % (self._format(k, descend=False),
return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False),
self._format(v, descend=False))
for k, v in obj.items()]) + "}"
for k, v in obj.items()]) + '}'
elif isinstance(obj, list):
return "[" + ", ".join([self._format(item, descend=False)
for item in obj]) + "]"
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]) + ")"
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)
return '%s: %s' % (type(obj), r)
def format(self, tree):
"""Return a list of string reprs from a nested list of referrers."""
@@ -93,7 +92,7 @@ class ReferrerTree(object):
def ascend(branch, depth=1):
for parent, grandparents in branch:
output.append((" " * depth) + self._format(parent))
output.append((' ' * depth) + self._format(parent))
if grandparents:
ascend(grandparents, depth + 1)
ascend(tree)
@@ -120,14 +119,14 @@ request_counter.subscribe()
def get_context(obj):
if isinstance(obj, _cprequest.Request):
return "path=%s;stage=%s" % (obj.path_info, obj.stage)
return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
elif isinstance(obj, _cprequest.Response):
return "status=%s" % obj.status
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 ""
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):
@@ -136,26 +135,27 @@ class GCRoot(object):
classes = [
(_cprequest.Request, 2, 2,
"Should be 1 in this request thread and 1 in the main thread."),
'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."),
'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."),
'Should be 1 in this request thread only.'),
]
@cherrypy.expose
def index(self):
return "Hello, world!"
index.exposed = True
return 'Hello, world!'
@cherrypy.expose
def stats(self):
output = ["Statistics:"]
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.")
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
@@ -173,11 +173,11 @@ class GCRoot(object):
for x in gc.garbage:
trash[type(x)] = trash.get(type(x), 0) + 1
if trash:
output.insert(0, "\n%s unreachable objects:" % unreachable)
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))
output.append(' ' + repr(pair))
# Check declared classes to verify uncollected instances.
# These don't have to be part of a cycle; they can be
@@ -193,25 +193,24 @@ class GCRoot(object):
if lenobj < minobj or lenobj > maxobj:
if minobj == maxobj:
output.append(
"\nExpected %s %r references, got %s." %
'\nExpected %s %r references, got %s.' %
(minobj, cls, lenobj))
else:
output.append(
"\nExpected %s to %s %r references, got %s." %
'\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))
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):" %
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)
stats.exposed = True
return '\n'.join(output)

View File

@@ -1,6 +0,0 @@
import warnings
warnings.warn('cherrypy.lib.http has been deprecated and will be removed '
'in CherryPy 3.3 use cherrypy.lib.httputil instead.',
DeprecationWarning)
from cherrypy.lib.httputil import *

View File

@@ -20,8 +20,18 @@ Usage:
SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
SUPPORTED_QOP - list of supported 'Digest' 'qop'.
"""
import time
from hashlib import md5
from cherrypy._cpcompat import (
base64_decode, ntob,
parse_http_list, parse_keqv_list
)
__version__ = 1, 0, 1
__author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>"
__author__ = 'Tiago Cogumbreiro <cogumbreiro@users.sf.net>'
__credits__ = """
Peter van Kampen for its recipe which implement most of Digest
authentication:
@@ -56,19 +66,16 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
"parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey",
"calculateNonce", "SUPPORTED_QOP")
__all__ = ('digestAuth', 'basicAuth', 'doAuth', 'checkResponse',
'parseAuthorization', 'SUPPORTED_ALGORITHM', 'md5SessionKey',
'calculateNonce', 'SUPPORTED_QOP')
##########################################################################
import time
from cherrypy._cpcompat import base64_decode, ntob, md5
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
MD5 = "MD5"
MD5_SESS = "MD5-sess"
AUTH = "auth"
AUTH_INT = "auth-int"
MD5 = 'MD5'
MD5_SESS = 'MD5-sess'
AUTH = 'auth'
AUTH_INT = 'auth-int'
SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
SUPPORTED_QOP = (AUTH, AUTH_INT)
@@ -93,10 +100,10 @@ def calculateNonce(realm, algorithm=MD5):
try:
encoder = DIGEST_AUTH_ENCODERS[algorithm]
except KeyError:
raise NotImplementedError("The chosen algorithm (%s) does not have "
"an implementation yet" % algorithm)
raise NotImplementedError('The chosen algorithm (%s) does not have '
'an implementation yet' % algorithm)
return encoder("%d:%s" % (time.time(), realm))
return encoder('%d:%s' % (time.time(), realm))
def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH):
@@ -127,7 +134,7 @@ def doAuth(realm):
This should be set in the HTTP header under the key 'WWW-Authenticate'."""
return digestAuth(realm) + " " + basicAuth(realm)
return digestAuth(realm) + ' ' + basicAuth(realm)
##########################################################################
@@ -141,31 +148,31 @@ def _parseDigestAuthorization(auth_params):
# Now validate the params
# Check for required parameters
required = ["username", "realm", "nonce", "uri", "response"]
required = ['username', 'realm', 'nonce', 'uri', 'response']
for k in required:
if 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):
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:
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}
username, password = base64_decode(auth_params).split(':', 1)
return {'username': username, 'password': password}
AUTH_SCHEMES = {
"basic": _parseBasicAuthorization,
"digest": _parseDigestAuthorization,
'basic': _parseBasicAuthorization,
'digest': _parseDigestAuthorization,
}
@@ -176,7 +183,7 @@ def parseAuthorization(credentials):
global AUTH_SCHEMES
auth_scheme, auth_params = credentials.split(" ", 1)
auth_scheme, auth_params = credentials.split(' ', 1)
auth_scheme = auth_scheme.lower()
parser = AUTH_SCHEMES[auth_scheme]
@@ -185,8 +192,8 @@ def parseAuthorization(credentials):
if params is None:
return
assert "auth_scheme" not in params
params["auth_scheme"] = auth_scheme
assert 'auth_scheme' not in params
params['auth_scheme'] = auth_scheme
return params
@@ -212,50 +219,50 @@ def md5SessionKey(params, password):
specification.
"""
keys = ("username", "realm", "nonce", "cnonce")
keys = ('username', 'realm', 'nonce', 'cnonce')
params_copy = {}
for key in keys:
params_copy[key] = params[key]
params_copy["algorithm"] = MD5_SESS
params_copy['algorithm'] = MD5_SESS
return _A1(params_copy, password)
def _A1(params, password):
algorithm = params.get("algorithm", MD5)
algorithm = params.get('algorithm', MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
if algorithm == MD5:
# If the "algorithm" directive's value is "MD5" or is
# unspecified, then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
return "%s:%s:%s" % (params["username"], params["realm"], password)
return '%s:%s:%s' % (params['username'], params['realm'], password)
elif algorithm == MD5_SESS:
# This is A1 if qop is set
# A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
# ":" unq(nonce-value) ":" unq(cnonce-value)
h_a1 = H("%s:%s:%s" % (params["username"], params["realm"], password))
return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
h_a1 = H('%s:%s:%s' % (params['username'], params['realm'], password))
return '%s:%s:%s' % (h_a1, params['nonce'], params['cnonce'])
def _A2(params, method, kwargs):
# If the "qop" directive's value is "auth" or is unspecified, then A2 is:
# A2 = Method ":" digest-uri-value
qop = params.get("qop", "auth")
if qop == "auth":
return method + ":" + params["uri"]
elif qop == "auth-int":
qop = params.get('qop', 'auth')
if qop == 'auth':
return method + ':' + params['uri']
elif qop == 'auth-int':
# If the "qop" value is "auth-int", then A2 is:
# A2 = Method ":" digest-uri-value ":" H(entity-body)
entity_body = kwargs.get("entity_body", "")
H = kwargs["H"]
entity_body = kwargs.get('entity_body', '')
H = kwargs['H']
return "%s:%s:%s" % (
return '%s:%s:%s' % (
method,
params["uri"],
params['uri'],
H(entity_body)
)
@@ -263,19 +270,19 @@ def _A2(params, method, kwargs):
raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
def _computeDigestResponse(auth_map, password, method="GET", A1=None,
def _computeDigestResponse(auth_map, password, method='GET', A1=None,
**kwargs):
"""
Generates a response respecting the algorithm defined in RFC 2617
"""
params = auth_map
algorithm = params.get("algorithm", MD5)
algorithm = params.get('algorithm', MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
KD = lambda secret, data: H(secret + ":" + data)
KD = lambda secret, data: H(secret + ':' + data)
qop = params.get("qop", None)
qop = params.get('qop', None)
H_A2 = H(_A2(params, method, kwargs))
@@ -284,7 +291,7 @@ def _computeDigestResponse(auth_map, password, method="GET", A1=None,
else:
H_A1 = H(_A1(params, password))
if qop in ("auth", "auth-int"):
if qop in ('auth', 'auth-int'):
# If the "qop" value is "auth" or "auth-int":
# request-digest = <"> < KD ( H(A1), unq(nonce-value)
# ":" nc-value
@@ -292,11 +299,11 @@ def _computeDigestResponse(auth_map, password, method="GET", A1=None,
# ":" unq(qop-value)
# ":" H(A2)
# ) <">
request = "%s:%s:%s:%s:%s" % (
params["nonce"],
params["nc"],
params["cnonce"],
params["qop"],
request = '%s:%s:%s:%s:%s' % (
params['nonce'],
params['nc'],
params['cnonce'],
params['qop'],
H_A2,
)
elif qop is None:
@@ -304,12 +311,12 @@ def _computeDigestResponse(auth_map, password, method="GET", A1=None,
# for compatibility with RFC 2069):
# request-digest =
# <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
request = "%s:%s" % (params["nonce"], H_A2)
request = '%s:%s' % (params['nonce'], H_A2)
return KD(H_A1, request)
def _checkDigestResponse(auth_map, password, method="GET", A1=None, **kwargs):
def _checkDigestResponse(auth_map, password, method='GET', A1=None, **kwargs):
"""This function is used to verify the response given by the client when
he tries to authenticate.
Optional arguments:
@@ -327,7 +334,7 @@ def _checkDigestResponse(auth_map, password, method="GET", A1=None, **kwargs):
response = _computeDigestResponse(
auth_map, password, method, A1, **kwargs)
return response == auth_map["response"]
return response == auth_map['response']
def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
@@ -337,19 +344,19 @@ def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
pass_through = lambda password, username=None: password
encrypt = encrypt or pass_through
try:
candidate = encrypt(auth_map["password"], auth_map["username"])
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"])
candidate = encrypt(auth_map['password'])
return candidate == password
AUTH_RESPONSES = {
"basic": _checkBasicResponse,
"digest": _checkDigestResponse,
'basic': _checkBasicResponse,
'digest': _checkDigestResponse,
}
def checkResponse(auth_map, password, method="GET", encrypt=None, **kwargs):
def checkResponse(auth_map, password, method='GET', encrypt=None, **kwargs):
"""'checkResponse' compares the auth_map with the password and optionally
other arguments that each implementation might need.
@@ -366,6 +373,6 @@ def checkResponse(auth_map, password, method="GET", encrypt=None, **kwargs):
The 'A1' argument is only used in MD5_SESS algorithm based responses.
Check md5SessionKey() for more info.
"""
checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
checker = AUTH_RESPONSES[auth_map['auth_scheme']]
return checker(auth_map, password, method=method, encrypt=encrypt,
**kwargs)

View File

@@ -7,13 +7,26 @@ 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 cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou
from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr
from cherrypy._cpcompat import reversed, sorted, unicodestr, unquote_qs
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://bitbucket.org/cherrypy/cherrypy/issue/361
# 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.')
@@ -22,8 +35,8 @@ response_codes[503] = ('Service Unavailable',
'request due to a temporary overloading or '
'maintenance of the server.')
import re
import urllib
HTTPDate = functools.partial(email.utils.formatdate, usegmt=True)
def urljoin(*atoms):
@@ -32,11 +45,11 @@ def urljoin(*atoms):
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("//", "/")
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 "/"
return url or '/'
def urljoin_bytes(*atoms):
@@ -45,11 +58,11 @@ def urljoin_bytes(*atoms):
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("/"))
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("/")
return url or ntob('/')
def protocol_from_http(protocol_str):
@@ -72,9 +85,9 @@ def get_ranges(headervalue, content_length):
return None
result = []
bytesunit, byteranges = headervalue.split("=", 1)
for brange in byteranges.split(","):
start, stop = [x.strip() for x in brange.split("-", 1)]
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
@@ -132,8 +145,8 @@ class HeaderElement(object):
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)))
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__())
@@ -141,50 +154,17 @@ class HeaderElement(object):
def __unicode__(self):
return ntou(self.__str__())
@staticmethod
def parse(elementstr):
"""Transform 'token;key=val' to ('token', {'key': 'val'})."""
# Split the element into a value and parameters. The 'value' may
# be of the form, "token=token", but we don't split that here.
atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
# Clumsy fix for lack of proper handling of constructions like:
# form-data; name="name"; filename="one;word.nzb"
# A proper parser should be used, but this patch will at least allow
# having semicolons in a file name.
if 'filename' in elementstr:
xatoms = []
append_next = False
for atom in atoms:
if append_next:
append_next = False
atom = xatoms.pop(-1) + ';' + atom
if '"' in atom and not atom.endswith('"'):
append_next = True
xatoms.append(atom)
atoms = xatoms
# End of patch
if not atoms:
initial_value = ''
else:
initial_value = atoms.pop(0).strip()
params = {}
for atom in atoms:
atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
key = atom.pop(0)
if atom:
val = atom[0]
else:
val = ""
params[key] = val
initial_value, params = parse_header(elementstr)
return initial_value, params
parse = staticmethod(parse)
@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)
from_str = classmethod(from_str)
q_separator = re.compile(r'; *q *=')
@@ -201,6 +181,7 @@ class AcceptElement(HeaderElement):
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
@@ -214,16 +195,16 @@ class AcceptElement(HeaderElement):
media_type, params = cls.parse(media_range)
if qvalue is not None:
params["q"] = qvalue
params['q'] = qvalue
return cls(media_type, params)
from_str = classmethod(from_str)
@property
def qvalue(self):
val = self.params.get("q", "1")
'The qvalue, or priority, of this value.'
val = self.params.get('q', '1')
if isinstance(val, HeaderElement):
val = val.value
return float(val)
qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
def __cmp__(self, other):
diff = cmp(self.qvalue, other.qvalue)
@@ -246,7 +227,7 @@ def header_elements(fieldname, fieldvalue):
result = []
for element in RE_HEADER_SPLIT.split(fieldvalue):
if fieldname.startswith("Accept") or fieldname == 'TE':
if fieldname.startswith('Accept') or fieldname == 'TE':
hv = AcceptElement.from_str(element)
else:
hv = HeaderElement.from_str(element)
@@ -257,13 +238,8 @@ def header_elements(fieldname, fieldvalue):
def decode_TEXT(value):
r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr")."""
try:
# Python 3
from email.header import decode_header
except ImportError:
from email.Header import decode_header
atoms = decode_header(value)
decodedvalue = ""
decodedvalue = ''
for atom, charset in atoms:
if charset is not None:
atom = atom.decode(charset)
@@ -284,7 +260,7 @@ def valid_status(status):
status = 200
status = str(status)
parts = status.split(" ", 1)
parts = status.split(' ', 1)
if len(parts) == 1:
# No reason supplied.
code, = parts
@@ -296,16 +272,16 @@ def valid_status(status):
try:
code = int(code)
except ValueError:
raise ValueError("Illegal response status from server "
"(%s is non-numeric)." % repr(code))
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))
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 = "", ""
default_reason, message = '', ''
else:
default_reason, message = response_codes[code]
@@ -346,7 +322,7 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
nv = name_value.split('=', 1)
if len(nv) != 2:
if strict_parsing:
raise ValueError("bad query field: %r" % (name_value,))
raise ValueError('bad query field: %r' % (name_value,))
# Handle case of a control-name with no equal sign
if keep_blank_values:
nv.append('')
@@ -364,7 +340,7 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
return d
image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
image_map_pattern = re.compile(r'[0-9]+,[0-9]+')
def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
@@ -377,7 +353,7 @@ def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
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 = query_string.split(',')
pm = {'x': int(pm[0]), 'y': int(pm[1])}
else:
pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
@@ -414,12 +390,12 @@ class CaseInsensitiveDict(dict):
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
fromkeys = classmethod(fromkeys)
def setdefault(self, key, x=None):
key = str(key).title()
@@ -438,7 +414,7 @@ class CaseInsensitiveDict(dict):
# 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 nativestr == bytestr:
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)
@@ -458,7 +434,7 @@ class HeaderMap(CaseInsensitiveDict):
"""
protocol = (1, 1)
encodings = ["ISO-8859-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,
@@ -481,19 +457,20 @@ class HeaderMap(CaseInsensitiveDict):
"""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, unicodestr):
if isinstance(k, six.text_type):
k = cls.encode(k)
if not isinstance(v, basestring):
if not isinstance(v, text_or_bytes):
v = str(v)
if isinstance(v, unicodestr):
if isinstance(v, six.text_type):
v = cls.encode(v)
# See header_translate_* constants above.
@@ -504,8 +481,8 @@ class HeaderMap(CaseInsensitiveDict):
header_translate_deletechars)
yield (k, v)
encode_header_items = classmethod(encode_header_items)
@classmethod
def encode(cls, v):
"""Return the given header name or value, encoded for HTTP output."""
for enc in cls.encodings:
@@ -523,10 +500,9 @@ class HeaderMap(CaseInsensitiveDict):
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." %
raise ValueError('Could not encode header part %r using '
'any of the encodings %r.' %
(v, cls.encodings))
encode = classmethod(encode)
class Host(object):
@@ -539,9 +515,9 @@ class Host(object):
"""
ip = "0.0.0.0"
ip = '0.0.0.0'
port = 80
name = "unknown.tld"
name = 'unknown.tld'
def __init__(self, ip, port, name=None):
self.ip = ip
@@ -551,4 +527,4 @@ class Host(object):
self.name = name
def __repr__(self):
return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name)
return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name)

View File

@@ -1,17 +1,15 @@
import cherrypy
from cherrypy._cpcompat import basestring, ntou, json_encode, json_decode
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("")):
if not entity.headers.get(ntou('Content-Length'), ntou('')):
raise cherrypy.HTTPError(411)
body = entity.fp.read()
try:
with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'):
cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
except ValueError:
raise cherrypy.HTTPError(400, 'Invalid JSON document')
def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
@@ -41,7 +39,7 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
package importable; otherwise, ValueError is raised during processing.
"""
request = cherrypy.serving.request
if isinstance(content_type, basestring):
if isinstance(content_type, text_or_bytes):
content_type = [content_type]
if force:

View File

@@ -1,147 +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
#@abc.abstract_method
# def _lock_file(self):
# """Attempt to obtain the lock on self.fp. Raise LockError if not
# acquired."""
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
"""
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

View File

@@ -11,7 +11,7 @@ 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)"
'Create a timer that expires at `expiration` (UTC datetime)'
self.expiration = expiration
@classmethod
@@ -26,7 +26,7 @@ class Timer(object):
class LockTimeout(Exception):
"An exception when a lock could not be acquired before a timeout period"
'An exception when a lock could not be acquired before a timeout period'
class LockChecker(object):
@@ -43,5 +43,5 @@ class LockChecker(object):
def expired(self):
if self.timer.expired():
raise LockTimeout(
"Timeout acquiring lock for %(session_id)s" % vars(self))
'Timeout acquiring lock for %(session_id)s' % vars(self))
return False

View File

@@ -8,11 +8,11 @@ You can profile any of your pages as follows::
from cherrypy.lib import profiler
class Root:
p = profile.Profiler("/path/to/profile/dir")
p = profiler.Profiler("/path/to/profile/dir")
@cherrypy.expose
def index(self):
self.p.run(self._index)
index.exposed = True
def _index(self):
return "Hello, world!"
@@ -33,29 +33,32 @@ module from the command line, it will call ``serve()`` for you.
"""
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
try:
import profile
import pstats
pstats.func_strip_path = new_func_strip_path
except ImportError:
profile = None
pstats = None
import io
import os
import os.path
import sys
import warnings
from cherrypy._cpcompat import StringIO
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
@@ -64,7 +67,7 @@ class Profiler(object):
def __init__(self, path=None):
if not path:
path = os.path.join(os.path.dirname(__file__), "profile")
path = os.path.join(os.path.dirname(__file__), 'profile')
self.path = path
if not os.path.exists(path):
os.makedirs(path)
@@ -73,7 +76,7 @@ class Profiler(object):
"""Dump profile data into self.path."""
global _count
c = _count = _count + 1
path = os.path.join(self.path, "cp_%04d.prof" % c)
path = os.path.join(self.path, 'cp_%04d.prof' % c)
prof = profile.Profile()
result = prof.runcall(func, *args, **params)
prof.dump_stats(path)
@@ -83,12 +86,12 @@ class Profiler(object):
""":rtype: list of available profiles.
"""
return [f for f in os.listdir(self.path)
if f.startswith("cp_") and f.endswith(".prof")]
if f.startswith('cp_') and f.endswith('.prof')]
def stats(self, filename, sortby='cumulative'):
""":rtype stats(index): output of print_stats() for the given profile.
"""
sio = StringIO()
sio = io.StringIO()
if sys.version_info >= (2, 5):
s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
s.strip_dirs()
@@ -110,6 +113,7 @@ class Profiler(object):
sio.close()
return response
@cherrypy.expose
def index(self):
return """<html>
<head><title>CherryPy profile data</title></head>
@@ -119,23 +123,21 @@ class Profiler(object):
</frameset>
</html>
"""
index.exposed = True
@cherrypy.expose
def menu(self):
yield "<h2>Profiling runs</h2>"
yield "<p>Click on one of the runs below to see profiling data.</p>"
yield '<h2>Profiling runs</h2>'
yield '<p>Click on one of the runs below to see profiling data.</p>'
runs = self.statfiles()
runs.sort()
for i in runs:
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (
i, i)
menu.exposed = True
@cherrypy.expose
def report(self, filename):
import cherrypy
cherrypy.response.headers['Content-Type'] = 'text/plain'
return self.stats(filename)
report.exposed = True
class ProfileAggregator(Profiler):
@@ -147,7 +149,7 @@ class ProfileAggregator(Profiler):
self.profiler = profile.Profile()
def run(self, func, *args, **params):
path = os.path.join(self.path, "cp_%04d.prof" % self.count)
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
@@ -172,11 +174,11 @@ class make_app:
"""
if profile is None or pstats is None:
msg = ("Your installation of Python does not have a profile "
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.")
'`sudo apt-get install python-profiler`. '
'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
'for details.')
warnings.warn(msg)
self.nextapp = nextapp
@@ -197,20 +199,19 @@ class make_app:
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. "
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.")
'`sudo apt-get install python-profiler`. '
'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
'for details.')
warnings.warn(msg)
import cherrypy
cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10,
'environment': "production",
'environment': 'production',
})
cherrypy.quickstart(Profiler(path))
if __name__ == "__main__":
if __name__ == '__main__':
serve(*tuple(sys.argv[1:]))

View File

@@ -25,14 +25,9 @@ except ImportError:
from ConfigParser import ConfigParser
try:
set
text_or_bytes
except NameError:
from sets import Set as set
try:
basestring
except NameError:
basestring = str
text_or_bytes = str
try:
# Python 3
@@ -47,7 +42,7 @@ import sys
def as_dict(config):
"""Return a dict from 'config' whether it is a dict, file, or filename."""
if isinstance(config, basestring):
if isinstance(config, text_or_bytes):
config = Parser().dict_from_file(config)
elif hasattr(config, 'read'):
config = Parser().dict_from_file(config)
@@ -83,8 +78,8 @@ class NamespaceSet(dict):
# Separate the given config into namespaces
ns_confs = {}
for k in config:
if "." in k:
ns, name = k.split(".", 1)
if '.' in k:
ns, name = k.split('.', 1)
bucket = ns_confs.setdefault(ns, {})
bucket[name] = config[k]
@@ -95,7 +90,7 @@ class NamespaceSet(dict):
# for k, v in ns_confs.get(ns, {}).iteritems():
# callable(k, v)
for ns, handler in self.items():
exit = getattr(handler, "__exit__", None)
exit = getattr(handler, '__exit__', None)
if exit:
callable = handler.__enter__()
no_exc = True
@@ -120,7 +115,7 @@ class NamespaceSet(dict):
handler(k, v)
def __repr__(self):
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
dict.__repr__(self))
def __copy__(self):
@@ -155,7 +150,7 @@ class Config(dict):
def update(self, config):
"""Update self from a dict, file or filename."""
if isinstance(config, basestring):
if isinstance(config, text_or_bytes):
# Filename
config = Parser().dict_from_file(config)
elif hasattr(config, 'read'):
@@ -192,7 +187,7 @@ class Parser(ConfigParser):
return optionstr
def read(self, filenames):
if isinstance(filenames, basestring):
if isinstance(filenames, text_or_bytes):
filenames = [filenames]
for filename in filenames:
# try:
@@ -218,8 +213,8 @@ class Parser(ConfigParser):
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." %
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
@@ -241,7 +236,7 @@ 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" %
raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__))
return m(o)
@@ -254,7 +249,7 @@ class _Builder2:
# e.g. IronPython 1.0.
return eval(s)
p = compiler.parse("__tempvalue__ = " + s)
p = compiler.parse('__tempvalue__ = ' + s)
return p.getChildren()[1].getChildren()[0].getChildren()[1]
def build_Subscript(self, o):
@@ -281,13 +276,14 @@ class _Builder2:
# 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
return kw_dict
def build_List(self, o):
return map(self.build, o.getChildren())
@@ -326,7 +322,7 @@ class _Builder2:
except AttributeError:
pass
raise TypeError("unrepr could not resolve the name %s" % repr(name))
raise TypeError('unrepr could not resolve the name %s' % repr(name))
def build_Add(self, o):
left, right = map(self.build, o.getChildren())
@@ -355,7 +351,7 @@ 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" %
raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__))
return m(o)
@@ -368,7 +364,7 @@ class _Builder3:
# e.g. IronPython 1.0.
return eval(s)
p = ast.parse("__tempvalue__ = " + s)
p = ast.parse('__tempvalue__ = ' + s)
return p.body[0].value
def build_Subscript(self, o):
@@ -377,7 +373,39 @@ class _Builder3:
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:
@@ -388,13 +416,16 @@ class _Builder3:
if o.starargs is None:
starargs = ()
else:
starargs = self.build(o.starargs)
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):
@@ -435,7 +466,7 @@ class _Builder3:
except AttributeError:
pass
raise TypeError("unrepr could not resolve the name %s" % repr(name))
raise TypeError('unrepr could not resolve the name %s' % repr(name))
def build_NameConstant(self, o):
return o.value
@@ -487,7 +518,7 @@ 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(".")
last_dot = full_attribute_name.rfind('.')
attr_name = full_attribute_name[last_dot + 1:]
mod_path = full_attribute_name[:last_dot]

View File

@@ -4,13 +4,13 @@ You need to edit your config file to use sessions. Here's an example::
[/]
tools.sessions.on = True
tools.sessions.storage_type = "file"
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_type`` the sessions will be saved in RAM.
``storage_class``, the sessions will be saved in RAM.
``tools.sessions.on`` is the only required line for working sessions,
the rest are optional.
@@ -94,10 +94,9 @@ import datetime
import os
import time
import threading
import types
import cherrypy
from cherrypy._cpcompat import copyitems, pickle, random20, unicodestr
from cherrypy._cpcompat import copyitems, pickle, random20
from cherrypy.lib import httputil
from cherrypy.lib import lockfile
from cherrypy.lib import locking
@@ -122,10 +121,10 @@ class Session(object):
self._id = value
for o in self.id_observers:
o(value)
id = property(_get_id, _set_id, doc="The current session ID.")
id = property(_get_id, _set_id, doc='The current session ID.')
timeout = 60
"Number of minutes after which to delete session data."
'Number of minutes after which to delete session data.'
locked = False
"""
@@ -138,16 +137,16 @@ class Session(object):
automatically on the first attempt to access session data."""
clean_thread = None
"Class-level Monitor which calls self.clean_up."
'Class-level Monitor which calls self.clean_up.'
clean_freq = 5
"The poll rate for expired session cleanup in minutes."
'The poll rate for expired session cleanup in minutes.'
originalid = None
"The session id passed by the client. May be missing or unsafe."
'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."
'True if the session requested by the client did not exist.'
regenerated = False
"""
@@ -155,7 +154,7 @@ class Session(object):
internal calls to regenerate the session id."""
debug = False
"If True, log debug information."
'If True, log debug information.'
# --------------------- Session management methods --------------------- #
@@ -182,7 +181,7 @@ class Session(object):
cherrypy.log('Expired or malicious session %r; '
'making a new one' % id, 'TOOLS.SESSIONS')
# Expired or malicious session. Make a new one.
# See https://bitbucket.org/cherrypy/cherrypy/issue/709.
# See https://github.com/cherrypy/cherrypy/issues/709.
self.id = None
self.missing = True
self._regenerate()
@@ -471,9 +470,10 @@ class FileSession(Session):
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.")
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.
@@ -485,12 +485,11 @@ class FileSession(Session):
for k, v in kwargs.items():
setattr(cls, k, v)
setup = classmethod(setup)
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.")
raise cherrypy.HTTPError(400, 'Invalid session id in cookie.')
return f
def _exists(self):
@@ -498,12 +497,12 @@ class FileSession(Session):
return os.path.exists(path)
def _load(self, path=None):
assert self.locked, ("The session load without being locked. "
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")
f = open(path, 'rb')
try:
return pickle.load(f)
finally:
@@ -511,21 +510,21 @@ class FileSession(Session):
except (IOError, EOFError):
e = sys.exc_info()[1]
if self.debug:
cherrypy.log("Error loading the session pickle: %s" %
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. "
assert self.locked, ('The session was saved without being locked. '
"Check your tools' priority levels.")
f = open(self._get_file_path(), "wb")
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. "
assert self.locked, ('The session deletion without being locked. '
"Check your tools' priority levels.")
try:
os.unlink(self._get_file_path())
@@ -591,93 +590,6 @@ class FileSession(Session):
and not fname.endswith(self.LOCK_SUFFIX))])
class PostgresqlSession(Session):
""" Implementation of the PostgreSQL backend for sessions. It assumes
a table like this::
create table session (
id varchar(40),
data text,
expiration_time timestamp
)
You must provide your own get_db function.
"""
pickle_protocol = pickle.HIGHEST_PROTOCOL
def __init__(self, id=None, **kwargs):
Session.__init__(self, id, **kwargs)
self.cursor = self.db.cursor()
def setup(cls, **kwargs):
"""Set up the storage system for Postgres-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)
self.db = self.get_db()
setup = classmethod(setup)
def __del__(self):
if self.cursor:
self.cursor.close()
self.db.commit()
def _exists(self):
# Select session data from table
self.cursor.execute('select data, expiration_time from session '
'where id=%s', (self.id,))
rows = self.cursor.fetchall()
return bool(rows)
def _load(self):
# Select session data from table
self.cursor.execute('select data, expiration_time from session '
'where id=%s', (self.id,))
rows = self.cursor.fetchall()
if not rows:
return None
pickled_data, expiration_time = rows[0]
data = pickle.loads(pickled_data)
return data, expiration_time
def _save(self, expiration_time):
pickled_data = pickle.dumps(self._data, self.pickle_protocol)
self.cursor.execute('update session set data = %s, '
'expiration_time = %s where id = %s',
(pickled_data, expiration_time, self.id))
def _delete(self):
self.cursor.execute('delete from session where id=%s', (self.id,))
def acquire_lock(self):
"""Acquire an exclusive lock on the currently-loaded session data."""
# We use the "for update" clause to lock the row
self.locked = True
self.cursor.execute('select id from session where id=%s for update',
(self.id,))
if self.debug:
cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
def release_lock(self):
"""Release the lock on the currently-loaded session data."""
# We just close the cursor and that will remove the lock
# introduced by the "for update" clause
self.cursor.close()
self.locked = False
def clean_up(self):
"""Clean up expired sessions."""
self.cursor.execute('delete from session where expiration_time < %s',
(self.now(),))
class MemcachedSession(Session):
# The most popular memcached client for Python isn't thread-safe.
@@ -689,6 +601,7 @@ class MemcachedSession(Session):
servers = ['127.0.0.1:11211']
@classmethod
def setup(cls, **kwargs):
"""Set up the storage system for memcached-based sessions.
@@ -700,21 +613,6 @@ class MemcachedSession(Session):
import memcache
cls.cache = memcache.Client(cls.servers)
setup = classmethod(setup)
def _get_id(self):
return self._id
def _set_id(self, value):
# This encode() call is where we differ from the superclass.
# Memcache keys MUST be byte strings, not unicode.
if isinstance(value, unicodestr):
value = value.encode('utf-8')
self._id = value
for o in self.id_observers:
o(value)
id = property(_get_id, _set_id, doc="The current session ID.")
def _exists(self):
self.mc_lock.acquire()
@@ -737,7 +635,7 @@ class MemcachedSession(Session):
try:
if not self.cache.set(self.id, (self._data, expiration_time), td):
raise AssertionError(
"Session data for id %r not set." % self.id)
'Session data for id %r not set.' % self.id)
finally:
self.mc_lock.release()
@@ -766,13 +664,13 @@ class MemcachedSession(Session):
def save():
"""Save any changed session data."""
if not hasattr(cherrypy.serving, "session"):
if not hasattr(cherrypy.serving, 'session'):
return
request = cherrypy.serving.request
response = cherrypy.serving.response
# Guard against running twice
if hasattr(request, "_sessionsaved"):
if hasattr(request, '_sessionsaved'):
return
request._sessionsaved = True
@@ -791,8 +689,8 @@ save.failsafe = True
def close():
"""Close the session object for this request."""
sess = getattr(cherrypy.serving, "session", None)
if getattr(sess, "locked", False):
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:
@@ -801,13 +699,20 @@ close.failsafe = True
close.priority = 90
def init(storage_type='ram', path=None, path_header=None, name='session_id',
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, **kwargs):
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
One of 'ram', 'file', 'postgresql', 'memcached'. This will be
(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.
@@ -851,10 +756,13 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
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"):
if hasattr(request, '_session_init_flag'):
return
request._session_init_flag = True
@@ -866,11 +774,18 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
cherrypy.log('ID obtained from request.cookie: %r' % id,
'TOOLS.SESSIONS')
# Find the storage class and call setup (first time only).
storage_class = storage_type.title() + 'Session'
storage_class = globals()[storage_class]
if not hasattr(cherrypy, "session"):
if hasattr(storage_class, "setup"):
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.
@@ -887,7 +802,7 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
sess.id_observers.append(update_cookie)
# Create cherrypy.session which will proxy to cherrypy.serving.session
if not hasattr(cherrypy, "session"):
if not hasattr(cherrypy, 'session'):
cherrypy.session = cherrypy._ThreadLocalProxy('session')
if persistent:
@@ -955,7 +870,7 @@ def set_response_cookie(path=None, path_header=None, name='session_id',
cookie[name]['secure'] = 1
if httponly:
if not cookie[name].isReservedKey('httponly'):
raise ValueError("The httponly cookie token is not supported.")
raise ValueError('The httponly cookie token is not supported.')
cookie[name]['httponly'] = 1

View File

@@ -49,7 +49,10 @@ def serve_file(path, content_type=None, disposition=None, name=None,
try:
st = os.stat(path)
except OSError:
except (OSError, TypeError, ValueError):
# OSError when file fails to stat
# TypeError on Python 2 when there's a null byte
# ValueError on Python 3 when there's a null byte
if debug:
cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
raise cherrypy.NotFound()
@@ -68,7 +71,7 @@ def serve_file(path, content_type=None, disposition=None, name=None,
if content_type is None:
# Set content-type based on filename extension
ext = ""
ext = ''
i = path.rfind('.')
if i != -1:
ext = path[i:].lower()
@@ -83,7 +86,7 @@ def serve_file(path, content_type=None, disposition=None, name=None,
if name is None:
name = os.path.basename(path)
cd = '%s; filename="%s"' % (disposition, name)
response.headers["Content-Disposition"] = cd
response.headers['Content-Disposition'] = cd
if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
@@ -141,7 +144,7 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
cd = disposition
else:
cd = '%s; filename="%s"' % (disposition, name)
response.headers["Content-Disposition"] = cd
response.headers['Content-Disposition'] = cd
if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
@@ -155,12 +158,12 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
# 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"
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)")
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)
@@ -176,15 +179,15 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
cherrypy.log(
'Single part; start: %r, stop: %r' % (start, stop),
'TOOLS.STATIC')
response.status = "206 Partial Content"
response.status = '206 Partial Content'
response.headers['Content-Range'] = (
"bytes %s-%s/%s" % (start, stop - 1, content_length))
'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"
response.status = '206 Partial Content'
try:
# Python 3
from email.generator import _make_boundary as make_boundary
@@ -192,15 +195,15 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
# Python 2
from mimetools import choose_boundary as make_boundary
boundary = make_boundary()
ct = "multipart/byteranges; boundary=%s" % boundary
ct = 'multipart/byteranges; boundary=%s' % boundary
response.headers['Content-Type'] = ct
if "Content-Length" in response.headers:
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
del response.headers['Content-Length']
def file_ranges():
# Apache compatibility:
yield ntob("\r\n")
yield ntob('\r\n')
for start, stop in r:
if debug:
@@ -208,23 +211,23 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
'Multipart; start: %r, stop: %r' % (
start, stop),
'TOOLS.STATIC')
yield ntob("--" + boundary, 'ascii')
yield ntob("\r\nContent-type: %s" % content_type,
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" % (
'\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")
yield ntob('\r\n')
# Final boundary
yield ntob("--" + boundary + "--", 'ascii')
yield ntob('--' + boundary + '--', 'ascii')
# Apache compatibility:
yield ntob("\r\n")
yield ntob('\r\n')
response.body = file_ranges()
return response.body
else:
@@ -241,7 +244,7 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
def serve_download(path, name=None):
"""Serve 'path' as an application/x-download attachment."""
# This is such a common idiom I felt it deserved its own wrapper.
return serve_file(path, "application/x-download", "attachment", name)
return serve_file(path, 'application/x-download', 'attachment', name)
def _attempt(filename, content_types, debug=False):
@@ -265,7 +268,7 @@ def _attempt(filename, content_types, debug=False):
return False
def staticdir(section, dir, root="", match="", content_types=None, index="",
def staticdir(section, dir, root='', match='', content_types=None, index='',
debug=False):
"""Serve a static resource from the given (root +) dir.
@@ -303,7 +306,7 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
# If dir is relative, make absolute using "root".
if not os.path.isabs(dir):
if not root:
msg = "Static dir requires an absolute dir (or root)."
msg = 'Static dir requires an absolute dir (or root).'
if debug:
cherrypy.log(msg, 'TOOLS.STATICDIR')
raise ValueError(msg)
@@ -312,10 +315,10 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
# 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"\/")
section = '/'
section = section.rstrip(r'\/')
branch = request.path_info[len(section) + 1:]
branch = unquote(branch.lstrip(r"\/"))
branch = unquote(branch.lstrip(r'\/'))
# If branch is "", filename will end in a slash
filename = os.path.join(dir, branch)
@@ -335,11 +338,11 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
if index:
handled = _attempt(os.path.join(filename, index), content_types)
if handled:
request.is_index = filename[-1] in (r"\/")
request.is_index = filename[-1] in (r'\/')
return handled
def staticfile(filename, root=None, match="", content_types=None, debug=False):
def staticfile(filename, root=None, match='', content_types=None, debug=False):
"""Serve a static resource from the given (root +) filename.
match

View File

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

View File

@@ -7,8 +7,8 @@ import sys
import time
import threading
from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident
from cherrypy._cpcompat import ntob, set, Timer, SetDaemonProperty
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
@@ -104,17 +104,40 @@ class SignalHandler(object):
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.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():
@@ -129,19 +152,19 @@ class SignalHandler(object):
signame = self.signals[signum]
if handler is None:
self.bus.log("Restoring %s handler to SIG_DFL." % signame)
self.bus.log('Restoring %s handler to SIG_DFL.' % signame)
handler = _signal.SIG_DFL
else:
self.bus.log("Restoring %s handler %r." % (signame, handler))
self.bus.log('Restoring %s handler %r.' % (signame, handler))
try:
our_handler = _signal.signal(signum, handler)
if our_handler is None:
self.bus.log("Restored old %s handler %r, but our "
"handler was not registered." %
self.bus.log('Restored old %s handler %r, but our '
'handler was not registered.' %
(signame, handler), level=30)
except ValueError:
self.bus.log("Unable to restore %s handler %r." %
self.bus.log('Unable to restore %s handler %r.' %
(signame, handler), level=40, traceback=True)
def set_handler(self, signal, listener=None):
@@ -153,40 +176,40 @@ class SignalHandler(object):
If the given signal name or number is not available on the current
platform, ValueError is raised.
"""
if isinstance(signal, basestring):
if isinstance(signal, text_or_bytes):
signum = getattr(_signal, signal, None)
if signum is None:
raise ValueError("No such signal: %r" % signal)
raise ValueError('No such signal: %r' % signal)
signame = signal
else:
try:
signame = self.signals[signal]
except KeyError:
raise ValueError("No such signal: %r" % signal)
raise ValueError('No such signal: %r' % signal)
signum = signal
prev = _signal.signal(signum, self._handle_signal)
self._previous_handlers[signum] = prev
if listener is not None:
self.bus.log("Listening for %s." % signame)
self.bus.log('Listening for %s.' % signame)
self.bus.subscribe(signame, listener)
def _handle_signal(self, signum=None, frame=None):
"""Python signal handler (self.set_handler subscribes it for you)."""
signame = self.signals[signum]
self.bus.log("Caught signal %s." % signame)
self.bus.log('Caught signal %s.' % signame)
self.bus.publish(signame)
def handle_SIGHUP(self):
"""Restart if daemonized, else exit."""
if os.isatty(sys.stdin.fileno()):
# not daemonized (may be foreground or background)
self.bus.log("SIGHUP caught but not daemonized. Exiting.")
self.bus.exit()
else:
self.bus.log("SIGHUP caught while daemonized. Restarting.")
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:
@@ -200,7 +223,7 @@ 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>`_
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):
@@ -216,14 +239,14 @@ class DropPrivileges(SimplePlugin):
def _set_uid(self, val):
if val is not None:
if pwd is None:
self.bus.log("pwd module not available; ignoring uid.",
self.bus.log('pwd module not available; ignoring uid.',
level=30)
val = None
elif isinstance(val, basestring):
elif isinstance(val, text_or_bytes):
val = pwd.getpwnam(val)[2]
self._uid = val
uid = property(_get_uid, _set_uid,
doc="The uid under which to run. Availability: Unix.")
doc='The uid under which to run. Availability: Unix.')
def _get_gid(self):
return self._gid
@@ -231,14 +254,14 @@ class DropPrivileges(SimplePlugin):
def _set_gid(self, val):
if val is not None:
if grp is None:
self.bus.log("grp module not available; ignoring gid.",
self.bus.log('grp module not available; ignoring gid.',
level=30)
val = None
elif isinstance(val, basestring):
elif isinstance(val, text_or_bytes):
val = grp.getgrnam(val)[2]
self._gid = val
gid = property(_get_gid, _set_gid,
doc="The gid under which to run. Availability: Unix.")
doc='The gid under which to run. Availability: Unix.')
def _get_umask(self):
return self._umask
@@ -248,7 +271,7 @@ class DropPrivileges(SimplePlugin):
try:
os.umask
except AttributeError:
self.bus.log("umask function not available; ignoring umask.",
self.bus.log('umask function not available; ignoring umask.',
level=30)
val = None
self._umask = val
@@ -370,7 +393,7 @@ class Daemonizer(SimplePlugin):
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.exit('%s: fork #1 failed: (%d) %s\n'
% (sys.argv[0], exc.errno, exc.strerror))
os.setsid()
@@ -383,15 +406,15 @@ class Daemonizer(SimplePlugin):
os._exit(0) # Exit second parent
except OSError:
exc = sys.exc_info()[1]
sys.exit("%s: fork #2 failed: (%d) %s\n"
sys.exit('%s: fork #2 failed: (%d) %s\n'
% (sys.argv[0], exc.errno, exc.strerror))
os.chdir("/")
os.chdir('/')
os.umask(0)
si = open(self.stdin, "r")
so = open(self.stdout, "a+")
se = open(self.stderr, "a+")
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.
@@ -419,7 +442,7 @@ class PIDFile(SimplePlugin):
if self.finalized:
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
else:
open(self.pidfile, "wb").write(ntob("%s\n" % pid, 'utf8'))
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
@@ -458,13 +481,13 @@ class PerpetualTimer(Timer):
except Exception:
if self.bus:
self.bus.log(
"Error in perpetual timer thread function %r." %
'Error in perpetual timer thread function %r.' %
self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs.
raise
class BackgroundTask(SetDaemonProperty, threading.Thread):
class BackgroundTask(threading.Thread):
"""A subclass of threading.Thread whose run() method repeats.
@@ -476,7 +499,7 @@ class BackgroundTask(SetDaemonProperty, threading.Thread):
"""
def __init__(self, interval, function, args=[], kwargs={}, bus=None):
threading.Thread.__init__(self)
super(BackgroundTask, self).__init__()
self.interval = interval
self.function = function
self.args = args
@@ -500,7 +523,7 @@ class BackgroundTask(SetDaemonProperty, threading.Thread):
self.function(*self.args, **self.kwargs)
except Exception:
if self.bus:
self.bus.log("Error in background task thread function %r."
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
@@ -537,24 +560,24 @@ class Monitor(SimplePlugin):
bus=self.bus)
self.thread.setName(threadname)
self.thread.start()
self.bus.log("Started monitor thread %r." % threadname)
self.bus.log('Started monitor thread %r.' % threadname)
else:
self.bus.log("Monitor thread %r already started." % threadname)
self.bus.log('Monitor thread %r already started.' % threadname)
start.priority = 70
def stop(self):
"""Stop our callback's background task thread."""
if self.thread is None:
self.bus.log("No thread running for %s." %
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 get_daemon(self.thread):
self.bus.log("Joining %r" % name)
if not self.thread.daemon:
self.bus.log('Joining %r' % name)
self.thread.join()
self.bus.log("Stopped thread %r." % name)
self.bus.log('Stopped thread %r.' % name)
self.thread = None
def graceful(self):
@@ -651,10 +674,10 @@ class Autoreloader(Monitor):
else:
if mtime is None or mtime > oldtime:
# The file has been deleted or modified.
self.bus.log("Restarting because %s changed." %
self.bus.log('Restarting because %s changed.' %
filename)
self.thread.cancel()
self.bus.log("Stopped thread %r." %
self.bus.log('Stopped thread %r.' %
self.thread.getName())
self.bus.restart()
return

View File

@@ -59,9 +59,9 @@ hello.py::
class HelloWorld:
\"""Sample request handler class.\"""
@cherrypy.expose
def index(self):
return "Hello world!"
index.exposed = True
cherrypy.tree.mount(HelloWorld())
# CherryPy autoreload must be disabled for the flup server to work
@@ -113,6 +113,7 @@ Please see `Lighttpd FastCGI Docs
an explanation of the possible configuration options.
"""
import os
import sys
import time
import warnings
@@ -151,49 +152,49 @@ class ServerAdapter(object):
def start(self):
"""Start the HTTP server."""
if self.bind_addr is None:
on_what = "unknown interface (dynamic?)"
on_what = 'unknown interface (dynamic?)'
elif isinstance(self.bind_addr, tuple):
on_what = self._get_base()
else:
on_what = "socket file: %s" % self.bind_addr
on_what = 'socket file: %s' % self.bind_addr
if self.running:
self.bus.log("Already serving on %s" % on_what)
self.bus.log('Already serving on %s' % on_what)
return
self.interrupt = None
if not self.httpserver:
raise ValueError("No HTTP server has been created.")
raise ValueError('No HTTP server has been created.')
# Start the httpserver in a new thread.
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
if not os.environ.get('LISTEN_PID', None):
# Start the httpserver in a new thread.
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
import threading
t = threading.Thread(target=self._start_http_thread)
t.setName("HTTPServer " + t.getName())
t.setName('HTTPServer ' + t.getName())
t.start()
self.wait()
self.running = True
self.bus.log("Serving on %s" % on_what)
self.bus.log('Serving on %s' % on_what)
start.priority = 75
def _get_base(self):
if not self.httpserver:
return ''
host, port = self.bind_addr
if getattr(self.httpserver, 'ssl_certificate', None) or \
getattr(self.httpserver, 'ssl_adapter', None):
scheme = "https"
if getattr(self.httpserver, 'ssl_adapter', None):
scheme = 'https'
if port != 443:
host += ":%s" % port
host += ':%s' % port
else:
scheme = "http"
scheme = 'http'
if port != 80:
host += ":%s" % port
host += ':%s' % port
return "%s://%s" % (scheme, host)
return '%s://%s' % (scheme, host)
def _start_http_thread(self):
"""HTTP servers MUST be running in new threads, so that the
@@ -205,32 +206,35 @@ class ServerAdapter(object):
try:
self.httpserver.start()
except KeyboardInterrupt:
self.bus.log("<Ctrl-C> hit: shutting down HTTP server")
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.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",
self.bus.log('Error in HTTP server: shutting down',
traceback=True, level=40)
self.bus.exit()
raise
def wait(self):
"""Wait until the HTTP server is ready to receive requests."""
while not getattr(self.httpserver, "ready", False):
while not getattr(self.httpserver, 'ready', False):
if self.interrupt:
raise self.interrupt
time.sleep(.1)
# Wait for port to be occupied
if isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
wait_for_occupied_port(host, port)
if not os.environ.get('LISTEN_PID', None):
# Wait for port to be occupied if not running via socket-activation
# (for socket-activation the port will be managed by systemd )
if isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
wait_for_occupied_port(host, port)
def stop(self):
"""Stop the HTTP server."""
@@ -241,9 +245,9 @@ class ServerAdapter(object):
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
self.running = False
self.bus.log("HTTP Server %s shut down" % self.httpserver)
self.bus.log('HTTP Server %s shut down' % self.httpserver)
else:
self.bus.log("HTTP Server %s already shut down" % self.httpserver)
self.bus.log('HTTP Server %s already shut down' % self.httpserver)
stop.priority = 25
def restart(self):
@@ -390,10 +394,10 @@ def check_port(host, port, timeout=1.0):
except socket.gaierror:
if ':' in host:
info = [(
socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0)
socket.AF_INET6, socket.SOCK_STREAM, 0, '', (host, port, 0, 0)
)]
else:
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, '', (host, port))]
for res in info:
af, socktype, proto, canonname, sa = res
@@ -409,8 +413,8 @@ def check_port(host, port, timeout=1.0):
if s:
s.close()
else:
raise IOError("Port %s is in use on %s; perhaps the previous "
"httpserver did not shut down properly." %
raise IOError('Port %s is in use on %s; perhaps the previous '
'httpserver did not shut down properly.' %
(repr(port), repr(host)))
@@ -436,7 +440,7 @@ def wait_for_free_port(host, port, timeout=None):
else:
return
raise IOError("Port %r not free on %r" % (port, host))
raise IOError('Port %r not free on %r' % (port, host))
def wait_for_occupied_port(host, port, timeout=None):
@@ -456,11 +460,11 @@ def wait_for_occupied_port(host, port, timeout=None):
time.sleep(timeout)
if host == client_host(host):
raise IOError("Port %r not bound on %r" % (port, 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
msg = 'Unable to verify that the server is bound on %r' % port
warnings.warn(msg)

View File

@@ -85,7 +85,7 @@ class Win32Bus(wspbus.Bus):
return self.events[state]
except KeyError:
event = win32event.CreateEvent(None, 0, 0,
"WSPBus %s Event (pid=%r)" %
'WSPBus %s Event (pid=%r)' %
(state.name, os.getpid()))
self.events[state] = event
return event
@@ -135,7 +135,7 @@ class _ControlCodes(dict):
for key, val in self.items():
if val is obj:
return key
raise ValueError("The given object could not be found: %r" % obj)
raise ValueError('The given object could not be found: %r' % obj)
control_codes = _ControlCodes({'graceful': 138})
@@ -153,14 +153,14 @@ class PyWebService(win32serviceutil.ServiceFramework):
"""Python Web Service."""
_svc_name_ = "Python Web Service"
_svc_display_name_ = "Python Web Service"
_svc_name_ = 'Python Web Service'
_svc_display_name_ = 'Python Web Service'
_svc_deps_ = None # sequence of service names on which this depends
_exe_name_ = "pywebsvc"
_exe_name_ = 'pywebsvc'
_exe_args_ = None # Default to no arguments
# Only exists on Windows 2000 or later, ignored on windows NT
_svc_description_ = "Python Web Service"
_svc_description_ = 'Python Web Service'
def SvcDoRun(self):
from cherrypy import process

View File

@@ -61,14 +61,20 @@ the new state.::
"""
import atexit
import ctypes
import operator
import os
import subprocess
import sys
import threading
import time
import traceback as _traceback
import warnings
from cherrypy._cpcompat import set
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
@@ -86,9 +92,7 @@ class ChannelFailures(Exception):
delimiter = '\n'
def __init__(self, *args, **kwargs):
# Don't use 'super' here; Exceptions are old-style in Py2.4
# See https://bitbucket.org/cherrypy/cherrypy/issue/959
Exception.__init__(self, *args, **kwargs)
super(Exception, self).__init__(*args, **kwargs)
self._exceptions = list()
def handle_exception(self):
@@ -118,7 +122,7 @@ class _StateEnum(object):
name = None
def __repr__(self):
return "states.%s" % self.name
return 'states.%s' % self.name
def __setattr__(self, key, value):
if isinstance(value, self.State):
@@ -162,16 +166,17 @@ class Bus(object):
def __init__(self):
self.execv = False
self.state = states.STOPPED
channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main'
self.listeners = dict(
[(channel, set()) for channel
in ('start', 'stop', 'exit', 'graceful', 'log', 'main')])
(channel, set())
for channel in channels
)
self._priorities = {}
def subscribe(self, channel, callback, priority=None):
"""Add the given callback at the given channel (if not present)."""
if channel not in self.listeners:
self.listeners[channel] = set()
self.listeners[channel].add(callback)
ch_listeners = self.listeners.setdefault(channel, set())
ch_listeners.add(callback)
if priority is None:
priority = getattr(callback, 'priority', 50)
@@ -192,14 +197,11 @@ class Bus(object):
exc = ChannelFailures()
output = []
items = [(self._priorities[(channel, listener)], listener)
for listener in self.listeners[channel]]
try:
items.sort(key=lambda item: item[0])
except TypeError:
# Python 2.3 had no 'key' arg, but that doesn't matter
# since it could sort dissimilar types just fine.
items.sort()
raw_items = (
(self._priorities[(channel, listener)], listener)
for listener in self.listeners[channel]
)
items = sorted(raw_items, key=operator.itemgetter(0))
for priority, listener in items:
try:
output.append(listener(*args, **kwargs))
@@ -217,7 +219,7 @@ class Bus(object):
# Assume any further messages to 'log' will fail.
pass
else:
self.log("Error in %r listener %r" % (channel, listener),
self.log('Error in %r listener %r' % (channel, listener),
level=40, traceback=True)
if exc:
raise exc
@@ -227,10 +229,10 @@ class Bus(object):
"""An atexit handler which asserts the Bus is not running."""
if self.state != states.EXITING:
warnings.warn(
"The main thread is exiting, but the Bus is in the %r state; "
"shutting it down automatically now. You must either call "
"bus.block() after start(), or call bus.exit() before the "
"main thread exits." % self.state, RuntimeWarning)
'The main thread is exiting, but the Bus is in the %r state; '
'shutting it down automatically now. You must either call '
'bus.block() after start(), or call bus.exit() before the '
'main thread exits.' % self.state, RuntimeWarning)
self.exit()
def start(self):
@@ -246,7 +248,7 @@ class Bus(object):
except (KeyboardInterrupt, SystemExit):
raise
except:
self.log("Shutting down due to error in start listener:",
self.log('Shutting down due to error in start listener:',
level=40, traceback=True)
e_info = sys.exc_info()[1]
try:
@@ -319,11 +321,11 @@ class Bus(object):
raise
# Waiting for ALL child threads to finish is necessary on OS X.
# See https://bitbucket.org/cherrypy/cherrypy/issue/581.
# See https://github.com/cherrypy/cherrypy/issues/581.
# It's also good to let them all shut down before allowing
# the main thread to call atexit handlers.
# See https://bitbucket.org/cherrypy/cherrypy/issue/751.
self.log("Waiting for child threads to terminate...")
# See https://github.com/cherrypy/cherrypy/issues/751.
self.log('Waiting for child threads to terminate...')
for t in threading.enumerate():
# Validate the we're not trying to join the MainThread
# that will cause a deadlock and the case exist when
@@ -335,13 +337,13 @@ class Bus(object):
not isinstance(t, threading._MainThread)
):
# Note that any dummy (external) threads are always daemonic.
if hasattr(threading.Thread, "daemon"):
if hasattr(threading.Thread, 'daemon'):
# Python 2.6+
d = t.daemon
else:
d = t.isDaemon()
if not d:
self.log("Waiting for thread %s." % t.getName())
self.log('Waiting for thread %s.' % t.getName())
t.join()
if self.execv:
@@ -378,14 +380,25 @@ class Bus(object):
This must be called from the main thread, because certain platforms
(OS X) don't allow execv to be called in a child thread very well.
"""
args = sys.argv[:]
try:
args = self._get_true_argv()
except NotImplementedError:
"""It's probably win32"""
# For the SABnzbd.exe binary we don't want interpreter flags
# https://github.com/cherrypy/cherrypy/issues/1526
if getattr(sys, 'frozen', False):
args = [sys.executable] + sys.argv
else:
args = [sys.executable] + _args_from_interpreter_flags() + sys.argv
self.log('Re-spawning %s' % ' '.join(args))
self._extend_pythonpath(os.environ)
if sys.platform[:4] == 'java':
from _systemrestart import SystemRestart
raise SystemRestart
else:
args.insert(0, sys.executable)
if sys.platform == 'win32':
args = ['"%s"' % arg for arg in args]
@@ -394,6 +407,58 @@ class Bus(object):
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).
@@ -439,7 +504,7 @@ class Bus(object):
return t
def log(self, msg="", level=20, traceback=False):
def log(self, msg='', level=20, traceback=False):
"""Log the given message. Append the last traceback if requested."""
if traceback:
# Work-around for bug in Python's traceback implementation

View File

File diff suppressed because it is too large Load Diff

View File

@@ -34,12 +34,26 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
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.")
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."""
@@ -48,10 +62,15 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
try:
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)
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:
@@ -60,13 +79,24 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
# the 'ping' isn't SSL.
return None, {}
elif e.errno == ssl.SSL_ERROR_SSL:
if e.args[1].endswith('http request'):
if 'http request' in e.args[1]:
# The client is speaking HTTP to an HTTPS server.
raise wsgiserver.NoSSLError
elif e.args[1].endswith('unknown protocol'):
# The client is speaking some non-HTTP protocol.
# Drop the conn.
return None, {}
# 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', 'unknown error',
'https proxy request', '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)
@@ -75,8 +105,8 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
"""Create WSGI environ entries to be merged into each request."""
cipher = sock.cipher()
ssl_environ = {
"wsgi.url_scheme": "https",
"HTTPS": "on",
'wsgi.url_scheme': 'https',
'HTTPS': 'on',
'SSL_PROTOCOL': cipher[1],
'SSL_CIPHER': cipher[0]
# SSL_VERSION_INTERFACE string The mod_ssl program version
@@ -84,9 +114,5 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
}
return ssl_environ
if sys.version_info >= (3, 0):
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
return wsgiserver.CP_makefile(sock, mode, bufsize)
else:
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
return wsgiserver.CP_fileobject(sock, mode, bufsize)
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
return wsgiserver.CP_makefile(sock, mode, bufsize)

View File

@@ -43,7 +43,7 @@ except ImportError:
SSL = None
class SSL_fileobject(wsgiserver.CP_fileobject):
class SSL_fileobject(wsgiserver.CP_makefile):
"""SSL file object attached to a socket object."""
@@ -68,17 +68,17 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
time.sleep(self.ssl_retry)
except SSL.WantWriteError:
time.sleep(self.ssl_retry)
except SSL.SysCallError, e:
except SSL.SysCallError as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ""
return ''
errnum = e.args[0]
if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
return ""
return ''
raise socket.error(errnum)
except SSL.Error, e:
except SSL.Error as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ""
return ''
thirdarg = None
try:
@@ -95,7 +95,7 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
raise
if time.time() - start > self.ssl_timeout:
raise socket.timeout("timed out")
raise socket.timeout('timed out')
def recv(self, size):
return self._safe_call(True, super(SSL_fileobject, self).recv, size)
@@ -166,7 +166,7 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
def __init__(self, certificate, private_key, certificate_chain=None):
if SSL is None:
raise ImportError("You must install pyOpenSSL to use HTTPS.")
raise ImportError('You must install pyOpenSSL to use HTTPS.')
self.context = None
self.certificate = certificate
@@ -192,18 +192,14 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
c = SSL.Context(SSL.SSLv23_METHOD)
c.use_privatekey_file(self.private_key)
if self.certificate_chain:
if isinstance(self.certificate_chain, unicode) and self.certificate_chain.encode('cp1252', 'ignore') == self.certificate_chain.encode('cp1252', 'replace'):
# Support buggy PyOpenSSL 0.14, which cannot handle Unicode names
c.load_verify_locations(self.certificate_chain.encode('cp1252', 'ignore'))
else:
c.load_verify_locations(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",
'HTTPS': 'on',
# pyOpenSSL doesn't provide access to any of these AFAICT
# 'SSL_PROTOCOL': 'SSLv2',
# SSL_CIPHER string The cipher specification name
@@ -224,8 +220,8 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
# Validity of server's certificate (end time),
})
for prefix, dn in [("I", cert.get_issuer()),
("S", cert.get_subject())]:
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=...'>"
@@ -237,9 +233,9 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
# 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("=")
pos = dnstr.rfind('=')
dnstr, value = dnstr[:pos], dnstr[pos + 1:]
pos = dnstr.rfind("/")
pos = dnstr.rfind('/')
dnstr, key = dnstr[:pos], dnstr[pos + 1:]
if key and value:
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/sabnzbd16_32.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
icons/sabnzbd16_32green.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -2,7 +2,7 @@
uniConfig for SABnzbd 0.7.x
zoggy@sabnzbd.org
Changed by Safihre for 0.8.x
Changed by Safihre for 1.0.x
========================================================
LIBRARIES USED
@@ -20,7 +20,7 @@ jQuery Form Plugin
* http://www.opensource.org/licenses/mit-license.php
Bootstrap v3.3.6 (http://getbootstrap.com)
* Licensed under the MIT license
* Licensed under the MIT license
Changed by Safihre, Nov 2015
We include the icon-file directly into the CSS,
this way we avoid errors when HTTPS is enabled

View File

@@ -1,12 +1,12 @@
<!-- Content end -->
<div class="clearfix"></div>
<!-- Filebrowser modal -->
<div class="modal fade" id="filebrowser_modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title"></h4>
</div>
<div class="modal-body">
@@ -28,7 +28,7 @@
</div>
<div class="main-restarting modal-backdrop fade in" style="display: none;">
<div>
<strong><span class="glyphicon glyphicon-retweet"></span> $T('wizard-restarting')</strong><br />
<strong><span class="glyphicon glyphicon-retweet"></span> $T('restarting-sab')</strong><br />
<small><span class="restarting-url"></span><span class="dotOne">.</span><span class="dotTwo">.</span><span class="dotThree">.</span></small>
</div>
</div>

View File

@@ -20,30 +20,32 @@
#if $pane == "Special" then $T('cmenu-special') else ""#
#if $pane == "RSS" then $T('cmenu-rss') else ""#
</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="apple-mobile-web-app-title" content="SABnzbd" />
<link rel="apple-touch-icon" sizes="76x76" href="${root}staticcfg/ico/apple-touch-icon-76x76-precomposed.png" />
<link rel="apple-touch-icon" sizes="120x120" href="${root}staticcfg/ico/apple-touch-icon-120x120-precomposed.png" />
<link rel="apple-touch-icon" sizes="152x152" href="${root}staticcfg/ico/apple-touch-icon-152x152-precomposed.png" />
<link rel="apple-touch-icon" sizes="180x180" href="${root}staticcfg/ico/apple-touch-icon-180x180-precomposed.png" />
<link rel="apple-touch-icon" sizes="180x180" href="${root}staticcfg/ico/apple-touch-icon-180x180-precomposed.png" />
<link rel="apple-touch-icon" sizes="192x192" href="${root}staticcfg/ico/android-192x192.png" />
<link rel="stylesheet" type="text/css" href="${root}staticcfg/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="${root}staticcfg/bootstrap/css/bootstrap.min.css?v=$version" />
<link rel="stylesheet" type="text/css" href="${root}staticcfg/css/style.css?p=$pid" />
<link rel="shortcut icon" href="${root}staticcfg/ico/favicon.ico" />
<link rel="shortcut icon" href="${root}staticcfg/ico/favicon.ico?v=$version" />
<script type="text/javascript">
// Keeping track of the form state
var formHasChanged = false;
var formWasSubmitted = false;
// Information we need
var sabSession = '$session';
var folderBrowseUrl = '${root}tapi?mode=browse&output=json&apikey=$session';
var rootURL = '${root}'
var folderBrowseUrl = '${root}tapi?mode=browse&output=json&apikey=$session';
var folderSeperator = '#if $os.sep == '\\' then '\\\\' else '/'#'
// Translations
var configTranslate = new Object();
configTranslate.browseText = "$T('browse-folder')";
@@ -51,14 +53,15 @@
configTranslate.saving = "$T('smpl-saving')";
configTranslate.failed = "$T('smpl-failed')";
configTranslate.explainRestart = "$T('explain-Restart')";
configTranslate.wizzardRestart = "$T('wizard-restarting')";
configTranslate.needRestart = "$T('restartRequired')";
configTranslate.wizzardRestart = "$T('restarting-sab')";
configTranslate.needRestart = "$T('restartRequired') $T('explain-needNewLogin')".replace(/\<br(\s*\/|)\>/g, '\n');
configTranslate.buttonRestart = "$T('button-restart')";
configTranslate.confirmLeave = "$T('confirmWithoutSavingPrompt')";
configTranslate.searchPages = ['$T('cmenu-general')', '$T('cmenu-folders')', '$T('cmenu-switches')', '$T('cmenu-sorting')', '$T('cmenu-notif')', '$T('cmenu-special')']
</script>
<script type="text/javascript" src="${root}staticcfg/js/jquery-1.11.2.min.js"></script>
<script type="text/javascript" src="${root}staticcfg/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="${root}staticcfg/js/script.js?p=$pid"></script>
<script type="text/javascript" src="${root}staticcfg/js/jquery-3.1.1.min.js?v=$version"></script>
<script type="text/javascript" src="${root}staticcfg/bootstrap/js/bootstrap.min.js?v=$version"></script>
<script type="text/javascript" src="${root}staticcfg/js/script.js?v=$version"></script>
<script type="text/javascript">
// Set default functions for the autocomplete everywhere
\$.extend(\$.fn.typeahead.defaults, {
@@ -82,6 +85,13 @@
return item
}
})
// to top right away
if(window.location.hash) {
scroll(0,0);
// void some browsers issue
setTimeout(function() { scroll(0,0); }, 1);
}
</script>
</head>
@@ -95,12 +105,10 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<!--#if $active_lang in ('pl', 'es', 'ru', 'sr', 'fi') #-->
<a class="navbar-brand navbar-brand-small" href="${root}" title="$T('Home')"><img src="${root}staticcfg/images/logo-small.png" width="47" height="45" id="logo" alt="$T('Home')" /></a>
<a class="navbar-brand navbar-brand-mobile" href="${root}" title="$T('Home')"><img src="${root}staticcfg/images/logo.png" width="120" height="45" id="logo" alt="$T('Home')" /></a>
<!--#else#-->
<a class="navbar-brand" href="${root}" title="$T('Home')"><img src="${root}staticcfg/images/logo.png" width="120" height="45" id="logo" alt="$T('Home')" /></a>
<!--#end if#-->
<a class="navbar-logo navbar-logo-small" href="${root}" title="$T('Home')">
#include $webdir + "/staticcfg/images/logo-small.svg"#
</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
@@ -175,9 +183,23 @@
<strong>$T('menu-help')</strong>
</a>
</li>
<li class="dropdown" id="search-menu">
<a data-toggle="dropdown" href="#">
<span class="glyphicon glyphicon-search"></span>
<strong><span class="glyphicon glyphicon-search"></span></strong>
</a>
<ul class="dropdown-menu" id="search-dropdown">
<li>
<form onsubmit="return false">
<input type="text" name="config-search" id="search-box" placeholder="$T('cmenu-search')" onkeyup="doConfigSearch(this)">
</form>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<div id="content" class="container">
<!-- Content start -->

View File

@@ -1,117 +1,143 @@
<!--#set global $pane="Config"#-->
<!--#set global $help_uri="configure-1-0"#-->
<!--#set global $help_uri="configuration/2.0/configure"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<!--#from sabnzbd.newswrapper import HAVE_SSL#-->
<!--#import sabnzbd.utils.sslinfo#-->
<!--#from sabnzbd.decoder import HAVE_YENC#-->
<!--#from sabnzbd.newsunpack import PAR2_COMMAND, PAR2C_COMMAND, RAR_COMMAND, ZIP_COMMAND, SEVEN_COMMAND, NICE_COMMAND, IONICE_COMMAND#-->
<div class="colmask">
<div class="section padTable">
<table class="table table-striped">
<tbody>
<tr>
<th scope="row">$T('version'): </th>
<td>$version [$build]</td>
</tr>
<tr>
<th scope="row">$T('uptime'): </th>
<td>$uptime</td>
</tr>
<tr>
<th scope="row">$T('confgFile'): </th>
<td>$configfn</td>
</tr>
<tr>
<th scope="row">$T('cache').capitalize(): </th>
<td><!--#set $msg=$T('ft-buffer@2')%($cache_art, $cache_size)#-->$msg</td>
</tr>
<tr>
<th scope="row">$T('parameters'): </th>
<td>$cmdline</td>
</tr>
<tr>
<th scope="row">$T('pythonVersion'): </th>
<td>$sys.version[:120]</td>
</tr>
<tr>
<th scope="row">OpenSSL:</th>
<td>
<!--#if HAVE_SSL#-->
<!--#set $sslversions = ', '.join(sabnzbd.utils.sslinfo.ssl_protocols())#-->
$sabnzbd.utils.sslinfo.ssl_version() <em>[$sslversions]</em>
<!--#else#-->
<span class="label label-warning">$T('notAvailable')</span>
<a href="$helpuri$help_uri#no_ssl" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
<!--#end if#-->
</td>
</tr>
<tr>
<th scope="row">pyOpenSSL:</th>
<td>
<!--#if HAVE_SSL#-->
$sabnzbd.utils.sslinfo.pyopenssl_version()
<!--#else#-->
<span class="label label-warning">$T('notAvailable')</span>
<a href="$helpuri$help_uri#no_ssl" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
<!--#end if#-->
</td>
</tr>
<tr>
<th scope="row">yEnc:</th>
<td>
<!--#if HAVE_YENC#-->
<span class="glyphicon glyphicon-ok"></span>
<!--#else#-->
<span class="label label-warning">$T('notAvailable')</span>
<a href="$helpuri$help_uri#no_yenc" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
<!--#end if#-->
</td>
</tr>
</tbody>
</table>
</div>
<!--#from locale import getpreferredencoding#-->
<div class="colmask">
<div class="section padTable">
<table class="table table-striped">
<tbody>
<tr>
<th scope="row">$T('version'): </th>
<td>$version [$build]</td>
</tr>
<tr>
<th scope="row">$T('uptime'): </th>
<td>$uptime</td>
</tr>
<tr>
<th scope="row">$T('confgFile'): </th>
<td>$configfn</td>
</tr>
<tr>
<th scope="row">$T('parameters'): </th>
<td>$cmdline</td>
</tr>
<tr>
<th scope="row">$T('pythonVersion'): </th>
<td>$sys.version[:120] [$getpreferredencoding()]</td>
</tr>
<tr>
<th scope="row">OpenSSL:</th>
<td>
$ssl_version &nbsp; [$ssl_protocols]
</td>
</tr>
<!--#if not $have_ssl_context#-->
<tr>
<th scope="row"></th>
<td>
<span class="label label-danger">$T('warning')</span> $T('explain-nosslcontext')
</td>
</tr>
<!--#end if#-->
<!--#if not $have_mt_par2#-->
<tr>
<th scope="row">Multicore Par2</th>
<td>
<span class="label label-warning">$T('notAvailable')</span> $T('explain-getpar2mt')
<a href="${helpuri}installation/multicore-par2" target="_blank">${helpuri}installation/multicore-par2</a>
</td>
</tr>
<!--#end if#-->
<!--#if not $have_cryptography #-->
<tr>
<th scope="row">Python Cryptography:</th>
<td>
<span class="label label-warning">$T('notAvailable')</span>
<a href="$helpuri$help_uri#no_cryptography" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
<!--#if not $have_yenc and not $have_sabyenc#-->
<tr>
<th scope="row">yEnc:</th>
<td>
<span class="label label-danger">$T('notAvailable')</span>
<a href="$helpuri$help_uri#no_yenc" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
<!--#if not $have_sabyenc#-->
<tr>
<th scope="row">SABYenc:</th>
<td>
<span class="label label-danger">$T('notAvailable')</span>
<a href="$helpuri$help_uri#no_sabyenc" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
<!--#if not $have_unzip #-->
<tr>
<th scope="row">$T('opt-enable_unzip'):</th>
<td>
<span class="label label-warning">$T('notAvailable')</span>
<a href="${helpuri}installation/install-off-modules#toc8" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
<!--#if not $have_7zip #-->
<tr>
<th scope="row">$T('opt-enable_7zip'):</th>
<td>
<span class="label label-warning">$T('notAvailable')</span>
<a href="${helpuri}installation/install-off-modules#toc8" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
</tbody>
</table>
</div>
<div class="colmask">
<div class="section padTable">
<table class="table table-striped">
<tbody>
<tr>
<th scope="row">$T('homePage') </th>
<td><a href="http://sabnzbd.org/" target="_blank">http://sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('menu-wiki') </th>
<td><a href="http://wiki.sabnzbd.org/faq" target="_blank">http://wiki.sabnzbd.org/faq</a></td>
</tr>
<tr>
<th scope="row">$T('menu-forums') </th>
<td><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('source') </th>
<td><a href="https://github.com/sabnzbd/sabnzbd" target="_blank">https://github.com/sabnzbd/sabnzbd</a></td>
</tr>
<tr>
<th scope="row">$T('menu-irc') </th>
<td><a href="irc://irc.synirc.net/#sabnzbd"><i>#sabnzbd</i> on <i>irc.synirc.net</i></a> $T('or') (<a href="http://sabnzbd.org/live-chat/" target="_blank">webchat</a>)</td>
</tr>
<tr>
<th scope="row">$T('menu-issues') </th>
<td><a href="http://wiki.sabnzbd.org/issues-1-0-0" target="_blank">http://wiki.sabnzbd.org/issues-1-0-0</a></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="colmask">
<div class="section padTable">
<table class="table table-striped">
<tbody>
<tr>
<th scope="row">$T('homePage') </th>
<td><a href="http://sabnzbd.org/" target="_blank">http://sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('menu-wiki') </th>
<td><a href="https://sabnzbd.org/wiki/" target="_blank">https://sabnzbd.org/wiki/</a></td>
</tr>
<tr>
<th scope="row">$T('menu-forums') </th>
<td><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('source') </th>
<td><a href="https://github.com/sabnzbd/sabnzbd" target="_blank">https://github.com/sabnzbd/sabnzbd</a></td>
</tr>
<tr>
<th scope="row">$T('menu-irc') </th>
<td><a href="irc://irc.synirc.net/#sabnzbd"><i>#sabnzbd</i> on <i>irc.synirc.net</i></a> $T('or') (<a href="http://sabnzbd.org/live-chat/" target="_blank">webchat</a>)</td>
</tr>
<tr>
<th scope="row">$T('menu-issues') </th>
<td><a href="https://sabnzbd.org/wiki/introduction/known-issues" target="_blank">https://sabnzbd.org/wiki/introduction/known-issues</a></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="colmask">
<div class="padding alt">
<h5 class="copyright">Copyright &copy; 2008-2017 The SABnzbd Team &lt;<a href="mailto:team@sabnzbd.org">team@sabnzbd.org</a>&gt;</h5>
<p class="copyright"><small>$T('yourRights')</small></p>
</div>
<div class="colmask">
<div class="padding alt">
<h5 class="copyright">Copyright &copy; 2008-2016 The SABnzbd Team &lt;<a href="mailto:team@sabnzbd.org">team@sabnzbd.org</a>&gt;</h5>
<p class="copyright"><small>$T('yourRights')</small></p>
</div>
</div>
</div>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

View File

@@ -1,35 +1,34 @@
<!--#set global $pane="Categories"#-->
<!--#set global $help_uri="configure-categories-1-0"#-->
<!--#set global $help_uri="configuration/2.0/categories"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<div class="section">
<div class="padTable"> <a class="main-helplink" href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
<p> $T('explain-catTags') $T('explain-catTags2')<br/> </p>
<p>$T('explain-catTags2')<br/>$T('explain-catTags')</p>
<hr>
<h5 class="darkred"><strong>$T('explain-relFolder'):</strong> <span class="path">$defdir</span></h5>
<!--#set $odd = False#-->
<!--#set $cur = 0#-->
<!--#for $slot in $slotinfo#-->
<!--#set $odd = not $odd#-->
<!--#set $cur = $cur+1#-->
<form action="save" method="get">
<!--#for $cur, $slot in enumerate($slotinfo)#-->
<!--#set $cansort = $slot.name != '*' and $slot.name != ''#-->
<form action="save" method="get" <!--#if $cansort#-->class="sorting-row"<!--#end if#-->>
<table class="catTable">
<!--#if $cur == 1#-->
<!--#if $cur == 0#-->
<tr>
<th></th>
<th>$T('category')</th>
<th>$T('priority')</th>
<th>$T('mode')</th>
<!--#if $script_list#-->
<th>$T('script')</th>
<!--#end if#-->
<th>$T('catFolderPath')</th>
<th>$T('catTags')</th>
<th>&nbsp;</th>
<th colspan="2">$T('catTags')</th>
</tr>
<!--#end if#-->
<tr class="<!--#if $odd then " alt " else " "#-->" <!--#if $slot.name=='*' #-->style="background-color: #FFFFE0;"<!--#end if#-->>
<tr>
<td>
<span class="glyphicon glyphicon-option-vertical"></span>
</td>
<td>
<input type="hidden" name="session" value="$session" />
<input type="hidden" name="order" value="$slot.order" />
<input type="hidden" value="$slot.name" name="name" />
<!--#if $slot.name != '*'#-->
<input type="text" name="newname" value="$slot.name" size="10" />
@@ -60,49 +59,83 @@
<option value="3" <!--#if $slot.pp=="3" then 'selected="selected"' else ""#-->>$T('pp-delete')</option>
</select>
</td>
<!--#if $script_list#-->
<td>
<!--#if $scripts#-->
<select name="script">
<!--#for $sc in $script_list#-->
<!--#if not ($sc == 'Default' and $slot.name == '*')#-->
<option value="$sc" <!--#if $slot.script.lower()==$sc.lower() then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end if#-->
<!--#for $sc in $scripts#-->
<!--#if not ($sc == 'Default' and $slot.name == '*')#-->
<option value="$sc" <!--#if $slot.script.lower()==$sc.lower() then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end if#-->
<!--#end for#-->
</select>
<!--#else#-->
<select name="script" disabled>
</select>
<!--#end if#-->
</td>
<!--#end if#-->
<td class="nowrap">
<input type="text" name="dir" class="fileBrowserSmall" value="$slot.dir" size="20" data-initialdir="$defdir" data-title="$T('catFolderPath')" />
<input type="text" name="dir" class="fileBrowserSmall" value="$slot.dir" size="20" data-initialdir="$defdir" data-title="$T('catFolderPath')" title="$T('explain-catTags2')" />
</td>
<td>
<input type="text" name="newzbin" value="$slot.newzbin" size="20" />
<input type="text" name="newzbin" value="$slot.newzbin" size="20" />
</td>
<td class="nowrap">
<button class="btn btn-default">
<!--#if $cur == 2#--><span class="glyphicon glyphicon-plus"></span> $T('button-add')
<!--#else#--><span class="glyphicon glyphicon-ok"></span> $T('button-save')
<!--#if $cur == 1#-->
<span class="glyphicon glyphicon-plus"></span> $T('button-add')
<!--#else#-->
<span class="glyphicon glyphicon-ok"></span> $T('button-save')
<!--#end if#-->
</button>
<!--#if $slot.name and $slot.name != '*'#-->
<!--#if $cansort#-->
<button class="btn btn-default delCat" type="button"><span class="glyphicon glyphicon-trash"></span></button>
<!--#end if#-->
</td>
</tr>
</table>
</form>
<!--#if not $cansort#-->
<hr>
<!--#end if#-->
<!--#end for#-->
</div>
</div>
</div>
<script type="text/javascript" src="${root}staticcfg/js/jquery-ui.min.js"></script>
<script type="text/javascript">
\$(document).ready(function() {
\$('.delCat').click(function() {
var theForm = \$(this).closest("form");
theForm.attr("action", "delete").submit();
});
// Add autocomplete and file-browser
\$('.fileBrowserSmall').typeahead().fileBrowser();
// Make categories sortable
\$('.padTable').sortable({
items: '.sorting-row',
containment: '.colmask',
axis: 'y',
update: function(event, ui) {
\$('.Categories form.sorting-row').each(function(index, elm) {
// Update order of all elements
if(index != elm.order.value) {
elm.order.value = index
// Submit changed order
var data = {}
\$(elm).extractFormDataTo(data);
\$.ajax({
type: "GET",
url: window.location.pathname + 'save',
data: data,
async: false // To prevent race-conditions when updating categories
})
}
})
}
})
});
</script>

View File

@@ -1,5 +1,5 @@
<!--#set global $pane="Folders"#-->
<!--#set global $help_uri="configure-folders-1-0"#-->
<!--#set global $help_uri="configuration/2.0/folders"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
@@ -65,7 +65,7 @@
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span id="config_err_msg" class="darkred nomargin">&nbsp;</span>
</div>

View File

@@ -1,5 +1,5 @@
<!--#set global $pane="General"#-->
<!--#set global $help_uri="configure-general-1-0"#-->
<!--#set global $help_uri="configuration/2.0/general"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
@@ -20,132 +20,68 @@
</div>
<div class="field-pair">
<label class="config" for="port">$T('opt-port')</label>
<input type="number" name="port" id="port" value="$port" size="8" />
<input type="number" name="port" id="port" value="$port" size="8" data-original="$port" />
<span class="desc">$T('explain-port')</span>
</div>
<div class="field-pair">
<label class="config" for="username">$T('opt-web_username')</label>
<input type="text" name="username" id="username" value="$username" />
<span class="desc">$T('explain-web_username')</span>
</div>
<div class="field-pair">
<label class="config" for="password">$T('opt-web_password')</label>
<input type="text" name="password" id="password" value="$password" />
<span class="desc">$T('explain-web_password')</span>
</div>
<div class="field-pair">
<label class="config" for="web_dir">$T('opt-web_dir')</label>
<select name="web_dir" id="web_dir">
<!--#for $webline in $web_list#-->
<!--#if $webline.lower() == $web_dir.lower()#-->
<option value="$webline" selected="selected">$webline</option>
<!--#else#-->
<option value="$webline">$webline</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-web_dir')&nbsp;&nbsp;<a href="$caller_url1">$caller_url1</a></span>
</div>
<div class="field-pair">
<label class="config" for="web_dir2">$T('opt-web_dir2')</label>
<select name="web_dir2" id="web_dir2">
<option value="None" selected="selected">$T("None")</option>
<!--#for $webline in $web_list#-->
<!--#if $webline.lower() == $web_dir2.lower()#-->
<option value="$webline" selected="selected">$webline</option>
<!--#else#-->
<option value="$webline">$webline</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-web_dir2')&nbsp;&nbsp;<a href="$caller_url2">$caller_url2</a></span>
</div>
<div class="field-pair">
<label class="config" for="language">$T('opt-language')</label>
<select name="language" id="language" class="select">
<!--#for $webline in $lang_list#-->
<!--#if $webline[0].lower() == $language.lower()#-->
<option value="$webline[0]" selected="selected">$webline[1]</option>
<!--#else#-->
<option value="$webline[0]">$webline[1]</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-language')</span>
</div>
<div class="field-pair">
<label class="config" for="local_ranges">$T('opt-local_ranges')</label>
<input type="text" name="local_ranges" id="local_ranges" value="$local_ranges" />
<span class="desc">$T('explain-local_ranges')</span>
</div>
<div class="field-pair">
<label class="config" for="inet_exposure">$T('opt-inet_exposure')</label>
<select name="inet_exposure" id="inet_exposure" class="select">
<option value="0" <!--#if $inet_exposure == 0 then 'selected="selected"' else ""#-->>$T('inet-local')</option>
<option value="1" <!--#if $inet_exposure == 1 then 'selected="selected"' else ""#-->>$T('inet-nzb')</option>
<option value="2" <!--#if $inet_exposure == 2 then 'selected="selected"' else ""#-->>$T('inet-api')</option>
<option value="3" <!--#if $inet_exposure == 3 then 'selected="selected"' else ""#-->>$T('inet-fullapi')</option>
<option value="4" <!--#if $inet_exposure == 4 then 'selected="selected"' else ""#-->>$T('inet-ui')</option>
</select>
<span class="desc">$T('explain-inet_exposure')</span>
</div>
<div class="field-pair">
<label class="config" for="disable_api_key">$T('opt-disableApikey')</label>
<input type="checkbox" name="disable_api_key" id="disable_api_key" value="1" <!--#if int($disable_api_key) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-disableApikey')</span>
</div>
<div class="field-pair">
<label class="config" for="apikey">$T('opt-apikey')</label>
<input type="text" id="apikey" class="fileBrowserField" value="$session" />
<button class="btn btn-default show_qrcode" title="$T('explain-qr-code')" rel="$session" ><span class="glyphicon glyphicon-qrcode"></span></button>
<button class="btn btn-default" id="generate_new_apikey" title="$T('button-apikey')"><span class="glyphicon glyphicon-repeat"></span></button>
<span class="desc">$T('explain-apikey')</span>
</div>
<div class="field-pair">
<label class="config" for="nzbkey">$T('opt-nzbkey')</label>
<input type="text" id="nzbkey" class="fileBrowserField" value="$nzb_key" />
<button class="btn btn-default show_qrcode" title="$T('explain-qr-code')" rel="$nzb_key" ><span class="glyphicon glyphicon-qrcode"></span></button>
<button class="btn btn-default" id="generate_new_nzbkey" title="$T('button-apikey')"><span class="glyphicon glyphicon-repeat"></span></button>
<span class="desc">$T('explain-nzbkey')</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default sabnzbd_restart"><span class="glyphicon glyphicon-refresh"></span> $T('button-restart') SABnzbd</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('httpsSupport') <a href="$helpuri$help_uri#toc1" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p><b>$T('restartRequired')</b></p>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<h5 class="darkred nomargin">$T('base-folder'): <span class="path">$my_lcldata</span></h5>
</div>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="enable_https">$T('opt-enable_https')</label>
<input type="checkbox" name="enable_https" id="enable_https" value="1" <!--#if int($enable_https) > 0 then 'checked="checked"' else ""#--> <!--#if int($have_ssl) == 0 then "disabled" else ""#--> />
<input type="checkbox" name="enable_https" id="enable_https" value="1" <!--#if int($enable_https) > 0 then 'checked="checked"' else ""#-->/>
<span class="desc">$T('explain-enable_https')</span>
</div>
<div class="field-pair">
<label class="config" for="web_dir">$T('opt-web_dir')</label>
<select name="web_dir" id="web_dir">
<!--#for $webline in $web_list#-->
<!--#if $webline.lower() == $web_dir.lower()#-->
<option value="$webline" selected="selected">$webline</option>
<!--#else#-->
<option value="$webline">$webline</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-web_dir')&nbsp;&nbsp;<a href="$caller_url">$caller_url</a></span>
</div>
<div class="field-pair">
<label class="config" for="language">$T('opt-language')</label>
<select name="language" id="language" class="select">
<!--#for $webline in $lang_list#-->
<!--#if $webline[0].lower() == $language.lower()#-->
<option value="$webline[0]" selected="selected">$webline[1]</option>
<!--#else#-->
<option value="$webline[0]">$webline[1]</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-language')</span>
<div class="alert alert-info alert-translate">
$T('explain-ask-language') <a href="https://sabnzbd.org/wiki/translate" target="_blank" class="alert-link">https://sabnzbd.org/wiki/translate</a>
</div>
</div>
<div class="field-pair advanced-settings">
<h5 class="darkred nomargin">$T('base-folder'): <span class="path">$my_lcldata</span></h5>
</div>
<div class="field-pair advanced-settings">
<label class="config" for="https_port">$T('opt-https_port')</label>
<input type="number" name="https_port" id="https_port" value="$https_port" size="8" />
<input type="number" name="https_port" id="https_port" value="$https_port" size="8" data-original="$https_port" />
<span class="desc">$T('explain-https_port')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="https_cert">$T('opt-https_cert')</label>
<input type="text" name="https_cert" id="https_cert" value="$https_cert" />
<button class="btn btn-default generate_cert" title="$T('explain-new-cert')" <!--#if int($have_cryptography) == 0 then "disabled" else ""#-->>
<span class="glyphicon glyphicon-repeat"></span>
</button>
<span class="desc">$T('explain-https_cert')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="https_key">$T('opt-https_key')</label>
<input type="text" name="https_key" id="https_key" value="$https_key" />
<button class="btn btn-default generate_cert" title="$T('explain-new-cert')" <!--#if int($have_cryptography) == 0 then "disabled" else ""#-->>
<span class="glyphicon glyphicon-repeat"></span>
</button>
<span class="desc">$T('explain-https_key')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="https_chain">$T('opt-https_chain')</label>
<input type="text" name="https_chain" id="https_chain" value="$https_chain" />
<span class="desc">$T('explain-https_chain')</span>
@@ -153,6 +89,97 @@
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default sabnzbd_restart"><span class="glyphicon glyphicon-refresh"></span> $T('button-restart') SABnzbd</button>
<button class="btn btn-default advancedButton enable_https_options"><span class="glyphicon glyphicon-cog"></span> $T('button-advanced')</button>
</div>
</fieldset>
</div>
</div>
<div class="section">
<div class="col2">
<h3>$T('security') <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p><b>$T('restartRequired')</b></p>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<!-- Tricks to avoid browser auto-fill, fixed on-submit with javascript -->
<div class="field-pair">
<label class="config" for="${pid}_wu">$T('opt-web_username')</label>
<input type="text" name="${pid}_wu" id="${pid}_wu" value="$username" data-hide="username" />
<span class="desc">$T('explain-web_username')</span>
</div>
<div class="field-pair">
<label class="config" for="${pid}_wp">$T('opt-web_password')</label>
<input type="text" name="${pid}_wp" id="${pid}_wp" value="$password" data-hide="password" />
<span class="desc">$T('explain-web_password')</span>
</div>
<div class="field-pair">
<label class="config" for="inet_exposure">$T('opt-inet_exposure')</label>
<select name="inet_exposure" id="inet_exposure" class="select">
<optgroup label="API">
<option value="0" <!--#if $inet_exposure == 0 then 'selected="selected"' else ""#-->>$T('inet-local')</option>
<option value="1" <!--#if $inet_exposure == 1 then 'selected="selected"' else ""#-->>$T('inet-nzb')</option>
<option value="2" <!--#if $inet_exposure == 2 then 'selected="selected"' else ""#-->>$T('inet-api')</option>
<option value="3" <!--#if $inet_exposure == 3 then 'selected="selected"' else ""#-->>$T('inet-fullapi')</option>
</optgroup>
<optgroup label="$T('inet-fullapi') &amp; $T('opt-web_dir')">
<option value="4" <!--#if $inet_exposure == 4 then 'selected="selected"' else ""#-->>$T('inet-ui')</option>
<option value="5" <!--#if $inet_exposure == 5 then 'selected="selected"' else ""#-->>$T('inet-ui') - $T('inet-external_login')</option>
</optgroup>
</select>
<span class="desc">$T('explain-inet_exposure').replace('. ','.<br><span class="label label-warning">'+$T('warning').upper()+'</span> ')</span>
</div>
<div class="field-pair">
<label class="config" for="local_ranges">$T('opt-local_ranges')</label>
<input type="text" name="local_ranges" id="local_ranges" value="$local_ranges" />
<span class="desc">$T('explain-local_ranges')</span>
</div>
<div class="field-pair">
<label class="config" for="apikey">$T('opt-apikey')</label>
<input type="text" id="apikey" class="fileBrowserField" value="$session" readonly />
<button class="btn btn-default show_qrcode" title="$T('explain-qr-code')" rel="$session" ><span class="glyphicon glyphicon-qrcode"></span></button>
<button class="btn btn-default generate_key" id="generate_new_apikey" title="$T('button-apikey')"><span class="glyphicon glyphicon-repeat"></span></button>
<span class="desc">$T('explain-apikey')</span>
</div>
<div class="field-pair">
<label class="config" for="nzbkey">$T('opt-nzbkey')</label>
<input type="text" id="nzbkey" class="fileBrowserField" value="$nzb_key" readonly />
<button class="btn btn-default show_qrcode" title="$T('explain-qr-code')" rel="$nzb_key" ><span class="glyphicon glyphicon-qrcode"></span></button>
<button class="btn btn-default generate_key" id="generate_new_nzbkey" title="$T('button-apikey')"><span class="glyphicon glyphicon-repeat"></span></button>
<span class="desc">$T('explain-nzbkey')</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('cmenu-switches') <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<label class="config" for="auto_browser">$T('opt-auto_browser')</label>
<input type="checkbox" name="auto_browser" id="auto_browser" value="1" <!--#if int($auto_browser) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-auto_browser')</span>
</div>
<div class="field-pair">
<label class="config" for="check_new_rel">$T('opt-check_new_rel')</label>
<select name="check_new_rel" id="check_new_rel">
<option value="0" <!--#if $check_new_rel == 0 then 'selected="selected"' else ""#--> >$T('off')</option>
<option value="1" <!--#if $check_new_rel == 1 then 'selected="selected"' else ""#--> >$T('on')</option>
<option value="2" <!--#if $check_new_rel == 2 then 'selected="selected"' else ""#--> >$T('also-test')</option>
</select>
<span class="desc">$T('explain-check_new_rel')</span>
</div>
<div class="field-pair <!--#if int($have_ssl_context) == 0 then "disabled" else ""#-->">
<label class="config" for="enable_https_verification">$T('opt-enable_https_verification')</label>
<input type="checkbox" name="enable_https_verification" id="enable_https_verification" value="1" <!--#if int($enable_https_verification) > 0 then 'checked="checked"' else ""#--> <!--#if int($have_ssl_context) == 0 then "disabled=\"disabled\"" else ""#--> />
<span class="desc">$T('explain-enable_https_verification')</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div><!-- /col1 -->
@@ -163,10 +190,15 @@
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<label class="config" for="bandwidth_max">$T('opt-bandwidth_max')</label>
<input type="text" name="bandwidth_max" id="bandwidth_max" value="$bandwidth_max" class="smaller_input" />
<span class="desc">$T('explain-bandwidth_max')<br />$T('wizard-bandwidth-explain')</span>
<div class="field-pair value-and-select">
<label class="config" for="bandwidth_max_value">$T('opt-bandwidth_max')</label>
<input type="number" name="bandwidth_max_value" id="bandwidth_max_value" class="smaller_input" />
<select name="bandwidth_max_dropdown" id="bandwidth_max_dropdown">
<option value="">B/s</option>
<option value="K">KB/s</option>
<option value="M" selected>MB/s</option>
</select>
<input type="hidden" name="bandwidth_max" id="bandwidth_max" value="$bandwidth_max" />
</div>
<div class="field-pair">
<label class="config" for="bandwidth_perc">$T('opt-bandwidth_perc')</label>
@@ -176,7 +208,7 @@
<div class="field-pair">
<label class="config" for="cache_limit">$T('opt-cache_limitstr')</label>
<input type="text" name="cache_limit" id="cache_limit" value="$cache_limit" class="smaller_input" />
<span class="desc">$T('explain-cache_limitstr')</span>
<span class="desc">$T('explain-cache_limitstr').replace("64M", "256M").replace("128M", "512M")</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
@@ -184,6 +216,7 @@
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
</form>
</div><!-- /colmask -->
@@ -199,7 +232,19 @@
<script type="text/javascript">
\$(document).ready(function(){
// Show the message about translating when it's non-English
function hideOrShowTranslate() {
if(\$('#language').val() == 'en') {
\$('.alert-translate').hide()
} else {
\$('.alert-translate').show()
}
}
\$('#language').on('change', hideOrShowTranslate)
hideOrShowTranslate()
\$('#apikey, #nzbkey').click(function () { \$(this).select() });
\$('#generate_new_apikey').click(function () {
if (confirm("$T('Plush-confirm')")) {
$.ajax({
@@ -218,7 +263,7 @@
$.ajax({
type: "POST",
url: "../../tapi",
data: {mode:'config', name:'set_nzbkey', apikey: \$('#apikey').val()},
data: { mode:'config', name:'set_nzbkey', apikey: \$('#apikey').val() },
success: function(msg){
\$('#nzbkey').val(msg);
document.location = document.location;
@@ -228,22 +273,57 @@
});
\$('.show_qrcode').click(function (e) {
// Make QR code
var qrcode = \$('<img />', {
src: 'https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl=' + \$(this).attr('rel'),
alt: 'loading...',
width: 300,
height: 300
});
// Show in modal
\$('#modal_qr .modal-dialog').width(330)
\$('#modal_qr .modal-body').html(qrcode)
\$('#modal_qr .modal-body').html('').qrcode({
"size": 280,
"color": "#3a3",
"text": \$(this).attr('rel')
});
\$('#modal_qr').modal('show');
// No save on this button click
e.preventDefault();
e.preventDefault();
});
\$('.generate_cert').click(function(e) {
if(!confirm('$T('explain-new-cert')')) {
return;
}
// Submit request and then restart
$.ajax({
type: "POST",
url: "../../tapi",
data: { mode: 'config', name: 'regenerate_certs', apikey: \$('#apikey').val() },
success: function(msg) {
do_restart()
}
});
e.preventDefault();
})
// Only allow re-generate if default certs
if(\$('#https_cert').val() != 'server.cert') {
\$('.generate_cert').attr('disabled', 'disabled')
}
// Parse the text
var bandwidthLimit = \$('#bandwidth_max').val()
if(bandwidthLimit) {
var bandwithLimitNumber = parseFloat(bandwidthLimit)
var bandwithLimitText = bandwidthLimit.replace(/[^a-zA-Z]+/g, '');
\$('#bandwidth_max_value').val(bandwithLimitNumber)
\$('#bandwidth_max_dropdown').val(bandwithLimitText)
}
// Update the value
\$('#bandwidth_max_value, #bandwidth_max_dropdown').on('change', function() {
\$('#bandwidth_max').val(\$('#bandwidth_max_value').val() + \$('#bandwidth_max_dropdown').val())
})
});
</script>

View File

@@ -1,7 +1,18 @@
<!--#set global $pane="Email"#-->
<!--#set global $help_uri="configure-notifications-1-0"#-->
<!--#set global $help_uri="configuration/2.0/notifications"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<!--#def show_notify_checkboxes($section_label)#-->
<!--#for $type in $notify_keys#-->
<div class="field-pair">
<label class="config wide" for="${section_label}_prio_$type">
$T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
</label>
<input type="checkbox" name="${section_label}_prio_$type" id="${section_label}_prio_$type" value="1" <!--#if int($getVar($section_label + '_prio_' + $type)) > 0 then 'checked="checked"' else ""#--> />
</div>
<!--#end for#-->
<!--#end def#-->
<div class="colmask">
<form action="saveEmail" method="post" name="fullform" class="fullform" autocomplete="off" novalidate>
<input type="hidden" id="session" name="session" value="$session" />
@@ -38,7 +49,7 @@
<div class="field-pair">
<label class="config" for="email_server">$T('opt-email_server')</label>
<input type="text" name="email_server" id="email_server" value="$email_server" />
<span class="desc">$T('explain-email_server') ($T('host').lower():$T('srv-port').lower())</span>
<span class="desc">$T('explain-email_server') ($T('host'):$T('srv-port'))</span>
</div>
<div class="field-pair">
<label class="config" for="email_to">$T('opt-email_to')</label>
@@ -63,8 +74,9 @@
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_email" rel="$T('askTestEmail')"><span class="glyphicon glyphicon-envelope"></span> $T('link-testEmail')</button>
<span id="testmail-result" class="icon path darkred">&nbsp;</span>
<span id="config_err_msg" class="icon path darkred">&nbsp;</span>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
@@ -82,51 +94,14 @@
</div><!-- /col2 -->
<div class="col1" <!--#if int($ncenter_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
$show_notify_checkboxes('ncenter')
<div class="field-pair">
<label class="config wide" for="ncenter_prio_startup">$T($notify_texts['startup']).replace('/', ' / ')</label>
<input type="checkbox" name="ncenter_prio_startup" id="ncenter_prio_startup" value="1" <!--#if int($ncenter_prio_startup) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ncenter_prio_download">$T($notify_texts['download']) / $T('link-pause') / $T('link-resume')</label>
<input type="checkbox" name="ncenter_prio_download" id="ncenter_prio_download" value="1" <!--#if int($ncenter_prio_download) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ncenter_prio_pp">$T($notify_texts['pp'])</label>
<input type="checkbox" name="ncenter_prio_pp" id="ncenter_prio_pp" value="1" <!--#if int($ncenter_prio_pp) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ncenter_prio_complete">$T($notify_texts['complete'])</label>
<input type="checkbox" name="ncenter_prio_complete" id="ncenter_prio_complete" value="1" <!--#if int($ncenter_prio_complete) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ncenter_prio_failed">$T($notify_texts['failed'])</label>
<input type="checkbox" name="ncenter_prio_failed" id="ncenter_prio_failed" value="1" <!--#if int($ncenter_prio_failed) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ncenter_prio_queue_done">$T($notify_texts['queue_done'])</label>
<input type="checkbox" name="ncenter_prio_queue_done" id="ncenter_prio_queue_done" value="1" <!--#if int($ncenter_prio_queue_done) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ncenter_prio_disk_full">$T($notify_texts['disk_full'])</label>
<input type="checkbox" name="ncenter_prio_disk_full" id="ncenter_prio_disk_full" value="1" <!--#if int($ncenter_prio_disk_full) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ncenter_prio_warning">$T($notify_texts['warning'])</label>
<input type="checkbox" name="ncenter_prio_warning" id="ncenter_prio_warning" value="1" <!--#if int($ncenter_prio_warning) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ncenter_prio_error">$T($notify_texts['error'])</label>
<input type="checkbox" name="ncenter_prio_error" id="ncenter_prio_error" value="1" <!--#if int($ncenter_prio_error) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ncenter_prio_other">$T($notify_texts['other'])</label>
<input type="checkbox" name="ncenter_prio_other" id="ncenter_prio_other" value="1" <!--#if int($ncenter_prio_other) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_notification"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
<span id="testnotice-result" class="icon path darkred">&nbsp;</span>
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_notif"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
@@ -144,51 +119,14 @@
</div><!-- /col2 -->
<div class="col1" <!--#if int($acenter_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
$show_notify_checkboxes('acenter')
<div class="field-pair">
<label class="config wide" for="acenter_prio_startup">$T($notify_texts['startup']).replace('/', ' / ')</label>
<input type="checkbox" name="acenter_prio_startup" id="acenter_prio_startup" value="1" <!--#if int($acenter_prio_startup) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="acenter_prio_download">$T($notify_texts['download']) / $T('link-pause') / $T('link-resume')</label>
<input type="checkbox" name="acenter_prio_download" id="acenter_prio_download" value="1" <!--#if int($acenter_prio_download) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="acenter_prio_pp">$T($notify_texts['pp'])</label>
<input type="checkbox" name="acenter_prio_pp" id="acenter_prio_pp" value="1" <!--#if int($acenter_prio_pp) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="acenter_prio_complete">$T($notify_texts['complete'])</label>
<input type="checkbox" name="acenter_prio_complete" id="acenter_prio_complete" value="1" <!--#if int($acenter_prio_complete) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="acenter_prio_failed">$T($notify_texts['failed'])</label>
<input type="checkbox" name="acenter_prio_failed" id="acenter_prio_failed" value="1" <!--#if int($acenter_prio_failed) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="acenter_prio_queue_done">$T($notify_texts['queue_done'])</label>
<input type="checkbox" name="acenter_prio_queue_done" id="acenter_prio_queue_done" value="1" <!--#if int($acenter_prio_queue_done) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="acenter_prio_disk_full">$T($notify_texts['disk_full'])</label>
<input type="checkbox" name="acenter_prio_disk_full" id="acenter_prio_disk_full" value="1" <!--#if int($acenter_prio_disk_full) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="acenter_prio_warning">$T($notify_texts['warning'])</label>
<input type="checkbox" name="acenter_prio_warning" id="acenter_prio_warning" value="1" <!--#if int($acenter_prio_warning) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="acenter_prio_error">$T($notify_texts['error'])</label>
<input type="checkbox" name="acenter_prio_error" id="acenter_prio_error" value="1" <!--#if int($acenter_prio_error) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="acenter_prio_other">$T($notify_texts['other'])</label>
<input type="checkbox" name="acenter_prio_other" id="acenter_prio_other" value="1" <!--#if int($acenter_prio_other) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_windows_notification"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
<span id="testnotice-result" class="icon path darkred">&nbsp;</span>
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_windows"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
@@ -206,51 +144,14 @@
</div><!-- /col2 -->
<div class="col1" <!--#if int($ntfosd_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
$show_notify_checkboxes('ntfosd')
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_startup">$T($notify_texts['startup']).replace('/', ' / ')</label>
<input type="checkbox" name="ntfosd_prio_startup" id="ntfosd_prio_startup" value="1" <!--#if int($ntfosd_prio_startup) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_download">$T($notify_texts['download']) / $T('link-pause') / $T('link-resume')</label>
<input type="checkbox" name="ntfosd_prio_download" id="ntfosd_prio_download" value="1" <!--#if int($ntfosd_prio_download) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_pp">$T($notify_texts['pp'])</label>
<input type="checkbox" name="ntfosd_prio_pp" id="ntfosd_prio_pp" value="1" <!--#if int($ntfosd_prio_pp) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_complete">$T($notify_texts['complete'])</label>
<input type="checkbox" name="ntfosd_prio_complete" id="ntfosd_prio_complete" value="1" <!--#if int($ntfosd_prio_complete) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_failed">$T($notify_texts['failed'])</label>
<input type="checkbox" name="ntfosd_prio_failed" id="ntfosd_prio_failed" value="1" <!--#if int($ntfosd_prio_failed) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_queue_done">$T($notify_texts['queue_done'])</label>
<input type="checkbox" name="ntfosd_prio_queue_done" id="ntfosd_prio_queue_done" value="1" <!--#if int($ntfosd_prio_queue_done) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_disk_full">$T($notify_texts['disk_full'])</label>
<input type="checkbox" name="ntfosd_prio_disk_full" id="ntfosd_prio_disk_full" value="1" <!--#if int($ntfosd_prio_disk_full) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_warning">$T($notify_texts['warning'])</label>
<input type="checkbox" name="ntfosd_prio_warning" id="ntfosd_prio_warning" value="1" <!--#if int($ntfosd_prio_warning) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_error">$T($notify_texts['error'])</label>
<input type="checkbox" name="ntfosd_prio_error" id="ntfosd_prio_error" value="1" <!--#if int($ntfosd_prio_error) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="ntfosd_prio_other">$T($notify_texts['other'])</label>
<input type="checkbox" name="ntfosd_prio_other" id="ntfosd_prio_other" value="1" <!--#if int($ntfosd_prio_other) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_osd"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
<span id="testosd-result" class="icon path darkred">&nbsp;</span>
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_osd"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
@@ -277,51 +178,14 @@
<input type="text" name="growl_password" id="growl_password" value="$growl_password" />
<span class="desc">$T('explain-growl_password')</span>
</div>
$show_notify_checkboxes('growl')
<div class="field-pair">
<label class="config wide" for="growl_prio_startup">$T($notify_texts['startup']).replace('/', ' / ')</label>
<input type="checkbox" name="growl_prio_startup" id="growl_prio_startup" value="1" <!--#if int($growl_prio_startup) > 0 then 'checked="checked"' else ""#--> />
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_growl"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair">
<label class="config wide" for="growl_prio_download">$T($notify_texts['download']) / $T('link-pause') / $T('link-resume')</label>
<input type="checkbox" name="growl_prio_download" id="growl_prio_download" value="1" <!--#if int($growl_prio_download) > 0 then 'checked="checked"' else ""#--> />
<div class="field-pair result-box">
<div class="alert"></div>
</div>
<div class="field-pair">
<label class="config wide" for="growl_prio_pp">$T($notify_texts['pp'])</label>
<input type="checkbox" name="growl_prio_pp" id="growl_prio_pp" value="1" <!--#if int($growl_prio_pp) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="growl_prio_complete">$T($notify_texts['complete'])</label>
<input type="checkbox" name="growl_prio_complete" id="growl_prio_complete" value="1" <!--#if int($growl_prio_complete) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="growl_prio_failed">$T($notify_texts['failed'])</label>
<input type="checkbox" name="growl_prio_failed" id="growl_prio_failed" value="1" <!--#if int($growl_prio_failed) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="growl_prio_queue_done">$T($notify_texts['queue_done'])</label>
<input type="checkbox" name="growl_prio_queue_done" id="growl_prio_queue_done" value="1" <!--#if int($growl_prio_queue_done) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="growl_prio_disk_full">$T($notify_texts['disk_full'])</label>
<input type="checkbox" name="growl_prio_disk_full" id="growl_prio_disk_full" value="1" <!--#if int($growl_prio_disk_full) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="growl_prio_warning">$T($notify_texts['warning'])</label>
<input type="checkbox" name="growl_prio_warning" id="growl_prio_warning" value="1" <!--#if int($growl_prio_warning) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="growl_prio_error">$T($notify_texts['error'])</label>
<input type="checkbox" name="growl_prio_error" id="growl_prio_error" value="1" <!--#if int($growl_prio_error) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="growl_prio_other">$T($notify_texts['other'])</label>
<input type="checkbox" name="growl_prio_other" id="growl_prio_other" value="1" <!--#if int($growl_prio_other) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_growl"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
<span id="testgrowl-result" class="icon path darkred">&nbsp;</span>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
@@ -343,128 +207,36 @@
<input type="text" name="prowl_apikey" id="prowl_apikey" value="$prowl_apikey" />
<span class="desc">$T('explain-prowl_apikey')</span>
</div>
<!--#set $section_label = 'prowl'#-->
<!--#for $type in $notify_keys#-->
<div class="field-pair">
<label class="config" for="${section_label}_prio_$type">
$T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
</label>
<select name="${section_label}_prio_$type" id="${section_label}_prio_$type">
<option value="-3" <!--#if $getVar($section_label + '_prio_' + $type) == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $getVar($section_label + '_prio_' + $type) == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $getVar($section_label + '_prio_' + $type) == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $getVar($section_label + '_prio_' + $type) == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $getVar($section_label + '_prio_' + $type) == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $getVar($section_label + '_prio_' + $type) == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<!--#end for#-->
<div class="field-pair">
<label class="config" for="prowl_prio_startup">$T($notify_texts['startup']).replace('/', ' / ')</label>
<select name="prowl_prio_startup" id="prowl_prio_startup">
<option value="-3" <!--#if $prowl_prio_startup == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_startup == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_startup == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_startup == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_startup == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_startup == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="prowl_prio_download">$T($notify_texts['download']) / $T('link-pause') / $T('link-resume')</label>
<select name="prowl_prio_download" id="prowl_prio_download">
<option value="-3" <!--#if $prowl_prio_download == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_download == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_download == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_download == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_download == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_download == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="prowl_prio_pp">$T($notify_texts['pp'])</label>
<select name="prowl_prio_pp" id="prowl_prio_pp">
<option value="-3" <!--#if $prowl_prio_pp == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_pp == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_pp == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_pp == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_pp == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_pp == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="prowl_prio_complete">$T($notify_texts['complete'])</label>
<select name="prowl_prio_complete" id="prowl_prio_complete">
<option value="-3" <!--#if $prowl_prio_complete == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_complete == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_complete == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_complete == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_complete == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_complete == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="prowl_prio_failed">$T($notify_texts['failed'])</label>
<select name="prowl_prio_failed" id="prowl_prio_failed">
<option value="-3" <!--#if $prowl_prio_failed == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_failed == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_failed == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_failed == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_failed == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_failed == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="prowl_prio_queue_done">$T($notify_texts['queue_done'])</label>
<select name="prowl_prio_queue_done" id="prowl_prio_queue_done">
<option value="-3" <!--#if $prowl_prio_queue_done == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_queue_done == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_queue_done == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_queue_done == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_queue_done == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_queue_done == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="prowl_prio_disk_full">$T($notify_texts['disk_full'])</label>
<select name="prowl_prio_disk_full" id="prowl_prio_disk_full">
<option value="-3" <!--#if $prowl_prio_disk_full == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_disk_full == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_disk_full == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_disk_full == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_disk_full == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_disk_full == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="prowl_prio_warning">$T($notify_texts['warning'])</label>
<select name="prowl_prio_warning" id="prowl_prio_warning">
<option value="-3" <!--#if $prowl_prio_warning == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_warning == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_warning == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_warning == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_warning == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_warning == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="prowl_prio_error">$T($notify_texts['error'])</label>
<select name="prowl_prio_error" id="prowl_prio_error">
<option value="-3" <!--#if $prowl_prio_error == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_error == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_error == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_error == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_error == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_error == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="prowl_prio_other">$T($notify_texts['other'])</label>
<select name="prowl_prio_other" id="prowl_prio_other">
<option value="-3" <!--#if $prowl_prio_other == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_other == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_other == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_other == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_other == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_other == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_prowl"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
<span id="testprowl-result" class="icon path darkred">&nbsp;</span>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section" id="pushover">
<div class="col2">
<h3>$T('section-Pushover')</h3>
<div class="section" id="pushover">
<div class="col2">
<h3>$T('section-Pushover')</h3>
<table>
<tr>
<td><input type="checkbox" name="pushover_enable" id="pushover_enable" value="1" <!--#if int($pushover_enable) > 0 then 'checked="checked"' else ""#--> /></td>
@@ -474,144 +246,51 @@
<em>$T('explain-pushover_enable')</em>
</div><!-- /col2 -->
<div class="col1" <!--#if int($pushover_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="pushover_token">$T('opt-pushover_token')</label>
<input type="text" name="pushover_token" id="pushover_token" value="$pushover_token" />
<span class="desc">$T('explain-pushover_token')</span>
</div>
<div class="field-pair">
<label class="config" for="pushover_userkey">$T('opt-pushover_userkey')</label>
<input type="text" name="pushover_userkey" id="pushover_userkey" value="$pushover_userkey" />
<span class="desc">$T('explain-pushover_userkey')</span>
</div>
<div class="field-pair">
<label class="config" for="pushover_device">$T('opt-pushover_device')</label>
<input type="text" name="pushover_device" id="pushover_device" value="$pushover_device" />
<span class="desc">$T('explain-pushover_device')</span>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_startup">$T($notify_texts['startup']).replace('/', ' / ')</label>
<select name="pushover_prio_startup" id="pushover_prio_startup">
<option value="-3" <!--#if $pushover_prio_startup == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_startup == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_startup == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_startup == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_startup == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_startup == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_download">$T($notify_texts['download']) / $T('link-pause') / $T('link-resume')</label>
<select name="pushover_prio_download" id="pushover_prio_download">
<option value="-3" <!--#if $pushover_prio_download == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_download == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_download == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_download == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_download == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_download == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_pp">$T($notify_texts['pp'])</label>
<select name="pushover_prio_pp" id="pushover_prio_pp">
<option value="-3" <!--#if $pushover_prio_pp == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_pp == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_pp == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_pp == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_pp == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_pp == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_complete">$T($notify_texts['complete'])</label>
<select name="pushover_prio_complete" id="pushover_prio_complete">
<option value="-3" <!--#if $pushover_prio_complete == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_complete == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_complete == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_complete == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_complete == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_complete == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_failed">$T($notify_texts['failed'])</label>
<select name="pushover_prio_failed" id="pushover_prio_failed">
<option value="-3" <!--#if $pushover_prio_failed == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_failed == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_failed == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_failed == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_failed == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_failed == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_queue_done">$T($notify_texts['queue_done'])</label>
<select name="pushover_prio_queue_done" id="pushover_prio_queue_done">
<option value="-3" <!--#if $pushover_prio_queue_done == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_queue_done == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_queue_done == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_queue_done == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_queue_done == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_queue_done == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_disk_full">$T($notify_texts['disk_full'])</label>
<select name="pushover_prio_disk_full" id="pushover_prio_disk_full">
<option value="-3" <!--#if $pushover_prio_disk_full == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_disk_full == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_disk_full == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_disk_full == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_disk_full == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_disk_full == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_warning">$T($notify_texts['warning'])</label>
<select name="pushover_prio_warning" id="pushover_prio_warning">
<option value="-3" <!--#if $pushover_prio_warning == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_warning == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_warning == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_warning == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_warning == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_warning == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_error">$T($notify_texts['error'])</label>
<select name="pushover_prio_error" id="pushover_prio_error">
<option value="-3" <!--#if $pushover_prio_error == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_error == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_error == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_error == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_error == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_error == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<label class="config" for="pushover_prio_other">$T($notify_texts['other'])</label>
<select name="pushover_prio_other" id="pushover_prio_other">
<option value="-3" <!--#if $pushover_prio_other == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $pushover_prio_other == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_other == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_other == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_other == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_other == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<fieldset>
<div class="field-pair">
<label class="config" for="pushover_token">$T('opt-pushover_token')</label>
<input type="text" name="pushover_token" id="pushover_token" value="$pushover_token" />
<span class="desc">$T('explain-pushover_token')</span>
</div>
<div class="field-pair">
<label class="config" for="pushover_userkey">$T('opt-pushover_userkey')</label>
<input type="text" name="pushover_userkey" id="pushover_userkey" value="$pushover_userkey" />
<span class="desc">$T('explain-pushover_userkey')</span>
</div>
<div class="field-pair">
<label class="config" for="pushover_device">$T('opt-pushover_device')</label>
<input type="text" name="pushover_device" id="pushover_device" value="$pushover_device" />
<span class="desc">$T('explain-pushover_device')</span>
</div>
<!--#set $section_label = 'pushover'#-->
<!--#for $type in $notify_keys#-->
<div class="field-pair">
<label class="config" for="${section_label}_prio_$type">
$T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
</label>
<select name="${section_label}_prio_$type" id="${section_label}_prio_$type">
<option value="-3" <!--#if $getVar($section_label + '_prio_' + $type) == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $getVar($section_label + '_prio_' + $type) == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $getVar($section_label + '_prio_' + $type) == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $getVar($section_label + '_prio_' + $type) == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $getVar($section_label + '_prio_' + $type) == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $getVar($section_label + '_prio_' + $type) == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<!--#end for#-->
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_pushover"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
<span id="testpushover-result" class="icon path darkred">&nbsp;</span>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section" id="pushbullet">
<div class="col2">
<h3>$T('section-Pushbullet')</h3>
<div class="section" id="pushbullet">
<div class="col2">
<h3>$T('section-Pushbullet')</h3>
<table>
<tr>
<td><input type="checkbox" name="pushbullet_enable" id="pushbullet_enable" value="1" <!--#if int($pushbullet_enable) > 0 then 'checked="checked"' else ""#--> /></td>
@@ -621,67 +300,68 @@
<em>$T('explain-pushbullet_enable')</em>
</div><!-- /col2 -->
<div class="col1" <!--#if int($pushbullet_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="pushbullet_apikey">$T('opt-pushbullet_apikey')</label>
<input type="text" name="pushbullet_apikey" id="pushbullet_apikey" value="$pushbullet_apikey" />
<span class="desc">$T('explain-pushbullet_apikey')</span>
</div>
<!--#if 0#-->
<div class="field-pair">
<label class="config" for="pushbullet_device">$T('opt-pushbullet_device')</label>
<input type="text" name="pushbullet_device" id="pushbullet_device" value="$pushbullet_device" />
<span class="desc">$T('explain-pushbullet_device')</span>
</div>
<!--#end if#-->
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_startup">$T($notify_texts['startup']).replace('/', ' / ')</label>
<input type="checkbox" name="pushbullet_prio_startup" id="pushbullet_prio_startup" value="1" <!--#if int($pushbullet_prio_startup) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_download">$T($notify_texts['download']) / $T('link-pause') / $T('link-resume')</label>
<input type="checkbox" name="pushbullet_prio_download" id="pushbullet_prio_download" value="1" <!--#if int($pushbullet_prio_download) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_pp">$T($notify_texts['pp'])</label>
<input type="checkbox" name="pushbullet_prio_pp" id="pushbullet_prio_pp" value="1" <!--#if int($pushbullet_prio_pp) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_complete">$T($notify_texts['complete'])</label>
<input type="checkbox" name="pushbullet_prio_complete" id="pushbullet_prio_complete" value="1" <!--#if int($pushbullet_prio_complete) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_failed">$T($notify_texts['failed'])</label>
<input type="checkbox" name="pushbullet_prio_failed" id="pushbullet_prio_failed" value="1" <!--#if int($pushbullet_prio_failed) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_queue_done">$T($notify_texts['queue_done'])</label>
<input type="checkbox" name="pushbullet_prio_queue_done" id="pushbullet_prio_queue_done" value="1" <!--#if int($pushbullet_prio_queue_done) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_disk_full">$T($notify_texts['disk_full'])</label>
<input type="checkbox" name="pushbullet_prio_disk_full" id="pushbullet_prio_disk_full" value="1" <!--#if int($pushbullet_prio_disk_full) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_warning">$T($notify_texts['warning'])</label>
<input type="checkbox" name="pushbullet_prio_warning" id="pushbullet_prio_warning" value="1" <!--#if int($pushbullet_prio_warning) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_error">$T($notify_texts['error'])</label>
<input type="checkbox" name="pushbullet_prio_error" id="pushbullet_prio_error" value="1" <!--#if int($pushbullet_prio_error) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="pushbullet_prio_other">$T($notify_texts['other'])</label>
<input type="checkbox" name="pushbullet_prio_other" id="pushbullet_prio_other" value="1" <!--#if int($pushbullet_prio_other) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<fieldset>
<div class="field-pair">
<label class="config" for="pushbullet_apikey">$T('opt-pushbullet_apikey')</label>
<input type="text" name="pushbullet_apikey" id="pushbullet_apikey" value="$pushbullet_apikey" />
<span class="desc">$T('explain-pushbullet_apikey')</span>
</div>
<!--#if 0#-->
<div class="field-pair">
<label class="config" for="pushbullet_device">$T('opt-pushbullet_device')</label>
<input type="text" name="pushbullet_device" id="pushbullet_device" value="$pushbullet_device" />
<span class="desc">$T('explain-pushbullet_device')</span>
</div>
<!--#end if#-->
$show_notify_checkboxes('pushbullet')
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_pushbullet"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
<span id="testpushbullet-result" class="icon path darkred">&nbsp;</span>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section" id="nscript">
<div class="col2">
<h3>$T('section-NScript')</h3>
<table>
<tr>
<td><input type="checkbox" name="nscript_enable" id="nscript_enable" value="1" <!--#if int($nscript_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="nscript_enable"> $T('opt-nscript_enable')</label></td>
</tr>
</table>
<em>$T('explain-nscript_enable')</em>
</div><!-- /col2 -->
<div class="col1" <!--#if int($nscript_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="nscript_script">$T('opt-nscript_script')</label>
<select name="nscript_script">
<!--#for $sc in $scripts#-->
<option value="$sc" <!--#if $nscript_script == $sc then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</select>
<span class="desc">$T('explain-nscript_script')</span>
</div>
<div class="field-pair">
<label class="config" for="nscript_parameters">$T('opt-nscript_parameters')</label>
<input type="text" name="nscript_parameters" id="nscript_parameters" value="$nscript_parameters" />
<span class="desc">$T('Optional') - $T('explain-nscript_parameters')</span>
</div>
$show_notify_checkboxes('nscript')
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_nscript"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
</form>
</div><!-- /colmask -->
@@ -689,7 +369,7 @@
\$(document).ready(function(){
// Autocomplete and filebrowser
\$('#email_dir').typeahead().fileBrowser();
// Expand on enable
\$('.col2 input[name$="enable"]').change(function() {
if(this.checked) {
@@ -699,198 +379,48 @@
}
\$('form').submit()
})
/**
Testing functions
**/
\$('#test_email').click(function () {
if (confirm(\$('#test_email').attr('rel'))) {
var data = { mode: 'test_email', apikey: '$session', output: 'json' };
\$("#email").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: data,
beforeSend: function () {
\$('#test_email').attr("disabled", "disabled");
\$('#testmail-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_email').removeAttr("disabled");
\$('#testmail-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testmail-result').addClass("success").html('$T('smpl-emailsent')');
} else {
\$('#testmail-result').addClass("failure").html(data.error);
}
}
});
function testNotification(buttonObj) {
// Confirm?
if(\$(buttonObj).attr('rel')) {
if(!confirm(\$(buttonObj).attr('rel'))) return false;
}
});
\$('#test_notification').click(function () {
// Disable button and get the data
\$(buttonObj).attr("disabled", "disabled")
\$(buttonObj).find('span').toggleClass('glyphicon-comment glyphicon-refresh spin-glyphicon')
var data = { mode: buttonObj.id, apikey: '$session', output: 'json' };
\$(buttonObj).parents('.section').extractFormDataTo(data);
// Clear up the box
resultBox = \$(buttonObj).parents('.section').find('.result-box .alert');
// Get the request
\$.ajax({
type: "GET",
url: "../../tapi",
data: {mode: 'test_notif', apikey: '$session', output: 'json' },
beforeSend: function () {
\$('#test_notification').attr("disabled", "disabled");
\$('#testnotice-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_notification').removeAttr("disabled");
\$('#testnotice-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testnotice-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testnotice-result').addClass("failure").html(data.error);
}
data: data
}).then(function(data) {
// Remove disabled and make the box
\$(buttonObj).removeAttr("disabled")
\$(buttonObj).find('span').toggleClass('glyphicon-comment glyphicon-refresh spin-glyphicon')
resultBox.removeClass('alert-success alert-danger').show()
if(data.status) {
resultBox.addClass('alert-success')
resultBox.text('$T('smpl-notesent')')
resultBox.prepend('<span class="glyphicon glyphicon-ok-sign"></span> ')
} else {
resultBox.addClass('alert-danger')
resultBox.text(data.error)
resultBox.prepend('<span class="glyphicon glyphicon-exclamation-sign"></span> ')
}
});
});
\$('#test_windows_notification').click(function () {
\$.ajax({
type: "GET",
url: "../../tapi",
data: {mode: 'test_windows', apikey: '$session', output: 'json' },
beforeSend: function () {
\$('#test_notification').attr("disabled", "disabled");
\$('#testnotice-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_notification').removeAttr("disabled");
\$('#testnotice-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testnotice-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testnotice-result').addClass("failure").html(data.error);
}
}
});
});
\$('#test_pushbullet').click(function () {
var data = { mode: 'test_pushbullet', apikey: '$session', output: 'json' };
\$("#pushbullet").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: data,
beforeSend: function () {
\$('#test_pushbullet').attr("disabled", "disabled");
\$('#testpushbullet-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_pushbullet').removeAttr("disabled");
\$('#testpushbullet-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testpushbullet-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testpushbullet-result').addClass("failure").html(data.error);
}
}
});
});
\$('#test_pushover').click(function () {
var data = { mode: 'test_pushover', apikey: '$session', output: 'json' };
\$("#pushover").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: data,
beforeSend: function () {
\$('#test_pushover').attr("disabled", "disabled");
\$('#testpushover-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_pushover').removeAttr("disabled");
\$('#testpushover-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testpushover-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testpushover-result').addClass("failure").html(data.error);
}
}
});
});
\$('#test_prowl').click(function () {
var data = { mode: 'test_prowl', apikey: '$session', output: 'json' };
\$("#prowl").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: data,
beforeSend: function () {
\$('#test_prowl').attr("disabled", "disabled");
\$('#testprowl-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_prowl').removeAttr("disabled");
\$('#testprowl-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testprowl-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testprowl-result').addClass("failure").html(data.error);
}
}
});
});
\$('#test_growl').click(function () {
var data = { mode: 'test_growl', apikey: '$session', output: 'json' };
\$("#growl").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: data,
beforeSend: function () {
\$('#test_growl').attr("disabled", "disabled");
\$('#testgrowl-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_growl').removeAttr("disabled");
\$('#testgrowl-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testgrowl-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testgrowl-result').addClass("failure").html(data.error);
}
}
});
});
\$('#test_osd').click(function () {
\$.ajax({
type: "GET",
url: "../../tapi",
data: {mode: 'test_osd', apikey: '$session', output: 'json' },
beforeSend: function () {
\$('#test_osd').attr("disabled", "disabled");
\$('#testosd-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_osdl').removeAttr("disabled");
\$('#testosd-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testosd-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testosd-result').addClass("failure").html(data.error);
}
}
});
});
})
}
\$('#test_email, #test_notif, #test_windows, #test_pushbullet, #test_pushover, #test_prowl, #test_growl, #test_osd, #test_nscript').click(function () {
testNotification(this)
})
});
</script>

View File

@@ -1,10 +1,10 @@
<!--#set global $pane="RSS"#-->
<!--#set global $help_uri="configure-rss-1-0"#-->
<!--#set global $help_uri="configuration/2.0/rss"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<!--#if not $active_feed#-->
<div class="section">
<div class="padTable">
<div class="padTable">
<a class="main-helplink" href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
<p>$T('explain-RSS')</p>
<form action="add_rss_feed" method="post" autocomplete="off" novalidate>
@@ -24,7 +24,7 @@
<input type="text" name="feed" class="smaller_input" value="$feed" />
</td>
<td>
<input type="text" name="uri" />
<input type="text" name="uri" placeholder="$T('addMultipleFeeds')" />
</td>
<td class="nowrap">
<button type="submit" class="btn btn-default Save"><span class="glyphicon glyphicon-plus"></span> $T('button-add')</button>
@@ -47,26 +47,26 @@
<!--#set $odd = not $odd#-->
<tr class="data-row <!--#if $odd then " alt " else " "#-->">
<td class="chk">
<input type="checkbox" class="toggleFeedCheckbox" name="enable" value="1" <!--#if int($rss[$feed_item]['enable']) !=0 then 'checked="checked"' else ""#--> rel="$feed_item" />
<input type="checkbox" class="toggleFeedCheckbox" name="enable" value="1" <!--#if int($rss[$feed_item]['enable']) !=0 then 'checked="checked"' else ""#--> rel="$feed_item" />
</td>
<td class="title">
<a href="?feed=$rss[$feed_item]['link']" class="subscription-title path feed <!--#if int($rss[$feed_item]['enable']) != 0 then 'feed_enabled' else 'feed_disabled'#-->">
<div class="favicon" style="background-image: url(https://www.google.com/s2/favicons?domain=$rss[$feed_item]['baselink']&amp;alt=feed);"></div> $feed_item
$feed_item
</a>
</td>
<td class="controls">
<button type="button" class="btn btn-default testFeed" rel="$feed_item"><span class="glyphicon glyphicon-sort"></span> $T('button-preFeed')</button>
<input type="hidden" name="uri" value="$rss[$feed_item]['uri']" />
<input type="hidden" name="uri" value="$rss[$feed_item]['uris']" />
<button type="button" class="btn btn-default editFeed" rel="$feed_item"><span class="glyphicon glyphicon-pencil"></span> $T('Edit')</button>
<button type="button" class="btn btn-default delFeed" rel="$feed_item"><span class="glyphicon glyphicon-trash"></span></button>
</td>
</tr>
<tr class="feed-row <!--#if $odd then " alt " else " "#-->">
<td></td>
<td colspan="2">
<div>$rss[$feed_item]['uri']</div>
</td>
</tr>
<!--#for $uri_index, $uri in enumerate($rss[$feed_item]['uri'])#-->
<tr class="feed-row <!--#if $odd then " alt " else " "#-->">
<td><div class="favicon" style="background-image: url(//$rss[$feed_item]['baselink'][$uri_index]/favicon.ico);" data-domain="$rss[$feed_item]['baselink'][$uri_index]"></div></td>
<td colspan="2"><div>$uri</div></td>
</tr>
<!--#end for#-->
<!--#end for#-->
</tbody>
</table>
@@ -89,9 +89,9 @@
<div class="field-pair">
<label class="config narrow" for="rss_rate">$T('opt-rss_rate')</label>
<input type="number" name="rss_rate" id="rss_rate" value="$rss_rate" min="15" max="1440" />
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-ok"></span> $T('button-save')</button>
<span class="config narrow">&nbsp;&nbsp;$T('Next scan at:')&nbsp;$rss_next</span>
<span class="desc narrow">$T('explain-rss_rate')</span>
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-ok"></span> $T('button-save')</button>
<span class="config narrow">&nbsp;&nbsp;$T('Next scan at:')&nbsp;$rss_next</span>
<span class="desc narrow">$T('explain-rss_rate')</span>
</div>
</fieldset>
</div>
@@ -105,7 +105,7 @@
<div class="padTable">
<a class="main-helplink" href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
<h2 class="nomargin activeRSS">
<a href="${root}config/rss/">$T('cmenu-rss')</a> &raquo;
<a href="${root}config/rss/">$T('cmenu-rss')</a> &raquo;
<a href="$rss[$active_feed]['uri']" onclick="window.open(this.href); return false;">$active_feed</a>
</h2>
<!--#if $error#-->
@@ -117,7 +117,7 @@
<form action="upd_rss_feed" method="post">
<input type="hidden" name="session" value="$session" />
<input type="hidden" name="feed" value="$feed" />
<input type="hidden" name="uri" value="$rss[$feed]['uri']" />
<input type="hidden" name="uri" value="$rss[$feed]['uris']" />
<table class="catTable">
<thead>
<tr>
@@ -140,10 +140,10 @@
<tbody>
<tr class="default">
<td>
<input type="checkbox" disabled="disabled" class="hidden" />
<input type="checkbox" disabled="disabled" class="hidden" />
</td>
<td>
<input type="text" disabled="disabled" size="1" class="hidden" />
<input type="text" disabled="disabled" size="1" class="hidden" />
</td>
<td>
<select name="filter_type" disabled="disabled" class="hidden">
@@ -154,15 +154,16 @@
<option value=">"> $T('rss-atleast')</option>
<option value="<"> $T('rss-atmost')</option>
<option value="F"> $T('rss-from')</option>
<option value="F"> $T('rss-from-show')</option>
</select>
</td>
<td>
<input type="text" disabled="disabled" value="" class="hidden" />
<input type="text" disabled="disabled" value="" class="hidden" />
</td>
<!--#if $rss[$feed]['pick_cat']#-->
<td>
<select name="cat">
<!--#for $ct in $cat_list#-->
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct==$rss[$feed]['cat'] then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
<!--#end for#-->
</select>
@@ -190,7 +191,7 @@
<!--#if $rss[$feed]['pick_script']#-->
<td>
<select name="script">
<!--#for $sc in $script_list#-->
<!--#for $sc in $scripts#-->
<option value="$sc" <!--#if $sc==$rss[$feed]['script'] then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</select>
@@ -212,10 +213,10 @@
<tbody>
<tr>
<td>
<input type="checkbox" name="enabled" value="1" checked="checked" />
<input type="checkbox" name="enabled" value="1" checked="checked" />
</td>
<td>
<input type="text" name="new_index" size="1" />
<input type="text" name="new_index" size="1" />
</td>
<td>
<select name="filter_type">
@@ -225,16 +226,17 @@
<option value="C"> $T('rss-mustcat')</option>
<option value=">"> $T('rss-atleast')</option>
<option value="<"> $T('rss-atmost')</option>
<option value="S"> $T('rss-from-show')</option>
<option value="F"> $T('rss-from')</option>
</select>
</td>
<td>
<input type="text" name="filter_text" value="" />
<input type="text" name="filter_text" value="" />
</td>
<!--#if $rss[$feed]['pick_cat']#-->
<td>
<select name="cat">
<!--#for $ct in $cat_list#-->
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct=='Default' then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
<!--#end for#-->
</select>
@@ -262,7 +264,7 @@
<!--#if $rss[$feed]['pick_script']#-->
<td>
<select name="script">
<!--#for $sc in $script_list#-->
<!--#for $sc in $scripts#-->
<option value="$sc" <!--#if $sc=='Default' then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</select>
@@ -285,12 +287,12 @@
<input type="hidden" name="feed" value="$feed" />
<table class="catTable">
<tbody>
<tr class="<!--#if $odd then " alt " else " "#-->">
<tr class="<!--#if $odd then " alt " else " "#--> <!--#if $filter[3]!="A" then 'disabled_options_rule' else ""#-->">
<td>
<input type="checkbox" name="enabled" value="1" <!--#if $filter[6]=='1' then 'checked="checked"' else ""#--> />
<input type="checkbox" name="enabled" value="1" <!--#if $filter[6]=='1' then 'checked="checked"' else ""#--> />
</td>
<td>
<input type="text" name="new_index" value="$fnum" size="1" />
<input type="text" name="new_index" value="$fnum" size="1" />
</td>
<td>
<select name="filter_type">
@@ -300,16 +302,17 @@
<option value="C" <!--#if $filter[3]=="C" then 'selected="selected"' else ""#-->> $T('rss-mustcat')</option>
<option value=">" <!--#if $filter[3]==">" then 'selected="selected"' else ""#-->> $T('rss-atleast')</option>
<option value="<" <!--#if $filter[3]=="<" then 'selected="selected"' else ""#-->> $T('rss-atmost')</option>
<option value="S" <!--#if $filter[3]=="S" then 'selected="selected"' else ""#-->> $T('rss-from-show')</option>
<option value="F" <!--#if $filter[3]=="F" then 'selected="selected"' else ""#-->> $T('rss-from')</option>
</select>
</td>
<td>
<input type="text" name="filter_text" value="$filter[4]" />
<input type="text" name="filter_text" value="$filter[4]" />
</td>
<!--#if $rss[$feed]['pick_cat']#-->
<td>
<select name="cat">
<!--#for $ct in $cat_list#-->
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct==$filter[0] then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
<!--#end for#-->
</select>
@@ -337,7 +340,7 @@
<!--#if $rss[$feed]['pick_script']#-->
<td>
<select name="script">
<!--#for $sc in $script_list#-->
<!--#for $sc in $scripts#-->
<option value="$sc" <!--#if $sc==$filter[2] then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</select>
@@ -362,7 +365,10 @@
<div class="padding">
<button type="button" class="btn btn-default testFeed" rel="$feed"><span class="glyphicon glyphicon-sort"></span> $T('button-preFeed')</button>
<button type="submit" class="btn btn-default Save"><span class="glyphicon glyphicon-forward"></span> $T('button-forceFeed')</button>
<button type="button" class="btn btn-default cleanFeed"><span class="glyphicon glyphicon-trash"></span> $T('button-clear') $T('link-download')</button>
<button type="button" class="btn btn-default cleanFeed"><span class="glyphicon glyphicon-trash"></span> $T('button-clear') $T('rss-done')</button>
<!--#if $evalButton#-->
<button type="button" class="btn btn-default evalFeed" rel="$feed"><span class="glyphicon glyphicon-ok-circle"></span> $T('button-evalFeed')</button>
<!--#end if#-->
</div>
</form>
</div>
@@ -381,11 +387,12 @@
<table class="catTable">
<thead>
<tr>
<th>$T('link-download')</th>
<th>$T('rss-skip')</th>
<th class="no-sort">$T('link-download')</th>
<th>$T('rss-filter')</th>
<th>$T('size')</th>
<th>$T('sort-title')</th>
<th width="65%">$T('sort-title')</th>
<th>$T('category')</th>
<th class="default-sort">$T('nzo-age')</th>
</tr>
</thead>
<!--#for $job in $matched#-->
@@ -394,15 +401,16 @@
<form action="download" method="get">
<input type="hidden" value="$feed" name="feed" />
<input type="hidden" name="session" value="$session" />
<input type="hidden" name="url" value="$job[0]" />
<input type="hidden" name="nzbname" value="$job[4]" />
<input type="hidden" name="url" value="$job['url']" />
<input type="hidden" name="nzbname" value="$job['nzbname']" />
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-plus-sign"></span> $T('link-download')</button>
</form>
</td>
<td class="align-center">$job[2]</td>
<td class="align-center"> <span>$job[3]</span> </td>
<td>$job[5]</td>
<td>$job[1]</td>
<td>$job['rule'] $job['skip']</td>
<td data-sort-value="$job['size']">$job['size_units']</td>
<td>$job['title']</td>
<td>$job['cat']</td>
<td data-sort-value="$job['age_ms']">$job['age']</td>
</tr>
<!--#end for#-->
</table>
@@ -415,11 +423,12 @@
<table class="catTable">
<thead>
<tr>
<th>$T('link-download')</th>
<th>$T('rss-skip')</th>
<th class="no-sort">$T('link-download')</th>
<th>$T('rss-filter')</th>
<th>$T('size')</th>
<th>$T('sort-title')</th>
<th width="65%">$T('sort-title')</th>
<th>$T('category')</th>
<th class="default-sort">$T('nzo-age')</th>
</tr>
</thead>
<!--#for $job in $unmatched#-->
@@ -428,15 +437,16 @@
<form action="download" method="get">
<input type="hidden" value="$feed" name="feed" />
<input type="hidden" name="session" value="$session" />
<input type="hidden" name="url" value="$job[0]" />
<input type="hidden" name="nzbname" value="$job[4]" />
<input type="hidden" name="url" value="$job['url']" />
<input type="hidden" name="nzbname" value="$job['nzbname']" />
<button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-plus-sign"></span> $T('link-download')</button>
</form>
</td>
<td class="align-center">$job[2]</td>
<td class="align-center"> <span>$job[3]</span> </td>
<td>$job[5]</td>
<td>$job[1]</td>
<td>$job['rule'] $job['skip']</td>
<td data-sort-value="$job['size']">$job['size_units']</td>
<td>$job['title']</td>
<td>$job['cat']</td>
<td data-sort-value="$job['age_ms']">$job['age']</td>
</tr>
<!--#end for#-->
</table>
@@ -452,12 +462,24 @@
<table class="catTable">
<thead>
<tr>
<th>$T('sort-title')</th>
<th class="default-sort">$T('rss-added')</th>
<th>$T('size')</th>
<th width="60%">$T('sort-title')</th>
<th>$T('category')</th>
<th>$T('source')</th>
</tr>
</thead>
<!--#for $job in $downloaded#-->
<tr class="infoTableSeperator">
<td>$job</td>
<td data-sort-value="$job['time_downloaded_ms']">$job['time_downloaded']</td>
<td data-sort-value="$job['size']">$job['size_units']</td>
<td>$job['title']</td>
<td>$job['cat']</td>
<td data-sort-value="$job['baselink']" title="$job['baselink']">
<!--#if $job['baselink']#-->
<div class="favicon source-icon" style="background-image: url(//$job['baselink']/favicon.ico);" data-domain="$job['baselink']"></div>
<!--#end if#-->
</td>
</tr>
<!--#end for#-->
</table>
@@ -468,16 +490,28 @@
</div>
<!--#end if#-->
</div>
</div>
<!-- /colmask -->
<script type="text/javascript" src="${root}staticcfg/js/jquery.tablesort.min.js"></script>
<script type="text/javascript">
function urlencode(str) {
return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A').replace(/%20/g, '+');
return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A').replace(/%20/g, '+');
}
\$(document).ready(function(){
\$('.favicon').each(function(i, theContainer) {
// Easy favicon grabber
var favUrl = '//' + \$(theContainer).data('domain') + '/favicon.ico'
// Does the image exist? Otherwise place a glyphicon
var testFavImg = new Image();
testFavImg.src = favUrl;
testFavImg.onerror = function (evt){
\$(theContainer).append('<span class="glyphicon glyphicon-list"></span>')
}
})
\$('.tabs a').click(function (e) {
e.preventDefault()
\$(this).tab('show')
@@ -485,7 +519,7 @@ function urlencode(str) {
\$('.editFeed').click(function(){
var oldURI = \$(this).prev().val();
var newURI = prompt("$T('feed') URL", oldURI );
var newURI = prompt("$T('feed') URL. \n$T('addMultipleFeeds').", oldURI );
if(newURI != "" && newURI !== null) {
var whichFeed = \$(this).attr("rel");
var isEnabled = \$('.toggleFeedCheckbox[rel="'+whichFeed+'"]').attr('checked') == "checked"? 1 : 0;
@@ -512,26 +546,12 @@ function urlencode(str) {
}).done(function( msg ) {
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
formHasChanged = false;
location.reload();
});
}
});
\$('.testFeed').click(function(){
var whichFeed = \$(this).attr("rel");
\$.ajax({
type: "POST",
url: "test_rss_feed",
data: {feed: whichFeed, session: "$session" }
}).done(function( msg ) {
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
location = '?feed=' + urlencode(whichFeed);
});
});
\$('.toggleFeedCheckbox').click(function(){
var whichFeed = \$(this).attr("rel");
\$.ajax({
@@ -541,20 +561,82 @@ function urlencode(str) {
}).done(function() {
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
formHasChanged = false;
location.reload();
});
});
// Only the Accept filter needs all the options
\$('form[action="upd_rss_filter"]').find('select[name="filter_type"]').change(function() {
\$(this).parent().parent().find('select:not([name="filter_type"])').attr('disabled', \$(this).val() != "A")
})
// Trigger on-load for all
\$('.disabled_options_rule').find('td select:not([name="filter_type"])').attr('disabled', true)
function setActiveIcon(objButton) {
// Let's make it look like things are happening!
\$(objButton).attr('disabled', true)
\$(objButton).find('span').remove()
\$(objButton).prepend('<span class="glyphicon glyphicon-transfer"></span>')
}
// Enable sorting and set default
if (\$('#rss-tab-matched table').length) {
\$('#rss-tab-matched table').tablesort().data('tablesort').sort(\$('#rss-tab-matched th.default-sort'), 'desc');
}
if (\$('#rss-tab-not-matched table').length) {
\$('#rss-tab-not-matched table').tablesort().data('tablesort').sort(\$('#rss-tab-not-matched th.default-sort'), 'desc');
}
if (\$('#rss-tab-done table').length) {
\$('#rss-tab-done table').tablesort().data('tablesort').sort(\$('#rss-tab-done th.default-sort'), 'desc');
}
\$('.testFeed').click(function(){
setActiveIcon(this)
var whichFeed = \$(this).attr("rel");
\$.ajax({
type: "POST",
url: "test_rss_feed",
data: {feed: whichFeed, session: "$session" }
}).done(function( msg ) {
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
location = '?feed=' + urlencode(whichFeed);
});
});
\$('.cleanFeed').click(function(){
setActiveIcon(this)
var theForm = \$(this).closest("form");
theForm.attr("action", "clean_rss_jobs").submit();
});
\$('.evalFeed').click(function(){
setActiveIcon(this)
var theForm = \$(this).closest("form");
theForm.attr("action", "eval_rss_feed").submit();
});
\$('.delFilter').click(function(){
var theForm = \$(this).closest("form");
theForm.attr("action", "del_rss_filter").submit();
});
\$('form[action="download"]').ajaxForm({
datatype: 'json',
beforeSubmit: function (_, form) {
\$(form).find('button').attr("disabled", "disabled")
// Remove icon and add new one
\$(form).find('button span').remove()
\$(form).find('button').prepend('<span class="glyphicon glyphicon-transfer"></span>')
},
success: function (_, _, _, form) {
// Set success
\$(form).find('button').html('<span class="glyphicon glyphicon-ok"></span> $T('rss-added')')
}
});
});
</script>

View File

@@ -1,5 +1,5 @@
<!--#set global $pane="Scheduling"#-->
<!--#set global $help_uri="configure-scheduling-1-0"#-->
<!--#set global $help_uri="configuration/2.0/scheduling"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<%
@@ -55,8 +55,8 @@ else:
</optgroup>
<optgroup label="$T('cmenu-servers')">
<!--#for $server in $actions_servers.keys()#-->
<option value="$server" data-action="1"data-noarg="1">$T('sch-enable_server') "$actions_servers[$server]"</option>
<option value="$server" data-action="0"data-noarg="1">$T('sch-disable_server') "$actions_servers[$server]"</option>
<option value="$server" data-action="1" data-noarg="1">$T('sch-enable_server') "$actions_servers[$server]"</option>
<option value="$server" data-action="0" data-noarg="1">$T('sch-disable_server') "$actions_servers[$server]"</option>
<!--#end for#-->
</optgroup>
</select>
@@ -72,7 +72,7 @@ else:
</div><!-- /col1 -->
</form>
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('currentSchedules')</h3>
@@ -80,21 +80,20 @@ else:
<div class="col1">
<fieldset>
<!--#if $schedlines#-->
<!--#set $schednum = 0#-->
<!--#set $odd = True#-->
<!--#for $line in $schedlines#-->
<!--#for $schednum, $line in enumerate($schedlines)#-->
<!--#set $odd = not $odd#-->
<form action="delSchedule" method="post">
<input type="hidden" name="session" value="$session"/>
<input type="hidden" name="line" id="line" value="$line"/>
<div class="field-pair infoTableSeperator <!--#if $odd then "" else " alt"#-->">
<input type="checkbox" name="schedenabled" value="$line" <!--#if int($taskinfo[$schednum][5]) > 0 then 'checked="checked"' else ""#-->>
<button class="btn btn-default float-left"><span class="glyphicon glyphicon-trash"></span></button>
<div class="scheduleEntry">
<span class="time">$taskinfo[$schednum][1]:$taskinfo[$schednum][2]</span><span class="frequency">$taskinfo[$schednum][3]</span> <span class="darkred">$taskinfo[$schednum][4]</span>
</div>
</div>
</form>
<!--#set $schednum = $schednum+1#-->
<!--#end for#-->
<!--#else#-->
<div class="field-pair">
@@ -109,12 +108,35 @@ else:
\$('#action').on('change', function() {
// Set the action
\$('#arguments').val((\$(this).find('option:selected').data('action')))
// Arguments
// Is it speedlimit?
if(\$(this).find('option:selected').val() == 'speedlimit') {
\$('#hidden_arguments').show()
\$('#hidden_arguments input').attr('placeholder', 'Bytes/s, "1M" = 1 MB/s, "500K" = 500 KB/s')
} else {
\$('#hidden_arguments').hide()
\$('#hidden_arguments input').attr('placeholder', '')
}
/* Arguments - since we only have speedlimit with arguments, disabled for now
if(\$(this).find('option:selected').data('noarg')) {
\$('#hidden_arguments').hide()
} else {
\$('#hidden_arguments').show()
}
}*/
})
\$('[name="schedenabled"]').click(function() {
\$.ajax({
type: "POST",
url: "toggleSchedule",
data: {line: \$(this).val(), session: "$session" }
}).done(function() {
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
location.reload();
});
});
</script>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

View File

@@ -1,9 +1,9 @@
<!--#set global $pane="Servers"#-->
<!--#set global $help_uri="configure-servers-1-0"#-->
<!--#set global $help_uri="configuration/2.0/servers"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<form action="addServer" method="post" autocomplete="off" novalidate>
<form action="addServer" method="post" autocomplete="off" onsubmit="removeObfuscation();" novalidate>
<input type="hidden" name="session" value="$session" />
<div id="addServer">
<div class="padding alt">
@@ -27,72 +27,58 @@
</div>
<div class="field-pair">
<label class="config" for="port">$T('srv-port')</label>
<input type="number" name="port" id="port" size="8" />
<input type="number" name="port" id="port" size="8" value="119" />
</div>
<div class="field-pair">
<label class="config" for="username">$T('srv-username')</label>
<input type="text" name="username" id="username" />
<label class="config" for="ssl">$T('srv-ssl')</label>
<input type="checkbox" name="ssl" id="ssl" value="1" />
<span class="desc">$T('explain-ssl')</span>
</div>
<!-- Tricks to avoid browser auto-fill, fixed on-submit with javascript -->
<div class="field-pair">
<label class="config" for="${pid}_00">$T('srv-username')</label>
<input type="text" name="${pid}_00" id="${pid}_00" data-hide="username" />
</div>
<div class="field-pair">
<label class="config" for="password">$T('srv-password')</label>
<input type="text" name="password" id="password" />
<label class="config" for="${pid}_01">$T('srv-password')</label>
<input type="text" name="${pid}_01" id="${pid}_01" data-hide="password" />
</div>
<div class="field-pair">
<label class="config" for="connections">$T('srv-connections')</label>
<input type="number" name="connections" id="connections" min="0" max="100" />
<input type="number" name="connections" id="connections" min="0" max="100" value="8" />
</div>
<div class="field-pair">
<label class="config" for="priority">$T('srv-priority')</label>
<input type="number" name="priority" id="priority" min="0" max="100" /> <i>$T('explain-svrprio')</i>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="retention">$T('srv-retention')</label>
<input type="number" name="retention" id="retention" min="0" /> <i>$T('days')</i>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="timeout">$T('srv-timeout')</label>
<input type="number" name="timeout" id="timeout" min="30" /> <i>$T('seconds')</i>
</div>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="ssl">$T('srv-ssl')</label>
<input type="checkbox" name="ssl" id="ssl" value="1" <!--#if int($have_ssl) == 0 then "disabled=\"disabled\"" else ""#--> />
<span class="desc">$T('srv-ssl')</span>
<div class="field-pair <!--#if int($have_ssl_context) == 0 then "disabled" else ""#--> advanced-settings">
<label class="config" for="ssl_verify">$T('opt-ssl_verify')</label>
<select name="ssl_verify" id="ssl_verify" <!--#if int($have_ssl_context) == 0 then "disabled=\"disabled\"" else ""#-->>
<option value="2" selected>$T('ssl_verify-strict')</option>
<option value="1">$T('ssl_verify-normal')</option>
<option value="0">$T('ssl_verify-disabled')</option>
</select>
<span class="desc">$T('explain-ssl_verify').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="ssl_type">$T('srv-ssl_type')</label>
<!--#if int($have_ssl) == 1#-->
<select name="ssl_type" id="ssl_type">
<option value="">$T('Default')</option>
<!--#if 't12' in $ssl_protocols#-->
<option value="t12">TLS v1.2</option>
<!--#end if#-->
<!--#if 't11' in $ssl_protocols#-->
<option value="t11">TLS v1.1</option>
<!--#end if#-->
<!--#if 't1' in $ssl_protocols#-->
<option value="t1">TLS v1</option>
<!--#end if#-->
<!--#if 'v3' in $ssl_protocols#-->
<option value="v3">SSL v3</option>
<!--#end if#-->
<!--#if 'v2' in $ssl_protocols#-->
<option value="v2">SSL v2</option>
<!--#end if#-->
</select>
<!--#end if#-->
<span class="desc">$T('srv-explain-ssl_type')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="send_group">$T('srv-send_group')</label>
<input type="checkbox" name="send_group" id="send_group" value="1" />
<span class="desc">$T('srv-explain-send_group')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="optional">$T('srv-optional')</label>
<input type="checkbox" name="optional" id="optional" value="1" />
<span class="desc">$T('srv-optional')</span>
<span class="desc">$T('explain-optional')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="categories">$T('srv-categories')</label>
<select name="categories" id="categories" multiple>
<!--#for $cat in $cats#-->
@@ -103,27 +89,31 @@
</select>
<span class="desc">$T('srv-explain-categories')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="displayname">$T('srv-displayname')</label>
<input type="text" name="displayname" id="displayname" />
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="notes">$T('srv-notes')</label>
<textarea name="notes" id="notes" rows="3" cols="50"></textarea>
</div>
<div class="field-pair">
<button class="btn btn-default"><span class="glyphicon glyphicon-plus"></span> $T('button-addServer')</button>
<button class="btn btn-default advancedButton"><span class="glyphicon glyphicon-cog"></span> $T('button-advanced')</button>
<button class="btn btn-default testServer" type="button"><span class="glyphicon glyphicon-sort"></span> $T('button-testServer')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
</form>
<!--#set $cur = 0#-->
<!--#for $server in $servers#-->
<!--#set $cur = $cur + 1#-->
<!--#set $prio_colors = ["#59cc33", "#3366cc","#7f33cc", "#cc33a6", "#cc3333"] #-->
<!--#set $cur_prio_color = -1 #-->
<!--#set $last_prio = -1 #-->
<!--#for $cur, $server in enumerate($servers) #-->
<form action="saveServer" method="post" class="fullform" autocomplete="off" novalidate>
<input type="hidden" name="session" value="$session" />
<input type="hidden" name="server" value="$server['name']" />
@@ -131,9 +121,14 @@
<div class="section <!--#if int($server['enable']) == 0 then 'server-disabled' else ""#-->">
<div class="col2 <!--#if int($server['enable']) == 0 then 'server-disabled' else ""#-->">
<h3>$server['displayname'] <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<!--#if int($server['enable']) != 0#-->
<span class="label label-primary" data-priority="$server['priority']#-->">$server['priority']</span>
<span class="label label-primary" data-priority="$server['priority']#-->">$T('srv-priority'):</span>
<!--#if int($server['enable']) != 0 #-->
<!--#if $last_prio != $server['priority'] and $cur_prio_color+1 < len($prio_colors) #-->
<!--#set $cur_prio_color = $cur_prio_color+1 #-->
<!--#set $last_prio = $server['priority'] #-->
<!--#end if#-->
<span class="label label-primary" style="background-color: $prio_colors[$cur_prio_color]">$server['priority']</span>
<span class="label label-primary" style="background-color: $prio_colors[$cur_prio_color]">$T('srv-priority'):</span>
<!--#end if#-->
<table><tr>
<td><input type="checkbox" class="toggleServerCheckbox" id="enable_$cur" name="$server['name']" value="1" <!--#if int($server['enable']) != 0 then 'checked="checked"' else ""#--> /></td>
@@ -155,12 +150,18 @@
<input type="number" name="port" id="port$cur" value="$server['port']" size="8" />
</div>
<div class="field-pair">
<label class="config" for="username$cur">$T('srv-username')</label>
<input type="text" name="username" id="username$cur" value="$server['username']" />
<label class="config" for="ssl$cur">$T('srv-ssl')</label>
<input type="checkbox" name="ssl" id="ssl$cur" value="1" <!--#if int($server['ssl']) != 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-ssl')</span>
</div>
<!-- Tricks to avoid browser auto-fill, fixed on-submit with javascript -->
<div class="field-pair">
<label class="config" for="${pid}_${cur}0">$T('srv-username')</label>
<input type="text" name="${pid}_${cur}0" id="${pid}_${cur}0" value="$server['username']" data-hide="username" />
</div>
<div class="field-pair">
<label class="config" for="password$cur">$T('srv-password')</label>
<input type="text" name="password" id="password$cur" value="$server['password']" />
<label class="config" for="${pid}_${cur}1">$T('srv-password')</label>
<input type="text" name="${pid}_${cur}1" id="${pid}_${cur}1" value="$server['password']" data-hide="password" />
</div>
<div class="field-pair">
<label class="config" for="connections$cur">$T('srv-connections')</label>
@@ -170,54 +171,35 @@
<label class="config" for="priority$cur">$T('srv-priority')</label>
<input type="number" name="priority" id="priority$cur" value="$server['priority']" min="0" max="100" /> <i>$T('explain-svrprio')</i>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="retention$cur">$T('srv-retention')</label>
<input type="number" name="retention" id="retention$cur" value="$server['retention']" min="0" /> <i>$T('days')</i>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="timeout$cur">$T('srv-timeout')</label>
<input type="number" name="timeout" id="timeout$cur" value="$server['timeout']" min="30" /> <i>$T('seconds')</i>
</div>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="ssl$cur">$T('srv-ssl')</label>
<input type="checkbox" name="ssl" id="ssl$cur" value="1" <!--#if int($server['ssl']) != 0 and int($have_ssl) == 1 then 'checked="checked"' else ""#--> <!--#if int($have_ssl) == 0 then "disabled=\"disabled\"" else ""#--> />
<span class="desc">$T('srv-ssl')</span>
<div class="field-pair <!--#if int($have_ssl_context) == 0 then "disabled" else ""#--> advanced-settings">
<label class="config" for="ssl_verify$cur">$T('opt-ssl_verify')</label>
<select name="ssl_verify" id="ssl_verify$cur" <!--#if int($have_ssl_context) == 0 then "disabled=\"disabled\"" else ""#-->>
<option value="2" <!--#if $server['ssl_verify'] == 2 then 'selected="selected"' else ""#--> >$T('ssl_verify-strict')</option>
<option value="1" <!--#if $server['ssl_verify'] == 1 then 'selected="selected"' else ""#--> >$T('ssl_verify-normal')</option>
<option value="0" <!--#if $server['ssl_verify'] == 0 then 'selected="selected"' else ""#--> >$T('ssl_verify-disabled')</option>
</select>
<span class="desc">$T('explain-ssl_verify').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="ssl_type$cur">$T('srv-ssl_type')</label>
<!--#if int($have_ssl) == 1#-->
<select name="ssl_type" id="ssl_type$cur">
<option value="" <!--#if $server['ssl_type'] == "" then 'selected="selected"' else ""#--> >$T('Default')</option>
<!--#if 't12' in $ssl_protocols#-->
<option value="t12" <!--#if $server['ssl_type'] == "t12" then 'selected="selected"' else ""#--> >TLS v1.2</option>
<!--#end if#-->
<!--#if 't11' in $ssl_protocols#-->
<option value="t11" <!--#if $server['ssl_type'] == "t11" then 'selected="selected"' else ""#--> >TLS v1.1</option>
<!--#end if#-->
<!--#if 't1' in $ssl_protocols#-->
<option value="t1" <!--#if $server['ssl_type'] == "t1" then 'selected="selected"' else ""#--> >TLS v1</option>
<!--#end if#-->
<!--#if 'v3' in $ssl_protocols#-->
<option value="v3" <!--#if $server['ssl_type'] == "v3" then 'selected="selected"' else ""#--> >SSL v3</option>
<!--#end if#-->
<!--#if 'v2' in $ssl_protocols#-->
<option value="v2" <!--#if $server['ssl_type'] == "v2" then 'selected="selected"' else ""#--> >SSL v2</option>
<!--#end if#-->
</select>
<!--#end if#-->
<span class="desc">$T('srv-explain-ssl_type')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="optional$cur">$T('srv-optional')</label>
<input type="checkbox" name="optional" id="optional$cur" value="1" <!--#if int($server['optional']) != 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('srv-optional')</span>
<span class="desc">$T('explain-optional')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="send_group$cur">$T('srv-send_group')</label>
<input type="checkbox" name="send_group" id="send_group$cur" value="1" <!--#if int($server['send_group']) != 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('srv-explain-send_group')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="categories$cur">$T('srv-categories')</label>
<select name="categories" id="categories$cur" multiple>
<!--#for $cat in $cats#-->
@@ -227,20 +209,28 @@
<!--#end for#-->
</select>
<span class="desc">$T('srv-explain-categories')</span>
<div class="alert alert-info alert-no-category">
<span class="glyphicon glyphicon-info-sign"></span>
$T('srv-explain-no-categories')
</div>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="displayname$cur">$T('srv-displayname')</label>
<input type="text" name="displayname" id="displayname$cur" value="$server['displayname']" />
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="notes$cur">$T('srv-notes')</label>
<textarea name="notes" id="notes$cur" rows="3" cols="50">$server['notes']</textarea>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default advancedButton"><span class="glyphicon glyphicon-cog"></span> $T('button-advanced')</button>
<button class="btn btn-default testServer" type="button"><span class="glyphicon glyphicon-sort"></span> $T('button-testServer')</button>
<button class="btn btn-default delServer"><span class="glyphicon glyphicon-trash"></span> $T('button-delServer')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
<div class="col2" style="display:block;">
@@ -267,25 +257,34 @@
this.submit()
})
})
/**
Color the priority labels
/**
Message on no Default category selected
**/
// They are already sorted
var colorList = ["#cc3333", "#cc33a6", "#7f33cc", "#3366cc", "#59cc33"];
var lastColor = colorList[0];
var lastPrio = '-1';
var theColor = '';
\$('[data-priority]').each(function() {
// New one?
if(\$(this).data('priority') != lastPrio) {
// Update
theColor = colorList.pop() || lastColor;
lastPrio = \$(this).data('priority');
}
\$(this).css('background-color', theColor)
})
function checkServerCats() {
// Now we check all of them
var hasDefault = false;
// Only check the active servers, not the add-server one
\$('.section:not(#addServerContent) select[name="categories"]').each(function() {
// See if this server is enabled
if(!\$(this).parents('.section').find('.col2').hasClass('server-disabled') ) {
// Is there Default?
if(\$(this).val() && \$(this).val().indexOf('Default') > -1) {
// Hide
\$('.alert-no-category').hide()
hasDefault = true
// All good!
return true
}
}
})
// We found nothing.. Let's show a warning
if(!hasDefault) \$('.alert-no-category').show()
}
\$('select[name="categories"]').on('change', checkServerCats)
checkServerCats()
/**
Click events
**/
@@ -301,42 +300,83 @@
\$(this).attr("value", "$T('showDetails')");
}
});
\$('#addServerButton').click(function(){
\$('#addServer').hide();
\$('#addServerContent').show();
});
\$('[name="ssl"]').click(function() {
// Use CSS transitions to do some highlighting
var portBox = \$(this).parent().parent().find('[name="port"]')
if(this.checked) {
// Enabled SSL change port when not already a custom port
if(portBox.val() == '119') {
portBox.val('563')
portBox.addClass('port-highlight')
}
} else {
// Remove SSL port
if(portBox.val() == '563') {
portBox.val('119')
portBox.addClass('port-highlight')
}
}
setTimeout(function() { portBox.removeClass('port-highlight') }, 2000)
})
\$('.testServer').click(function(event){
\$(this).attr("disabled", "disabled")
removeObfuscation()
var theButton = \$(this)
var resultBox = theButton.parents('.col1').find('.result-box .alert');
theButton.attr("disabled", "disabled")
theButton.find('span').toggleClass('glyphicon-sort glyphicon-refresh spin-glyphicon')
\$.ajax({
type: "POST",
url: "../../tapi",
data: "mode=config&amp;name=test_server&amp;" + \$(this).parents('form:first').serialize(),
success: function(msg){
alert(msg);
\$(event.target).removeAttr("disabled")
data: "mode=config&output=json&name=test_server&" + \$(this).parents('form:first').serialize()
}).then(function(data) {
// Let's replace the link
msg = data.value.message.replace('https://sabnzbd.org/certificate-errors', '<a href="https://sabnzbd.org/certificate-errors" class="alert-link" target="_blank">https://sabnzbd.org/certificate-errors</a>')
// Fill the box and enable the button
resultBox.removeClass('alert-success alert-danger').show()
resultBox.html(msg)
theButton.removeAttr("disabled")
theButton.find('span').toggleClass('glyphicon-sort glyphicon-refresh spin-glyphicon')
// Succes or not?
if(data.value.result) {
resultBox.addClass('alert-success')
resultBox.prepend('<span class="glyphicon glyphicon-ok-sign"></span> ')
} else {
resultBox.addClass('alert-danger')
resultBox.prepend('<span class="glyphicon glyphicon-exclamation-sign"></span> ')
}
});
});
\$('.delServer').click(function(){
if( confirm("$T('Plush-confirm')") ) {
\$(this).parents('form:first').attr('action','delServer').submit();
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
setTimeout(function() { location.reload(); }, 100)
setTimeout(function() { location.reload(); }, 500)
}
return false;
});
\$('.clrServer').click(function(){
if( confirm("$T('Plush-confirm')") ) {
\$(this).parents('form:first').attr('action','clrServer').submit();
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
setTimeout(function() { location.reload(); }, 100)
setTimeout(function() { location.reload(); }, 500)
}
return false;
});
\$('.toggleServerCheckbox').click(function(){
var whichServer = \$(this).attr("name");
\$.ajax({

View File

@@ -1,5 +1,5 @@
<!--#set global $pane="Sorting"#-->
<!--#set global $help_uri="configure-sorting-1-0"#-->
<!--#set global $help_uri="configuration/2.0/sorting"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
@@ -9,10 +9,10 @@
<div class="section">
<div class="col2">
<h3>$T('seriesSorting') <a href="$helpuri$help_uri#toc0" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p>
<p>
<b>$T('affectedCat')</b><br/>
<select name="tv_cat" multiple="multiple" class="multiple_cats">
<!--#for $ct in $cat_list#-->
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct in $tv_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
@@ -22,40 +22,38 @@
<div class="col1">
<fieldset>
<div class="field-pair">
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
</div>
<div class="field-pair">
<label class="config wide" for="enable_tv_sorting">$T('opt-tvsort')</label>
<input type="checkbox" name="enable_tv_sorting" id="enable_tv_sorting" value="1" <!--#if int($enable_tv_sorting)> 0 then 'checked="checked"' else ""#--> />
<input type="checkbox" name="enable_tv_sorting" id="enable_tv_sorting" value="1" <!--#if int($enable_tv_sorting)> 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config" for="tvfoldername">$T('sortString')</label>
<input type="text" id="tvfoldername" name="tv_sort_string" value="$tv_sort_string" />
<button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
<input type="text" id="tvfoldername" name="tv_sort_string" value="$tv_sort_string" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('presetSort')</label>
<div class="presets float-left">
<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" class="btn btn-default" onclick="tvSet('%sn/Season %s/%sn - %sx%0e - %en.%ext')" value="$T('button-Season1x05')" />
<input type="button" class="btn btn-default" onclick="tvSet('%sn/Season %s/%sn - S%0sE%0e - %en.%ext')" value="$T('button-SeasonS01E05')" />
<br/>
<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')" />
<input type="button" class="btn btn-default" onclick="tvSet('%sn/%sx%0e - %en/%sn - %sx%0e - %en.%ext')" value="$T('button-Ep1x05')" />
<input type="button" class="btn btn-default" onclick="tvSet('%sn/S%0sE%0e - %en/%sn - S%0sE%0e - %en.%ext')" value="$T('button-EpS01E05')" />
</div>
</div>
<div id="previewtv" class="example">
<div class="field-pair">
<label class="config" for="tvsamplename">Test Data</label>
<input type="text" id="tvsamplename" name="tvsamplename" placeholder="$T('show-name') S01E05 - $T('ep-name') [DTS]" />
<button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
<input type="text" id="tvsamplename" name="tvsamplename" placeholder="$T('show-name') S01E05 - $T('ep-name') [DTS]" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('sortResult')</label> <span class="desc path" id="previewtv-result">&nbsp;</span>
<label class="config">$T('sortResult')</label> <span class="desc path" id="previewtv-result">&nbsp;</span>
</div>
</div>
<div class="field-pair">
<label class="config">$T('sort-legenda')</label>
<input type="button" value="$T('sort-legenda')" onclick="jQuery(this).hide(); jQuery('#Key1').show();" />
<button type="button" class="btn btn-default patternKey" onclick="jQuery(this).hide(); jQuery('#Key1').show();"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> $T('sort-legenda')</button>
<table id="Key1" class="Key">
<thead>
<tr>
@@ -164,10 +162,10 @@
<div class="section">
<div class="col2">
<h3>$T('movieSort') <a href="$helpuri$help_uri#toc6" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p>
<p>
<b>$T('affectedCat')</b><br/>
<select name="movie_cat" multiple="multiple" class="multiple_cats">
<!--#for $ct in $cat_list#-->
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct in $movie_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
@@ -177,7 +175,7 @@
<div class="col1">
<fieldset>
<div class="field-pair">
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
</div>
<div class="field-pair">
<label class="config wide" for="enable_movie_sorting">$T('opt-movieSort')</label>
@@ -185,31 +183,28 @@
</div>
<div class="field-pair">
<label class="config wide" for="movie_extra_folder">$T('opt-movieExtra')</label>
<input type="checkbox" name="movie_extra_folder" id="movie_extra_folder" value="1" <!--#if int($movie_extra_folder)> 0 then 'checked="checked"' else ""#--> />
<input type="checkbox" name="movie_extra_folder" id="movie_extra_folder" value="1" <!--#if int($movie_extra_folder)> 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config" for="moviefoldername">$T('sortString')</label>
<input type="text" name="movie_sort_string" id="moviefoldername" value="$movie_sort_string" />
<button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
<input type="text" name="movie_sort_string" id="moviefoldername" value="$movie_sort_string" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config" for="movieextra">$T('multiPartLabel')</label>
<input type="text" name="movie_sort_extra" id="movieextra" value="$movie_sort_extra" />
<button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
<input type="text" name="movie_sort_extra" id="movieextra" value="$movie_sort_extra" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('presetSort')</label>
<div class="presets float-left">
<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')" />
<input type="button" onclick="movieSet('%0decade/%title (%y).%ext',' CD%1');movieExtraFolder(true)" value="Decades 1" />
<input type="button" class="btn btn-default" onclick="movieSet('%title (%y)/%title (%y).%ext',' CD%1');movieExtraFolder(false)" value="$T('button-inFolders')" />
<input type="button" class="btn btn-default" onclick="movieSet('%title (%y).%ext',' CD%1');movieExtraFolder(true)" value="$T('button-noFolders')" />
<input type="button" class="btn btn-default" onclick="movieSet('%0decade/%title (%y).%ext',' CD%1');movieExtraFolder(true)" value="Decades 1" />
</div>
</div>
<div id="previewmovie" class="example">
<div class="field-pair">
<label class="config" for="moviesamplename">Test Data</label>
<input type="text" id="moviesamplename" name="moviesamplename" placeholder="$T('movie-sp-name') (2009)" />
<button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
<input type="text" id="moviesamplename" name="moviesamplename" placeholder="$T('movie-sp-name') (2009)" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('sortResult')</label> <span class="desc path" id="previewmovie-result">&nbsp;</span>
@@ -217,7 +212,7 @@
</div>
<div class="field-pair">
<label class="config">$T('sort-legenda')</label>
<input type="button" value="$T('sort-legenda')" onclick="jQuery(this).hide(); jQuery('#Key2').show();" />
<button type="button" class="btn btn-default patternKey" onclick="jQuery(this).hide(); jQuery('#Key2').show();"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> $T('sort-legenda')</button>
<table id="Key2" class="Key">
<thead>
<tr>
@@ -308,7 +303,7 @@
<p>
<b>$T('affectedCat')</b><br/>
<select name="date_cat" multiple="multiple" class="multiple_cats">
<!--#for $ct in $cat_list#-->
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct in $date_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
@@ -318,38 +313,36 @@
<div class="col1">
<fieldset>
<div class="field-pair">
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
</div>
<div class="field-pair">
<label class="config wide" for="enable_date_sorting">$T('opt-dateSort')</label>
<input type="checkbox" name="enable_date_sorting" id="enable_date_sorting" value="1" <!--#if int($enable_date_sorting)> 0 then 'checked="checked"' else ""#--> />
<input type="checkbox" name="enable_date_sorting" id="enable_date_sorting" value="1" <!--#if int($enable_date_sorting)> 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config" for="datefoldername">$T('sortString')</label>
<input type="text" name="date_sort_string" id="datefoldername" value="$date_sort_string" />
<button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
<input type="text" name="date_sort_string" id="datefoldername" value="$date_sort_string" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('presetSort')</label>
<div class="presets float-left">
<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')" />
<input type="button" class="btn btn-default" onclick="dateSet('%t/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-ShowNameF')" />
<input type="button" class="btn btn-default" onclick="dateSet('%y-%0m/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-YMF')" />
<input type="button" class="btn btn-default" onclick="dateSet('%y-%0m-%0d/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-DailyF')" />
</div>
</div>
<div id="previewdate" class="example">
<div class="field-pair">
<label class="config" for="datesamplename">Test Data</label>
<input type="text" id="datesamplename" name="datesamplename" placeholder="$T('show-name') 2009-01-02" />
<button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
<input type="text" id="datesamplename" name="datesamplename" placeholder="$T('show-name') 2009-01-02" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('sortResult')</label> <span class="desc path" id="previewdate-result">&nbsp;</span>
<label class="config">$T('sortResult')</label> <span class="desc path" id="previewdate-result">&nbsp;</span>
</div>
</div>
<div class="field-pair">
<label class="config">$T('sort-legenda')</label>
<input type="button" value="$T('sort-legenda')" onclick="jQuery(this).hide(); jQuery('#Key3').show();" />
<button type="button" class="btn btn-default patternKey" onclick="jQuery(this).hide(); jQuery('#Key3').show();"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> $T('sort-legenda')</button>
<table id="Key3" class="Key">
<thead>
<tr>
@@ -526,12 +519,12 @@
else
\$('#previewdate').hide();
}
\$(document).ready(function(){
new_previewtv();
new_previewmovie();
new_previewdate();
\$('#tvfoldername, #tvsamplename').bind("keyup focus", new_previewtv);
\$('#moviefoldername, #movieextra, #moviesamplename').bind("keyup focus", new_previewmovie);
\$('#datefoldername, #datesamplename').bind("keyup focus", new_previewdate);

View File

@@ -1,5 +1,5 @@
<!--#set global $pane="Special"#-->
<!--#set global $help_uri="configure-special-1-0"#-->
<!--#set global $help_uri="configuration/2.0/special"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
@@ -17,7 +17,7 @@
<!--#for $option in $switches#-->
<div class="field-pair">
<label class="config wide" for="$option[0]">
$option[0] ( <span class="path"><!--#if $option[2] then $T('on') else $T('off')#--></span> )
$option[0] ( <span class="path"><!--#if $option[2] then $T('on') else $T('off')#--></span> )
<!--#if $option[1] != $option[2] then '<span class="glyphicon glyphicon-asterisk"></span>' else ''#-->
</label>
<input type="checkbox" name="$option[0]" id="$option[0]" value="1" <!--#if int($option[1]) > 0 then 'checked="checked"' else ""#--> />

View File

@@ -1,47 +1,31 @@
<!--#set global $pane="Switches"#-->
<!--#set global $help_uri="configure-switches-1-0"#-->
<!--#set global $help_uri="configuration/2.0/switches"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<form action="saveSwitches" method="post" name="fullform" class="fullform" autocomplete="off">
<input type="hidden" id="session" name="session" value="$session" />
<div class="section">
<div class="col2">
<h3>$T('swtag-general') <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<label class="config" for="auto_browser">$T('opt-auto_browser')</label>
<input type="checkbox" name="auto_browser" id="auto_browser" value="1" <!--#if int($auto_browser) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-auto_browser')</span>
</div>
<div class="field-pair">
<label class="config" for="check_new_rel">$T('opt-check_new_rel')</label>
<select name="check_new_rel" id="check_new_rel">
<option value="0" <!--#if $check_new_rel == 0 then 'selected="selected"' else ""#--> >$T('off')</option>
<option value="1" <!--#if $check_new_rel == 1 then 'selected="selected"' else ""#--> >$T('on')</option>
<option value="2" <!--#if $check_new_rel == 2 then 'selected="selected"' else ""#--> >$T('also-test')</option>
</select>
<span class="desc">$T('explain-check_new_rel')</span>
</div>
<div class="field-pair <!--#if not $have_ampm then "disabled" else "" #-->">
<label class="config" for="ampm">$T('opt-ampm')</label>
<input type="checkbox" name="ampm" id="ampm" value="1" <!--#if int($ampm) > 0 then 'checked="checked"' else ""#--> <!--#if not $have_ampm then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-ampm')</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-server') <a href="$helpuri$help_uri#toc1" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<label class="config" for="load_balancing">$T('opt-load_balancing')</label>
<select name="load_balancing" id="load_balancing">
<option value="0" <!--#if $load_balancing == 0 then 'selected="selected"' else ""#--> >$T('no-load-balancing')</option>
<option value="1" <!--#if $load_balancing == 1 then 'selected="selected"' else ""#--> >$T('load-balancing')</option>
<option value="2" <!--#if $load_balancing == 2 then 'selected="selected"' else ""#--> >$T('load-balancing-happy-eyeballs')</option>
</select>
<span class="desc">$T('explain-load_balancing')</span>
</div>
<div class="field-pair">
<label class="config" for="ssl_ciphers">$T('opt-ssl_ciphers')</label>
<input type="text" name="ssl_ciphers" id="ssl_ciphers" value="$ssl_ciphers" />
<span class="desc">$T('explain-ssl_ciphers') <br>$T('readwiki')
<a href="${helpuri}advanced/ssl-ciphers" target="_blank">${helpuri}advanced/ssl-ciphers</a></span>
</div>
<div class="field-pair">
<label class="config" for="max_art_tries">$T('opt-max_art_tries')</label>
<input type="number" name="max_art_tries" id="max_art_tries" value="$max_art_tries" min="2" max="2000" />
@@ -52,15 +36,7 @@
<input type="checkbox" name="max_art_opt" id="max_art_opt" value="1" <!--#if int($max_art_opt) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-max_art_opt')</span>
</div>
<div class="field-pair">
<label class="config" for="load_balancing">$T('opt-load_balancing')</label>
<select name="load_balancing" id="load_balancing">
<option value="0" <!--#if $load_balancing == 0 then 'selected="selected"' else ""#--> >$T('no-load-balancing')</option>
<option value="1" <!--#if $load_balancing == 1 then 'selected="selected"' else ""#--> >$T('load-balancing')</option>
<option value="2" <!--#if $load_balancing == 2 then 'selected="selected"' else ""#--> >$T('load-balancing-happy-eyeballs')</option>
</select>
<span class="desc">$T('explain-load_balancing')</span>
</div>
<div class="field-pair">
<label class="config" for="auto_disconnect">$T('opt-auto_disconnect')</label>
<input type="checkbox" name="auto_disconnect" id="auto_disconnect" value="1" <!--#if int($auto_disconnect) > 0 then 'checked="checked"' else ""#--> />
@@ -68,6 +44,7 @@
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
</div>
</fieldset>
</div><!-- /col1 -->
@@ -81,7 +58,7 @@
<div class="field-pair">
<label class="config" for="pre_script">$T('opt-pre_script')</label>
<select name="pre_script" id="pre_script">
<!--#for $sc in $script_list#-->
<!--#for $sc in $scripts#-->
<!--#if $sc.lower() == $pre_script.lower()#-->
<option value="$sc" selected="selected">$Tspec($sc)</option>
<!--#else#-->
@@ -91,6 +68,11 @@
</select>
<span class="desc">$T('explain-pre_script')</span>
</div>
<div class="field-pair">
<label class="config" for="propagation_delay">$T('opt-propagation_delay')</label>
<input type="number" name="propagation_delay" id="propagation_delay" value="$propagation_delay" /> <i>$T('minutes')</i>
<span class="desc">$T('explain-propagation_delay')</span>
</div>
<div class="field-pair">
<label class="config" for="top_only">$T('opt-top_only')</label>
<input type="checkbox" name="top_only" id="top_only" value="1" <!--#if int($top_only) > 0 then 'checked="checked"' else ""#--> />
@@ -102,16 +84,17 @@
<span class="desc">$T('explain-pre_check')</span>
</div>
<div class="field-pair">
<label class="config" for="fail_hopeless">$T('opt-fail_hopeless')</label>
<input type="checkbox" name="fail_hopeless" id="fail_hopeless" value="1" <!--#if int($fail_hopeless) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-fail_hopeless')</span>
<label class="config" for="fail_hopeless_jobs">$T('opt-fail_hopeless_jobs')</label>
<input type="checkbox" name="fail_hopeless_jobs" id="fail_hopeless_jobs" value="1" <!--#if int($fail_hopeless_jobs) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-fail_hopeless_jobs')</span>
</div>
<div class="field-pair">
<label class="config" for="no_dupes">$T('opt-no_dupes')</label>
<select name="no_dupes" id="no_dupes">
<option value="0" <!--#if int($no_dupes) == 0 then 'selected="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="1" <!--#if int($no_dupes) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-ignore')</option>
<option value="2" <!--#if int($no_dupes) == 2 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="3" <!--#if int($no_dupes) == 3 then 'selected="selected"' else ""#--> >$T('nodupes-fail')</option>
<option value="1" <!--#if int($no_dupes) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-ignore')</option>
</select>
<span class="desc">$T('explain-no_dupes')</span>
</div>
@@ -119,8 +102,9 @@
<label class="config" for="no_series_dupes">$T('opt-no_series_dupes')</label>
<select name="no_series_dupes" id="no_series_dupes">
<option value="0" <!--#if int($no_series_dupes) == 0 then 'selected="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="1" <!--#if int($no_series_dupes) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-ignore')</option>
<option value="2" <!--#if int($no_series_dupes) == 2 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="3" <!--#if int($no_series_dupes) == 3 then 'selected="selected"' else ""#--> >$T('nodupes-fail')</option>
<option value="1" <!--#if int($no_series_dupes) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-ignore')</option>
</select>
<span class="desc">$T('explain-no_series_dupes')</span>
</div>
@@ -147,7 +131,6 @@
<input type="text" name="unwanted_extensions" id="unwanted_extensions" value="$unwanted_extensions"/>
<span class="desc">$T('explain-unwanted_extensions')</span>
</div>
<div class="field-pair">
<label class="config" for="auto_sort">$T('opt-auto_sort')</label>
<input type="checkbox" name="auto_sort" id="auto_sort" value="1" <!--#if int($auto_sort) > 0 then 'checked="checked"' else ""#--> />
@@ -155,6 +138,7 @@
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
</div>
</fieldset>
</div><!-- /col1 -->
@@ -173,18 +157,15 @@
<div class="field-pair">
<label class="config" for="enable_all_par">$T('opt-enable_all_par')</label>
<input type="checkbox" name="enable_all_par" id="enable_all_par" value="1" <!--#if int($enable_all_par) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_all_par')</span>
<span class="desc">$T('explain-enable_all_par').replace('. ', '.<br/>')</span>
</div>
<!--#if $have_multicore#-->
<div class="field-pair">
<label class="config" for="quick_check">$T('opt-quick_check')</label>
<input type="checkbox" name="quick_check" id="quick_check" value="1" <!--#if int($quick_check) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-quick_check')</span>
</div>
<div class="field-pair <!--#if not $have_multicore then "disabled" else "" #-->">
<label class="config" for="par2_multicore">$T('opt-par2_multicore')</label>
<input type="checkbox" name="par2_multicore" id="par2_multicore" value="1" <!--#if int($par2_multicore) > 0 then 'checked="checked"' else ""#--> <!--#if not $have_multicore then 'readonly="readonly" disabled="disabled"' else "" #--> />
<input type="checkbox" name="par2_multicore" id="par2_multicore" value="1" <!--#if int($par2_multicore) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-par2_multicore')</span>
</div>
<!--#end if#-->
<div class="field-pair">
<label class="config" for="par_option">$T('opt-par_option')</label>
<input type="text" name="par_option" id="par_option" value="$par_option" />
@@ -200,21 +181,6 @@
<input type="checkbox" name="safe_postproc" id="safe_postproc" value="1" <!--#if int($safe_postproc) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-safe_postproc')</span>
</div>
<div class="field-pair <!--#if not $have_unrar then "disabled" else "" #-->">
<label class="config" for="enable_unrar">$T('opt-enable_unrar')</label>
<input type="checkbox" name="enable_unrar" id="enable_unrar" value="1" <!--#if not $have_unrar then 'readonly="readonly" disabled="disabled"' else "" #--> <!--#if int($enable_unrar) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_unrar')</span>
</div>
<div class="field-pair <!--#if not $have_unzip then "disabled" else "" #-->">
<label class="config" for="enable_unzip">$T('opt-enable_unzip')</label>
<input type="checkbox" name="enable_unzip" id="enable_unzip" value="1" <!--#if not $have_unzip then 'readonly="readonly" disabled="disabled"' else "" #--> <!--#if int($enable_unzip) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_unzip')</span>
</div>
<div class="field-pair <!--#if not $have_7zip then "disabled" else "" #-->">
<label class="config" for="enable_7zip">$T('opt-enable_7zip')</label>
<input type="checkbox" name="enable_7zip" id="enable_7zip" value="1" <!--#if not $have_7zip then 'readonly="readonly" disabled="disabled"' else "" #--> <!--#if int($enable_7zip) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_7zip')</span>
</div>
<div class="field-pair">
<label class="config" for="enable_recursive">$T('opt-enable_recursive')</label>
<input type="checkbox" name="enable_recursive" id="enable_recursive" value="1" <!--#if int($enable_recursive) > 0 then 'checked="checked"' else ""#--> />
@@ -225,27 +191,6 @@
<input type="checkbox" name="flat_unpack" id="flat_unpack" value="1" <!--#if int($flat_unpack) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-flat_unpack')</span>
</div>
<div class="field-pair">
<label class="config" for="overwrite_files">$T('opt-overwrite_files')</label>
<input type="checkbox" name="overwrite_files" id="overwrite_files" value="1" <!--#if int($overwrite_files) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-overwrite_files')</span>
</div>
<div class="field-pair">
<label class="config" for="enable_filejoin">$T('opt-enable_filejoin')</label>
<input type="checkbox" name="enable_filejoin" id="enable_filejoin" value="1" <!--#if int($enable_filejoin) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_filejoin')</span>
</div>
<div class="field-pair">
<label class="config" for="enable_tsjoin">$T('opt-enable_tsjoin')</label>
<input type="checkbox" name="enable_tsjoin" id="enable_tsjoin" value="1" <!--#if int($enable_tsjoin) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-ts_join')</span>
</div>
<div class="field-pair">
<label class="config" for="unpack_check">$T('opt-unpack_check')</label>
<input type="checkbox" name="unpack_check" id="unpack_check" value="1" <!--#if int($unpack_check) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-unpack_check')</span>
</div>
<div class="field-pair">
<label class="config" for="script_can_fail">$T('opt-script_can_fail')</label>
<input type="checkbox" name="script_can_fail" id="script_can_fail" value="1" <!--#if int($script_can_fail) > 0 then 'checked="checked"' else ""#--> />
@@ -280,6 +225,7 @@
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
</div>
</fieldset>
</div><!-- /col1 -->
@@ -310,18 +256,16 @@
<input type="checkbox" name="replace_illegal" id="replace_illegal" value="1" <!--#if int($replace_illegal) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-replace_illegal')</span>
</div>
<!--#if not $nt#-->
<div class="field-pair">
<label class="config" for="sanitize_safe">$T('opt-sanitize_safe')</label>
<input type="checkbox" name="sanitize_safe" id="sanitize_safe" value="1" <!--#if int($sanitize_safe) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-sanitize_safe')</span>
</div>
<div class="field-pair">
<label class="config" for="enable_meta">$T('opt-enable_meta')</label>
<input type="checkbox" name="enable_meta" id="enable_meta" value="1" <!--#if int($enable_meta) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_meta')</span>
</div>
<!--#end if#-->
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
</div>
</fieldset>
</div><!-- /col1 -->
@@ -372,18 +316,13 @@
<div class="field-pair">
<label class="config" for="rating_enable">$T('opt-rating_enable')</label>
<input type="checkbox" name="rating_enable" id="rating_enable" value="1" <!--#if int($rating_enable) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-rating_enable')</span>
<span class="desc">$T('explain-rating_enable').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair">
<label class="config" for="rating_api_key">$T('opt-rating_api_key')</label>
<input type="text" name="rating_api_key" id="rating_api_key" value="$rating_api_key" />
<span class="desc">$T('explain-rating_api_key')</span>
</div>
<div class="field-pair">
<label class="config" for="rating_feedback">$T('opt-rating_feedback')</label>
<input type="checkbox" name="rating_feedback" id="rating_feedback" value="1" <!--#if int($rating_feedback) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-rating_feedback')</span>
</div>
<div class="field-pair">
<label class="config" for="rating_filter_enable">$T('opt-rating_filter_enable')</label>
<input type="checkbox" name="rating_filter_enable" id="rating_filter_enable" value="1" <!--#if int($rating_filter_enable) > 0 then 'checked="checked"' else ""#--> />
@@ -395,7 +334,7 @@
<p>
<label for="rating_filter_abort_video">$T('opt-rating_filter_video')</label>
<select name="rating_filter_abort_video" id="rating_filter_abort_video">
<option value="0" <!--#if $rating_filter_abort_video == 0 then 'selected="selected"' else ""#--> >$T('notUsed')</option>
<option value="0" <!--#if $rating_filter_abort_video == 0 then 'selected="selected"' else ""#--> >$T('notUsed')</option>
<!--#for $val in $range(1, 10)#--><option value="$val" <!--#if $rating_filter_abort_video == $val then 'selected="selected"' else ""#--> >$val $T('orLess')</option><!--#end for#-->
</select>
</p>
@@ -415,7 +354,7 @@
<input type="checkbox" value="1" id="rating_filter_abort_encrypted_confirm" name="rating_filter_abort_encrypted_confirm" <!--#if int($rating_filter_abort_encrypted_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_encrypted_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
</p>
<p>
<span>
<input type="checkbox" value="1" id="rating_filter_abort_spam" name="rating_filter_abort_spam" <!--#if int($rating_filter_abort_spam) > 0 then 'checked="checked"' else ""#--> />
@@ -425,7 +364,7 @@
<input type="checkbox" value="1" id="rating_filter_abort_spam_confirm" name="rating_filter_abort_spam_confirm" <!--#if int($rating_filter_abort_spam_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_spam_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
</p>
<p>
<input type="checkbox" value="1" id="rating_filter_abort_downvoted" name="rating_filter_abort_downvoted" <!--#if int($rating_filter_abort_downvoted) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_downvoted">$T('opt-rating_filter_downvoted')</label>
@@ -463,7 +402,7 @@
<input type="checkbox" value="1" id="rating_filter_pause_encrypted_confirm" name="rating_filter_pause_encrypted_confirm" <!--#if int($rating_filter_pause_encrypted_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_encrypted_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
</p>
<p>
<span>
<input type="checkbox" value="1" id="rating_filter_pause_spam" name="rating_filter_pause_spam" <!--#if int($rating_filter_pause_spam) > 0 then 'checked="checked"' else ""#--> />
@@ -473,7 +412,7 @@
<input type="checkbox" value="1" id="rating_filter_pause_spam_confirm" name="rating_filter_pause_spam_confirm" <!--#if int($rating_filter_pause_spam_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_spam_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
</p>
<p>
<input type="checkbox" value="1" id="rating_filter_pause_downvoted" name="rating_filter_pause_downvoted" <!--#if int($rating_filter_pause_downvoted) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_downvoted">$T('opt-rating_filter_downvoted')</label>
@@ -497,19 +436,43 @@
<script type="text/javascript">
\$(document).ready(function() {
if (!\$('#rating_filter_enable').is(":checked")) {
\$("#rating_filter_abort").hide();
\$("#rating_filter_pause").hide();
\$("#rating_filter_abort").hide();
\$("#rating_filter_pause").hide();
}
\$('#rating_filter_enable').change(function () {
if (\$(this).is(":checked")) {
\$("#rating_filter_abort").show();
\$("#rating_filter_pause").show();
}
\$("#rating_filter_abort").show();
\$("#rating_filter_pause").show();
}
else {
\$("#rating_filter_abort").hide();
\$("#rating_filter_abort").hide();
\$("#rating_filter_pause").hide();
}
});
\$('.restoreDefaults').click(function(e) {
// Get section name
var sectionName = \$(this).parents('.section').find('.col2 h3').text().trim()
// Confirm?
if(!confirm("$T('explain-restoreDefaults') \""+sectionName+"\"\n$T('confirm')")) return false
e.preventDefault()
// Need to get all the input values, so same way as saving normally
var key_container = {}
\$(this).parents('.section').extractFormDataTo(key_container);
key_container = Object.keys(key_container)
// Send request
\$.ajax({
type: "GET",
url: "../../tapi",
data: "mode=set_config_default&session=${session}&output=json&keyword=" + key_container.join('&keyword=')
}).then(function(data) {
// Reload page
document.location = document.location
})
})
});
</script>

View File

@@ -0,0 +1,62 @@
<html lang="$active_lang">
<head>
<title>SABnzbd</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="apple-mobile-web-app-title" content="SABnzbd" />
<link rel="apple-touch-icon" sizes="76x76" href="../staticcfg/ico/apple-touch-icon-76x76-precomposed.png" />
<link rel="apple-touch-icon" sizes="120x120" href="../staticcfg/ico/apple-touch-icon-120x120-precomposed.png" />
<link rel="apple-touch-icon" sizes="152x152" href="../staticcfg/ico/apple-touch-icon-152x152-precomposed.png" />
<link rel="apple-touch-icon" sizes="180x180" href="../staticcfg/ico/apple-touch-icon-180x180-precomposed.png" />
<link rel="apple-touch-icon" sizes="192x192" href="../staticcfg/ico/android-192x192.png" />
<link rel="shortcut icon" href="../staticcfg/ico/favicon.ico?v=$version" />
<link rel="stylesheet" type="text/css" href="../staticcfg/bootstrap/css/bootstrap.min.css?v=$version" />
<link rel="stylesheet" type="text/css" href="../staticcfg/css/login.css?v=$version" />
<script type="text/javascript" src="../staticcfg/js/jquery-3.1.1.min.js?v=$version"></script>
<script type="text/javascript" src="../staticcfg/bootstrap/js/bootstrap.min.js?v=$version"></script>
</head>
<html>
<body>
<div class="account-wall">
<div class="text-center logo-header">
<!--#include $webdir + "/staticcfg/images/logo-full.svg"#-->
<a href="https://sabnzbd.org/wiki/faq#why-login" target="_blank">
<span class="glyphicon glyphicon-question-sign"></span>
</a>
</div>
<form class="form-signin" action="./" method="post">
<!--#if $error#-->
<div class="alert alert-danger" role="alert">$error</div>
<!--#end if#-->
<input type="text" class="form-control" name="username" placeholder="$T('srv-username')" required autofocus>
<input type="password" class="form-control" name="password" placeholder="$T('srv-password')" required>
<button class="btn btn-default"><span class="glyphicon glyphicon-circle-arrow-right"></span> $T('login') </button>
<div class="checkbox text-center" data-toggle="tooltip" data-placement="bottom" title="$T('explain-sessionExpire')">
<label>
<input type="checkbox" name="remember_me" value="1"> $T('rememberme')
</label>
</div>
</form>
</div>
<script type="text/javascript">
// Tooltip
\$('[data-toggle="tooltip"]').tooltip()
// Try-catch in case somebody disabled localstorage
try {
// Set what was done previously
\$('input[type="checkbox"]').prop('checked', localStorage.getItem("remember_me") === 'true')
// Store if we change something
\$('input[type="checkbox"]').on('change', function() {
localStorage.setItem("remember_me", \$(this).is(':checked'));
})
} catch(err) { }
</script>
</body>
</html>

View File

@@ -1,3 +1,3 @@
/* This file was intentionally left blank and is only needed for 'skin' detection routine */
/* https://github.com/thezoggy/sabnzbd-uni_Config */
/* Updated Nov 2015 by Safihre for 0.8.x */
/* Updated Nov 2015 by Safihre for 1.0.x */

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,74 @@
body {
overflow-y: scroll;
background-color: #E4E4E4;
}
* {
border-radius: 0 !important;
}
.btn, .btn:hover, .btn:active, .btn:focus {
box-shadow: 1px 1px 1px rgba(0,0,0,.1) !important;
background-color: white !important;
}
.logo-header {
padding-bottom: 10px;
}
.logo-header svg {
height: 55px;
width: 198px;
}
.account-wall {
width: 300px;
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -30%);
}
.text-center a {
position: absolute;
right: 13px;
top: 12px;
color: #636363;
font-size: 1.2em
}
.text-center a:hover {
color: black !important;
}
.form-signin .alert {
margin: 5px 0px;
}
.form-signin input {
margin-top: 5px
}
.form-signin .btn {
width: 100%;
margin-top: 10px;
font-weight: bold;
}
.btn .glyphicon {
margin-left: 3px;
float: right;
top: 1px;
font-size: 1.2em;
}
input[type="checkbox"] {
width: 16px;
height: 16px;
margin-top: 2px;
}
label {
color: #636363;
}

View File

@@ -83,9 +83,15 @@ body {
.example {
background-color: #fefeee;
}
.presets {
margin-bottom: -6px;
}
.presets input {
margin: 2px 0;
}
.presets input[type="button"] {
margin: 0px 2px 6px 0px;
}
label.config,
.field-pair h5 {
overflow: auto;
@@ -100,7 +106,7 @@ label.config,
}
label.config-hover {
cursor: pointer;
}
label.narrow {
width: 200px!important;
@@ -111,6 +117,7 @@ label.wide,
}
.field-pair h5 {
margin-bottom: 5px;
line-height: normal;
}
.desc {
display: block;
@@ -124,7 +131,7 @@ label.wide,
}
input[type="checkbox"]+.desc {
padding-top: 6px;
}
}
.desc-check {
font-size: 12px;
font-style: italic;
@@ -167,8 +174,8 @@ input[type="checkbox"]+.desc {
padding-left: 12px;
font-size: 13px;
}
.typeahead.dropdown-menu>.active>a,
.typeahead.dropdown-menu>.active>a:focus,
.typeahead.dropdown-menu>.active>a,
.typeahead.dropdown-menu>.active>a:focus,
.typeahead.dropdown-menu>.active>a:hover {
color: black;
background-color: #E5E5E5;
@@ -222,8 +229,14 @@ textarea:hover, input[type="date"]:hover, input[type="datetime"]:hover, input[ty
.padding {
padding: 6px;
}
.disabled {
color: #aaa;
.disabled,
.disabled input,
.disabled .clearBtn,
.disabled .patternKey,
.disabled .show_qrcode,
.disabled .generate_key,
.disabled .generate_cert {
color: #aaa !important;
}
.padTable p {
margin-top: 0;
@@ -259,6 +272,9 @@ textarea:hover, input[type="date"]:hover, input[type="datetime"]:hover, input[ty
overflow: auto;
clear: both;
}
.label {
font-style: normal;
}
.padTable h3 {
margin-top: 0;
}
@@ -271,6 +287,27 @@ tr.separator {
.catTable th {
text-align: center;
}
.catTable .glyphicon-option-vertical {
color: #808080;
display: block;
margin-right: -5px;
cursor: move;
}
.Categories form:not(.sorting-row) .glyphicon-option-vertical {
visibility: hidden;
}
.Categories form.sorting-row:nth-child(even) tr {
background-color: #F8F8F8;
}
.Categories form:first-of-type tr:last-of-type {
background-color: #FFFFE0;
}
.Categories hr {
margin: 5px;
}
.RSS .rss-section input[type="text"] {
max-width: 180px;
@@ -287,6 +324,37 @@ tr.separator {
.RSS form[action="add_rss_feed"] tr:nth-child(even) {
border: 1px solid #E5E5E5;
}
.RSS .tab-pane table th:not(.no-sort) {
cursor: pointer;
}
.RSS .tab-pane table th:not(.sorted) {
padding-right: 12px;
}
.RSS .tab-pane table th.sorted {
padding-right: 0px;
}
.RSS .tab-pane table th.sorted.ascending:after {
content:'';
display: inline-block;
width: 0;
height: 0;
margin-left: 4px;
vertical-align: middle;
border-bottom: 4px dashed;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
.RSS .tab-pane table th.sorted.descending:after {
content:'';
display: inline-block;
width: 0;
height: 0;
margin-left: 4px;
vertical-align: middle;
border-top: 4px dashed;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
.Config .table {
margin: 0;
border: 1px solid #ddd
@@ -398,15 +466,18 @@ tr.separator {
background-color: #3C3C3C;
color: white;
border: 0;
padding: 10px 12px;
padding:11px 12px 8px;
}
.modal-header .close {
opacity: 0.7;
opacity: 0.5;
background: transparent;
color: white;
text-shadow: none !important;
font-weight: normal !important;
font-size: 28px;
font-size: 32px;
vertical-align: middle;
margin-top: -5px;
font-family: arial, sans-serif !important;
}
.modal-header .close:hover {
opacity: 1;
@@ -417,14 +488,20 @@ tr.separator {
opacity: 0 !important;
transition: all 0.3s !important;
}
#modal_qr .modal-body canvas {
padding: 10px;
margin-bottom: 10px;
}
.modal.in .modal-dialog {
transform: scale(1) !important;
-webkit-transform: scale(1) !important;
opacity: 1 !important;
}
.fileBrowser {
margin-left: -1px!important;
.btn-default.fileBrowser,
.btn-default.clearBtn {
margin-left: -1px !important;
box-shadow: 1px 0px 1px rgba(0,0,0,.1) !important;
}
#filebrowser_modal .list-group-item {
font-weight: bold;
@@ -474,12 +551,17 @@ h2.activeRSS {
}
.favicon {
background-position: center center!important;
background-size: 16px 16px;
opacity: 1;
top: -1px;
height: 16px;
width: 16px;
float: left;
margin-right: 2px;
margin: 0 6px 0 2px;
text-align: center;
}
.source-icon span {
top: -3px;
}
.feed {
text-decoration: none;
@@ -510,6 +592,9 @@ h2.activeRSS {
padding-right: .3em;
width: auto;
}
#subscriptions .favicon {
margin-left: 8px;
}
.ie6 .subscription-title {
width: 20em;
}
@@ -541,12 +626,18 @@ h2.activeRSS {
.tab-content .catTable {
width: 100%;
}
.tab-content .catTable th {
text-align: left;
}
.tab-content .catTable tbody {
border: 1px solid #ddd;
}
.tab-content .catTable td {
line-height: 2em;
}
.tab-content .catTable td:nth-child(4) {
word-break: break-all;
}
.tab-content .catTable tr:hover td {
background-color: #F2F2F2;
}
@@ -597,12 +688,7 @@ ul.tabs li.active a {
color: #000;
cursor: default;
}
.ui-tooltip-qrcode {
max-width: 320px;
}
.ui-tooltip-qrcode img {
margin: 0 auto;
}
.checkbox-days {
float: left;
}
@@ -663,7 +749,7 @@ ul.tabs li.active a {
transition: none;
}
input[type=checkbox],
input[type=checkbox],
input[type=radio] {
margin-top: 2px;
}
@@ -707,10 +793,10 @@ select {
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
display:inline-block;
vertical-align:middle;
max-width: 100%;
max-width: 100%;
min-height: 34px;
font-size: 13px;
background-color: white;
background-color: white;
}
input[type="text"]:not([size]),
@@ -775,46 +861,91 @@ input[type="checkbox"] {
max-width: 150px;
}
.Categories input {
.Categories #content input {
max-width: 150px !important;
}
.Scheduling input[type="checkbox"] {
.Scheduling form[action="addSchedule"] input[type="checkbox"] {
margin-top: 0px;
margin-left: -20px;
}
.Scheduling form[action="delSchedule"] input[type="checkbox"] {
position: initial;
float: left;
margin: 9px 10px 0px 5px;
}
.navbar .container {
padding-right: 0;
}
.navbar .nav>li>a {
height: 50px;
}
.navbar-default {
background-color: #FFF;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
margin-bottom: 10px;
}
.navbar-fixed-top {
z-index: 1000;
}
.navbar-default .navbar-brand {
padding: 0 !important;
margin: 0 !important;
padding-right: 5px !important;
.navbar-logo {
display: inline-block;
padding-right: 10px;
padding-left: 10px;
padding-top: 5px;
margin-left: 5px;
margin-bottom: -1px;
}
.navbar-default .navbar-brand-small {
padding-right: 10px !important;
#search-menu .glyphicon {
top: 3px;
}
.navbar-default .navbar-brand-mobile {
#search-dropdown {
right: 0;
left: auto;
width: 300px;
padding: 5px;
}
#search-dropdown .dropdown-header {
font-weight: bold;
color: black;
}
#search-dropdown a {
overflow: hidden;
text-overflow: ellipsis;
}
.navbar-logo-small svg {
height: 40px;
width: 40px;
}
/*
.navbar-logo-wide {
display: none;
}
.navbar-logo-wide {
padding-top: 9px;
}
.navbar-logo-wide svg {
height: 36px;
width: 130px;
}
*/
.navbar-default .navbar-nav>li>a {
color: black !important;
}
.navbar-default .navbar-nav>li>a:hover {
.navbar-default .navbar-nav>li>a:hover,
.navbar-logo:hover {
background-color: #e2e2e2 !important;
}
@@ -838,8 +969,8 @@ input[type="checkbox"] {
width: 1em;
height: 1em;
}
.rss-symbol {
stroke: none;
.rss-symbol {
stroke: none;
fill: white;
}
@@ -862,6 +993,14 @@ input[type="checkbox"] {
opacity: 0.7;
}
.advanced-settings {
display: none;
}
.Servers .port-highlight {
animation: highlight-fade 1s ease-in 1;
}
.Servers .col2 label,
.Email .col2 label {
margin: 0;
@@ -874,6 +1013,17 @@ input[type="checkbox"] {
margin-bottom: 8px;
}
.result-box .alert {
margin-bottom: 0px;
display: none;
}
.alert-no-category,
.alert-translate {
display: none;
margin: 5px 0px 0px;
}
.server-disabled {
background-color: #eee;
}
@@ -920,6 +1070,12 @@ input[type="checkbox"] {
.main-restarting small {
font-size: 3rem !important;
}
.value-and-select input {
margin-right: -5px;
}
.value-and-select select {
min-width: 30px;
}
.dotOne, .dotTwo, .dotThree {
opacity: 0;
@@ -941,28 +1097,51 @@ input[type="checkbox"] {
100% { opacity: 1; }
}
@media screen and (min-width: 1200px) {
@keyframes highlight-fade {
0% { background: #FEFEDA; }
100% { background: none; }
}
.spin-glyphicon {
animation: spin 2s infinite linear;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); }
}
@media screen and (min-width: 1200px) {
.Categories input[name="dir"] {
max-width: 240px !important;
}
.navbar-nav {
/* For extra wide languages like Polish */
margin-right: -50px;
}
}
@media screen and (min-width: 768px) and (max-width: 1200px) {
@media screen and (min-width: 768px) and (max-width: 1200px) {
.navbar-default .navbar-nav>li>a>span,
.navbar-default .navbar-nav>li>a:hover>strong,
.navbar-default .navbar-nav>li>a.active>strong {
display: inline;
display: inline;
}
.navbar-default .navbar-nav>li>a>svg {
display: inline-block;
}
.navbar-default .navbar-nav>li>a>strong,
.navbar-default .navbar-nav:hover>li>a.active>strong {
display: none;
}
#search-menu>a>strong {
display: none;
}
.RSS #content,
.Categories #content {
width: 100%;
@@ -974,20 +1153,20 @@ input[type="checkbox"] {
float: right;
padding: 5px 0px;
}
.col1 h5 {
margin-left: 20px;
}
.col2 {
width: 100%;
padding: 10px;
}
.col2 p {
margin-left: 20px;
}
.Servers .col2 button:first-of-type {
margin-bottom: 0;
}
@@ -1005,33 +1184,41 @@ input[type="checkbox"] {
}
.navbar-collapse.in .navbar-nav>li>a>span,
.navbar-collapse.collapsing .navbar-nav>li>a>span {
display: inline-block;
width: 1.5em;
display: inline-block;
width: 1.5em;
}
.navbar-collapse.in .navbar-nav>li>a>svg,
.navbar-collapse.collapsing .navbar-nav>li>a>svg {
display: inline-block;
margin-right: 0.5em;
}
.navbar .nav>li>a {
height: auto;
}
#search-menu {
display: none;
}
.section .col2 h3 {
padding-right: 25px;
}
.section .col2 h3 a {
display: inline-block;
}
.main-restarting.in strong {
font-size: 2em;
font-weight: bold;
}
.main-restarting.in span {
top: 2px;
}
.main-restarting.in small {
font-size: 2rem !important;
}
@@ -1041,55 +1228,55 @@ input[type="checkbox"] {
padding: 0 !important;
padding-top: 55px !important;
}
#content fieldset {
min-width: 0;
padding: 0 !important;
}
.colmask {
border: none !important;
}
.col2 {
padding: 10px;
}
.col2 h3 {
margin: -10px -10px 0px;
padding-left: 10px;
}
.col2 p {
margin-left: 0px;
margin-bottom: 0;
}
.col1 h5 {
margin-left: 0px;
}
}
label.config {
margin-left: 0px;
margin-bottom: 2px;
width: 100%;
float: none;
}
.desc {
margin: 0;
margin-left: 3px;
margin-top: 2px;
padding: 0 !important;
}
.col1 input[type="checkbox"] {
position: inherit;
float: left;
margin-right: 5px;
margin-top: 0px;
}
div.field-pair{
min-width: 0;
padding: 5px 10px;
@@ -1097,11 +1284,10 @@ input[type="checkbox"] {
.fileBrowserField {
max-width: 65% !important;
}
}
ul.tabs a {
padding-left: 5px;
padding-right: 5px;
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

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