Compare commits

...
907 Commits
Author SHA1 Message Date
SABnzbd Automation f13339d64d Automatic translation update 2017-09-05 19:53:42 +00:00
Safihre fa5b44be99 Update text files for 2.3.0Alpha2 2017-09-05 16:36:12 +02:00
Safihre 08bc7c5b9d Use NZO_LOCK to limit possible dir conflicts when adding NZBs
Hopefully resolving problems with possible overlapping directory names and import errors.
2017-09-05 16:07:43 +02:00
Safihre d9b5dd549a Revert "Only allow 1 NZB to be added at the same time"
Could result in deadlock for some reason.
This reverts commit e64df8ed60.
2017-09-05 16:05:48 +02:00
Safihre 8ec53a3bce Giving up on elegant solutions to prevent stalling, just use a check
This stupid nightmare still wasn't fixed.
The proper solution creates slowdown on many systems because it's not efficient enough. Instead just do a check every 90 seconds if there's stalled stuff.
2017-09-05 16:05:19 +02:00
Safihre 0aac9a5e5c Correct display of download percentage
No need to calculate in javascript, we get it from API
2017-09-05 16:03:58 +02:00
Safihre 11a880d040 MultiPar shows 'PAR File(s) Incomplete' on verification success
When there are Par2-files with very similar filenames in the folder.
2017-09-05 13:21:48 +02:00
Safihre 67b66beb13 Only count really extra files during Multipar 2017-09-04 22:43:59 +02:00
Safihre 1da633442b Correct byte counts when retrying 2017-09-04 22:43:01 +02:00
Safihre 13de40881e Correct display of Forced items 2017-09-04 21:44:00 +02:00
Safihre 1c6419ea65 Revert "Paused status for individual download trumps Force priority"
Oops, I was wrong. This reverts commit d06c11673f.
2017-09-04 20:51:05 +02:00
Safihre a2adeffc1a Correctly count all bytes and drop 'missing' use 'mbmissing'
Reporting number of missing articles makes 0 sense, it's the MB that matters.
2017-09-04 20:39:15 +02:00
Safihre 71fa3c544a Also shjow scanning of extra files for par2cmdline/tbb 2017-09-04 14:54:41 +02:00
Safihre b739fb7f07 Do not count overhead-bytes for NZO statistics 2017-09-04 14:18:55 +02:00
Safihre 860728beae Show counter when Multipar is scanning other files in the directory 2017-09-04 13:58:50 +02:00
Safihre 1bdbf1c6a8 Show different icon when priority is Force 2017-09-04 10:59:48 +02:00
Safihre abbed4cd77 Show missing articles starting at 2% 2017-09-04 10:59:18 +02:00
Safihre d06c11673f Paused status for individual download trumps Force priority 2017-09-04 10:57:02 +02:00
Safihre 67d67f5ff6 Correct typo in reject_duplicate_files
Closes #1021
2017-09-03 17:07:41 +02:00
Safihre 2386d65b84 Extrapars could be empty if not +Repair set for job 2017-09-03 16:54:07 +02:00
SABnzbd Automation be638ecca1 Automatic translation update 2017-09-03 08:23:45 +00:00
Safihre c32bcea3f6 Re-update text files for 2.3.0 Alpha 1 2017-09-03 10:16:08 +02:00
Safihre bf0aa6569b Need to do comparison to true number of servers
Previous solution was too simple and could still fail.
2017-09-03 10:12:52 +02:00
Safihre e64df8ed60 Only allow 1 NZB to be added at the same time 2017-09-02 19:18:13 +02:00
Safihre f7c3a4381d More efficient to compare TryList sizes
Only less is more.
2017-09-02 17:45:32 +02:00
Safihre 55efb34f03 Allow duplicate filenames by default 2017-09-02 15:39:55 +02:00
Safihre ae8e9d83f1 Less CPU intensive anti-stall fix 2017-09-02 15:27:31 +02:00
SABnzbd Automation c4406df73f Automatic translation update 2017-09-02 09:23:23 +00:00
Safihre 12004802b6 Allow users to set custom basepath
Closes #904
https://forums.sabnzbd.org/viewtopic.php?f=11&t=22511
2017-09-02 11:10:13 +02:00
Safihre 6068ca6376 Disable CherryPy timeout monitor
Besides throwing errors, it doesn't really help anything. The actions still get performed. 
See also: https://github.com/cherrypy/cherrypy/issues/1625
2017-09-02 09:50:55 +02:00
Safihre 613ba49165 Only really count passwords that came from the passwords file 2017-09-01 21:21:48 +02:00
Safihre 9cd21d84ee Only reset trylist once when reset of nw
Also happens within the decode()
2017-09-01 13:36:04 +02:00
SABnzbd Automation 8d813f125e Automatic translation update 2017-08-31 14:44:35 +00:00
Safihre cb66bc28ab create_default_context only availble on Python 2.7.9+ 2017-08-31 13:14:29 +02:00
Safihre 30b13b1856 Small tweaks to startup logging 2017-08-30 23:59:27 +02:00
Safihre 67a133068c Move logging of number of certificates to Debug only
It's a bit slow, can take up to 1.5 seconds of startup time.
2017-08-30 23:48:07 +02:00
Safihre 731a3bcb22 Move Bonjour/ZeroConfig to after SABnzbd start
Slow to mount or to fail (1-2 seconds)
2017-08-30 23:25:49 +02:00
Safihre 724ec8ca9f ZeroConfig could crash due to UTF8 hostname 2017-08-30 23:21:54 +02:00
Safihre d28f775c71 Remove information about SSL/TSL Protocols because it is inccorect
There is no way to get the actually enabled SSL/TLS protcols on a system, let along from Python. It's not even possible from the `openssl` command line.
See also #994
And: https://stackoverflow.com/questions/45924030/get-available-ssl-tls-protocols-in-python-2-7
2017-08-30 23:15:19 +02:00
Safihre a834c1c7a7 Add Refresh button to Glitter when Refresh Rate > 2
Linked #842
2017-08-30 15:28:17 +02:00
Safihre 04e595e706 Update text files for 2.3.0Alpha1 (2) 2017-08-29 23:53:36 +02:00
Safihre 46c28dbf68 Do not show dropdown for URL-grabs that are paused
Setting changes are not propegated to the actual downloading, could be optimized.
2017-08-29 23:50:54 +02:00
Safihre 8594bfe817 Timing of try-list check could cause stalling
My god I hate this stalling problem, it haunts me in my dreams.
2017-08-29 23:42:27 +02:00
SABnzbd Automation e61a01512b Automatic translation update 2017-08-29 16:04:48 +00:00
Safihre 657e3bb594 Update text files for 2.3.0Alpha1 2017-08-29 16:42:34 +02:00
Safihre bbbdca6a00 Update INSTALL.txt 2017-08-29 16:01:59 +02:00
Safihre a95c705e4c Auto-disconnect if the last item was deleted from the Queue 2017-08-29 10:02:25 +02:00
Safihre 446c5ba80f Block for more reasons so that try-lists get reset 2017-08-29 09:06:38 +02:00
Safihre 7641c0e1cc Correct Retry function after par2-changes 2017-08-27 23:56:24 +02:00
Safihre 42fdd9c890 Flexible block-search
Also add blocks from setnames that are a lot a like. Especially in those cases with multi-sets where the sets have the same filenames.
2017-08-27 22:48:30 +02:00
Safihre dca63878db Force priority should be listed as 'Downloading' even if Queue paused
Closes #1012
2017-08-27 22:37:13 +02:00
Safihre dc67fc414c Do not rely on par2/Multipar parsing to detect par2 files of the parset
We know much bettter now we do full parsing of each par2 file that comes in.
2017-08-27 20:09:40 +02:00
Safihre 90be3cc5a0 Only remove par2's that really belong to the set 2017-08-27 18:49:03 +02:00
Safihre 42cdba5ce3 Improve Prospective Par2 to handle multisets better
Now we get too many blocks, but before the second set wouldn't get enough blocks
2017-08-27 18:32:13 +02:00
Safihre 9c069cfb2c Let par2/MultiPar only remove used par2-files
In case of duplicae filenames we don't want "base.1.par2" to be removed when checking "base.par2".
2017-08-27 17:41:44 +02:00
Safihre 544b420baa Correct get_files API-call to only show really queued files 2017-08-27 16:18:48 +02:00
Safihre 094c96f270 Improve obfuscated and broken par2 handeling
Linked #998.
- In case of full obfuscation we detect the files directly after assembling and rename it such that par2 will pick it up
- Turns out we do need 'partable' to keep track what is the main par2 file
- Handle missing of first par2 file, automatically fetch next one
2017-08-27 14:49:01 +02:00
Safihre aa7bad56f0 Fix get_unique_filename function 2017-08-27 14:42:08 +02:00
Safihre 977f4e1036 Rework par2-handeling
See #998
- Seperate par2 functions in new file
- Always check for par2-signature if file is not a rar-file
- Always parse par2 contents for signatures (hash -> filename)
- Use the signatures to keep track of par2-packs, not basenames
- Remove seperate 'extrapars' for NZF's, all in the NZO
- Remove 'partable' for NZO, it's all in it's 'extrapars'
2017-08-27 12:23:17 +02:00
Safihre e05a98d22b Do not show 'Apply filters' from the start
Nothing more we can do to speed up the RSS evaluation.
Closes #679
2017-08-26 18:57:47 +02:00
Safihre 452e955a1e Disable extra features of feedparser
NZB-feeds do not contain the properties that feedparser has fancy parsing for.
The reduces the time spent in feedparser by 40%. 
Linked #679
2017-08-25 22:59:40 +02:00
Safihre a724f6a979 Revert "Remove locks from ArticleCache"
This reverts commit 5e7558ce4a.
2017-08-25 21:59:11 +02:00
Safihre 715b25b52f More logging when adding NZB's 2017-08-25 21:59:07 +02:00
SABnzbd Automation 4d3f370b3a Automatic translation update 2017-08-25 16:04:37 +00:00
Safihre 07f6717728 Duplicate files in NZB could result in broken unpack after repair
Because par2 would detect them, but not use them. So ".1" files would later be used for unpack, even though it's not a real set.
2017-08-25 16:59:01 +02:00
Safihre 35b4aa6b7a Ignore unpack errors in duplicate rarsets
Multipar and par2tbb will detect and log them so we can remove them, but par2cmdline will not.
2017-08-25 16:58:56 +02:00
Safihre a7a04d912c Use fileobj to prevent having to chdir, which can crash on macOS
If something is "wrong" with the current directory, for example when SABnzbd is started after downloading in a sandbox by macOS security, this function can error and break the adding of NZB's.
Have to use a fileobj, otherwise GZip will put the whole path inside there.
2017-08-25 10:51:52 +02:00
Safihre ce558b0850 Original files would be deleted after a MultiPar rename 2017-08-25 10:51:25 +02:00
Safihre 8ab7c294ee Do not fail a job if recursive unpack fails
The user can handle it, we did our part.
2017-08-25 09:19:29 +02:00
Safihre 306228462e Use existing texts for wizard ad translations
Yeah, my fault. I deleted them in a clean-up of the skintext during 1.0.0 development. Very stupid.
2017-08-25 09:19:15 +02:00
SABnzbd Automation 9e7a8468e2 Automatic translation update 2017-08-24 15:24:59 +00:00
SABnzbd Automation 4c2445485a Automatic translation update 2017-08-23 23:13:38 +00:00
Safihre 103c46e2b4 Translation fix for unpack warning 2017-08-23 22:54:40 +02:00
Safihre b4922d69a2 Pause between unpacks on Windows, otherwise subprocess_fix overloads
Strange but true, but on jobs with many small files to unpack, it would just fail.
2017-08-23 22:50:40 +02:00
Safihre 110a06a3cd Handle '482 Download limt exceeded'
Closes #1009
2017-08-23 22:50:27 +02:00
Safihre 6f0f67110f Only auto-disconnect after first run of verification 2017-08-23 22:48:53 +02:00
Safihre 848721da84 Clean last functions from the real anti-stalling fix 2017-08-23 22:10:06 +02:00
Safihre 127d7ab40c The real anti-stalling fix
Woohoo!
For each NZF (file) make sure all articles have tried a server before marking it as tried. Before if articles were still in transit they could be marked as tried on NZF level before the server could get to them,
2017-08-23 15:43:54 +02:00
Safihre 4fb7246082 TryList reset at NZO level also nessecary
Timing issue between when a new server is selected and when a job is added to the NZO-level try-list. Locks were tried, but failed.
2017-08-23 09:11:34 +02:00
Safihre 8c42237d51 Correct handeling of TryList when server has timeout 2017-08-23 09:11:29 +02:00
Safihre 6a87f0c4e4 Correctly remove + from INFO label in all languages 2017-08-23 09:11:24 +02:00
Safihre f8630a878c Correct redirect after ports change 2017-08-22 10:11:41 +02:00
Safihre 7f6ef5e204 Only iterate over RSS feeds when there are feeds 2017-08-22 09:52:25 +02:00
Safihre 547d4dbf0a Only discard really non-unique hashes from md5of16k 2017-08-22 09:25:34 +02:00
Safihre 65e70a431c Show hover-title that the compress icon is Direct Unpack 2017-08-22 09:13:40 +02:00
Safihre f85f4de5ff Error when applying changes to RSS-feeds
Closes #1005
2017-08-21 20:25:34 +02:00
Safihre 97644dea16 Show more clear error message when UnRar of Par2 is missing 2017-08-21 16:27:04 +02:00
SABnzbd Automation 0a6105ebc1 Automatic translation update 2017-08-21 07:18:33 +00:00
Safihre de3d4f8d14 UnRar is required to read some RAR files 2017-08-21 08:26:12 +02:00
Safihre f337053aea Add error when NZO creation fails 2017-08-20 20:45:55 +02:00
Safihre 2a4b49a679 Warning "Invalid par2 files" can also be due to bad Par2-parameters 2017-08-20 16:41:18 +02:00
Safihre ebe526f8cf Also hide email-accounts in logging 2017-08-20 10:33:24 +02:00
Safihre 926cd7b132 Take the risk of allowing up to 5 bad articles in jobs without Par2 2017-08-20 01:35:06 +02:00
Safihre 99667aa410 Remove obsolete code from RSS 2017-08-20 01:20:56 +02:00
Safihre 67cab3465e Remove extra checks from RSS file-store loading 2017-08-20 00:31:22 +02:00
Safihre ce68a0654b Scheduler Priority/Category Pause/Resume also affects URL-grabbing
Linked #988
2017-08-19 23:59:14 +02:00
Safihre dffdc3ae1f Extend Scheduler to Pause/Resume based on category
Closes #549
2017-08-19 23:56:51 +02:00
Safihre 28e5311c6c Change logging line of SSL-protocols
Linked #994
2017-08-19 22:52:33 +02:00
Safihre ebf0526420 Do not show NZB-age when it is nonsense
For example with a grabbing NZB.
2017-08-19 21:49:03 +02:00
Safihre a2f73ca1f0 Add feed name to URL-grabbing title
Closes #988
2017-08-19 21:40:43 +02:00
Safihre 8ca150c48d Allow pausing of URL-grabbing
Linked #988
2017-08-19 21:20:28 +02:00
Safihre d765fa09f1 Change certificate-file location for distributions 2017-08-19 20:33:46 +02:00
Safihre 878d68d343 Warn in case the password file has too many passwords 2017-08-19 16:06:06 +02:00
Safihre fc7bd78dfa Translation changes 2017-08-19 13:28:21 +02:00
SABnzbd Automation aca6ed360c Automatic translation update 2017-08-19 09:17:50 +00:00
Safihre 53672d1d73 Reset all NZO TryList when doing Prospective Add
I thought in c14b3ed82a that this was enough, but clearly it is not.
2017-08-18 19:36:20 +02:00
SABnzbd Automation 07d316ed4f Automatic translation update 2017-08-18 15:44:05 +00:00
Safihre 8aa57bf406 Update Config jQuery to 3.2.1 and Peity to 3.2.1 2017-08-17 11:00:47 +02:00
Safihre d369097573 Update KnockoutJS to 3.4.2 2017-08-17 11:00:47 +02:00
Safihre f4960715fa Do not run get_new_id forever in case of problems
#984
2017-08-17 11:00:47 +02:00
Safihre 83ba676c43 Correct loading of extra certificate files
Also include logging of number of found certificates
2017-08-17 11:00:47 +02:00
Safihre 12cca9dea1 Provide certificates with macOS and Windows build
We have to bring our own certficates on Homebrew Python 
The certifi package brings the latest certificates on build 
This will cause the create_default_context to load it automatically
We also use this on Windows so users are always up-to-date
2017-08-17 11:00:47 +02:00
Safihre 5f52535c44 Faster look-ups when restoring queue 2017-08-17 11:00:47 +02:00
Safihre 2f95410ab4 Allow for older queue-files 2017-08-17 11:00:47 +02:00
Safihre 5749c0c008 Keep TryList state
Part 1 of #973
2017-08-17 11:00:47 +02:00
Safihre 73c71ef4bf Allow filenames with also 1-char file extension 2017-08-17 11:00:47 +02:00
Safihre 6bc1c51013 Remove server specific categories
Finally. This feature has only caused problems.
2017-08-17 11:00:47 +02:00
Safihre bcdd3302a6 Stall prevention by checking TryList 2017-08-17 11:00:47 +02:00
Jonathon Saine 0fc3b60054 Add propercheck to allow PROPER/REAL/REPACK releases to bypass the series episode dupe check. 2017-08-17 10:54:57 +02:00
Safihre e9b0d4d691 On to 2.3.0 we go 2017-08-17 10:53:32 +02:00
SABnzbd Automation 0dc2c6687d Automatic translation update 2017-08-17 08:43:38 +00:00
Safihre b061e582b6 Update text files for 2.2.0 2017-08-16 22:56:10 +02:00
Safihre 690731fd79 Update wizard Ad URL 2017-08-16 13:34:01 +02:00
Safihre 068b7ed7f5 Disk-speed test for Direct Unpack would cause restart 2017-08-15 21:53:24 +02:00
Safihre aae2fdcd32 Update Unrar to 5.5.0 for Windows and macOS
Right on time.
Closes #935
2017-08-14 20:37:11 +02:00
Safihre d3628a1eb7 CherryPy 8.1.2 - Catch the [Errno 0] Error
Untill a fix is found.
2017-08-13 16:58:20 +02:00
Safihre 9cc8176d87 Server-tests were broken due to deprecation warning
Linked #996
2017-08-12 14:52:34 +02:00
Safihre 27f83f21be Update text files for 2.2.0RC3 2017-08-12 11:39:28 +02:00
Safihre 5e31a31a21 Make Server charts timezone-proof
Closes #997
2017-08-12 21:23:18 +12:00
Safihre a077012478 Windows fix for subprocess would break when options were not specified 2017-08-12 09:42:47 +02:00
Safihre fed0e0f765 Use win32api for power-options on Windows 2017-08-12 09:39:25 +02:00
Safihre fbdbf7ab22 Improve par2 handeling by always parsing md5of16k and checking new sets
- We postpone only par2-files with actual blocks, in case of duplicate named par2 files that are of different sets we want all base-par2 files.
- The md5of16k is now calculated for every par2 file we get so we can rename everything.
- We also check during assembly if maybe a md5of16k is now available, in case the par2 file came in later.
- If a par2 file comes in, we double check if maybe this pack was not known yet. The setname might not be unique. This way we make sure that everything gets verified at the end.

Still need obfuscation improvements, but that's for later.
Linked #998
2017-08-12 00:33:32 +02:00
SABnzbd Automation f013d38d00 Automatic translation update 2017-08-10 11:45:54 +00:00
Safihre 93b9c8a6da Correctly escape the values in EN.po 2017-08-10 13:33:11 +02:00
SABnzbd Automation e3a779bbc6 Automatic translation update 2017-08-09 21:44:13 +00:00
Safihre adfce8c8b6 Update text files for 2.2.0RC2 2017-08-09 23:03:44 +02:00
Safihre a49d68c0db Double check that we have pynotify version we can work with
Closes #992
2017-08-09 22:56:07 +02:00
Safihre e4156e76d1 Disable auto-zoom on mobile for adaptive pages 2017-08-09 22:50:53 +02:00
Safihre 35b66eea0e Add more space to Config header just in case 2017-08-09 18:45:01 +03:00
Safihre 4d0cf8d45f Correct naming and small style fix 2017-08-09 18:36:20 +03:00
Safihre ad9fef5f41 Prevent SQL injection via category-argument with ' in them 2017-08-09 17:43:17 +03:00
Safihre 6235174995 CherryPy 8.1.2 - Correct f6c163b: macOS "Protocol wrong type for socket"
https://github.com/cherrypy/cheroot/pull/31
https://github.com/cherrypy/cheroot/pull/44
2017-08-09 09:55:04 +03:00
Safihre 4b9ca989c4 Correctly log the rar-files used by DirectUnpack
Not just assume we used them all.
2017-08-08 13:39:03 +03:00
Safihre 4d54aecceb Check the workdir after extraction if we got everything
Sometimes there might be a file left that we did not extract. For example there are NZBs that have the main download in abc.part01.rar etc but also a abc.rar file with the NFO or other things. 
SABnzbd would detect this as 1 set and not unpack both.
2017-08-08 13:37:09 +03:00
Safihre 11eeb6f2e9 Glitter filelist would not show 100% for very small files
Plus some cleanup of move-to-top/bottom code
2017-08-08 12:19:07 +03:00
Safihre 00364b1317 Only update RSS URI if it was modified
See #993
2017-08-08 12:04:54 +03:00
shypike 6666663f78 Fix typo in text about "allow duplicate files" 2017-08-08 09:17:25 +02:00
Safihre 3d6dfec47a Disk-space check in Assembler and check if space to write current file 2017-08-06 19:01:59 +03:00
Safihre 0f3d44aa4b safe_fnmatch should only do the matching
Oops, globber and globber_full are not the same!
2017-08-06 12:57:23 +03:00
SABnzbd Automation d2d2471950 Automatic translation update 2017-08-06 09:42:54 +00:00
Safihre b71343e8ab Safe fnmatch everywhere, just to be sure
Linked #990
2017-08-06 11:46:00 +03:00
Safihre 489f3f4ba0 Catch special chars like "[]!*" to break fnmatch and thus repair
Closes #990
2017-08-06 11:25:27 +03:00
Safihre 3765e8c350 Add warning when many duplicate files were discarded
Linked #531, Closes #986
2017-08-02 12:50:26 +02:00
Safihre 28d4f527b8 Fix the server-graphs
They did not display anything if it was the first of the month. Plus some style-fixes.
2017-08-01 13:31:18 +02:00
Safihre 1d8af8f97d Correct counts if one_folder is enabled 2017-08-01 11:28:34 +02:00
Safihre 829ef4bee8 Only one_folder is a reason not to delete the folder in DircetUnpack 2017-08-01 11:06:40 +02:00
Safihre 7e40c12e47 Get rarfiles to delete from both RarFile and parsing Unrar output
RarFile will fail to list all volumes when the job is encrypted, it will only list the first volume. But parsing the output of Unrar will fail on special-char filenames (probably limited to Windows). So now only jobs that are encrypted *and* have many special-chars will not have all rars deleted correctly,
2017-07-31 23:45:56 +02:00
Safihre 37d8d659f5 Show deprication warning for Server Categories 2017-07-31 22:26:21 +02:00
Safihre 0a29291be2 Abort all direct unpackers when disk-full 2017-07-31 22:26:04 +02:00
Safihre 7f3a5f309b Don't postpone if all par2 are desired and should be kept
Prevents downloading of all par2 in case people just wanted to keep some par2, but not all. If people really want all and no-cleanup, they should enable 'All par2'
2017-07-31 16:51:48 +02:00
Safihre 60ec5f9191 Less aggresive cache-busting 2017-07-31 10:38:45 +02:00
Safihre d03e801e74 Only show import warnings when not gone
Can be removed if #952 is resolved
2017-07-30 20:29:28 +02:00
Safihre 56bf484e77 Also show Verifying Repair status for par2cmdline 2017-07-30 16:51:28 +02:00
Safihre 66674469d5 Return of the DIR_LOCK
Yeah, turns out we probably do need that. In case automation adds a new job while the old one is still getting deleted. Hopefully solving #952
2017-07-30 15:57:34 +02:00
Safihre 09a86683e5 Retry icon was too red in Glitter Night 2017-07-30 15:56:49 +02:00
Safihre fc9a13879e CSS Tweaks to Night theme 2017-07-30 11:32:35 +02:00
Safihre 73f0885566 Schedule to deprecate Categories setting for Servers in next release
This option only gives headaches because users do not use it correctly or edge cases where downloads stall. 
If users report that they really want to keep this option, we can of course consider it.
2017-07-29 13:29:10 +02:00
Safihre 090b22f193 Revert "CherryPy 8.1.2 - Catch error of Python 2 in combination with new OpenSSL"
Not catching the right error because it's of the general Error class, not SSLError.
2017-07-28 18:46:46 +02:00
Safihre f9c092ae8f Move Notification Script up on the Config page and add Wiki-link
So they can see other services
2017-07-28 09:52:25 +02:00
Safihre 4246bc2aea Move enable_meta to more logical spot 2017-07-27 16:00:40 +02:00
Safihre c4fa047393 Automatic redirect to login page in Glitter 2017-07-27 14:56:34 +02:00
SABnzbd Automation 22e4d24a71 Automatic translation update 2017-07-27 12:29:06 +00:00
Safihre 96fccff63b Update text files for 2.2.0RC1 2017-07-27 13:50:41 +02:00
Safihre 6b46a15b49 Include indexer-hostname in the API-key missing warning 2017-07-27 11:50:12 +02:00
Safihre 98cc0dad55 API-key was always required for Indexer-feedback
Remove oznzb specific links for realy now.
Indexers should provide their rating-url in the headers/meta-data. Keeping the Specials setting for backwards compatibility for current OZnzb users.
2017-07-27 11:48:00 +02:00
Safihre 6fdeab6948 Permanent notice in Plush to upgrade to Glitter 2017-07-27 10:35:26 +02:00
Safihre 553dd04cea Restore 'Use tags from indexer' switch
https://forums.sabnzbd.org/viewtopic.php?f=11&t=22864
2017-07-27 09:52:59 +02:00
Safihre 0baa316a72 Revert "Remove enable_meta"
This reverts commit ff3c46fe1f.
2017-07-27 08:35:06 +02:00
Safihre 3ac209f9a9 CherryPy 8.1.2 - Catch error of Python 2 in combination with new OpenSSL
Closes #853
2017-07-26 16:34:45 +02:00
Safihre ec55f64a8a Correctly remove files when flat_unpack or no-job-folders set
Results could be pretty bad otherwise: https://forums.sabnzbd.org/viewtopic.php?f=11&p=112774
2017-07-26 09:30:39 +02:00
Safihre 932e1e577b Do not double check 'part' in RAR filename
Already caught by the regex, otherwise filenames that have "part" somwhere in the name will not be used for direct-unpack.
2017-07-26 08:59:26 +02:00
Hellowlol 56d5b1d9f8 Make bonjour easier to setup. 2017-07-25 15:52:30 +02:00
Hellowlol 28bdebb147 Let zeroconf know if we are using https. 2017-07-25 15:52:30 +02:00
Safihre 827fc7b64e Direct Unpack would block forever if file was missing 2017-07-25 11:58:29 +02:00
Safihre f5dde93644 Change History Retention label to fit
Closes #978
2017-07-24 19:21:43 +02:00
Safihre 60c574828e Sort Servers in Download Report
Closes #979
2017-07-24 19:14:31 +02:00
Safihre 14ca8342f9 Update wiki URL to 2.2 2017-07-24 13:36:16 +02:00
Travis 5d5b1bf053 Automatic translation update 2017-07-24 08:16:18 +00:00
Safihre ea4cdba3eb Update text files for 2.2.0Beta2 2017-07-24 10:08:21 +02:00
Safihre d6ecebc75a Use existing texts for Tag duplicate 2017-07-24 10:07:15 +02:00
Jonathon Saine b0af6a1761 User requested a way to track dupes but not have sab block/discard/pause the items (as he doesnt want to have to manually unpause). Figured we could add a 'Tag' switch for the Dupe detection. Works same way as Pause (shows duplicate tag in queue/adds warning/internally tags it) but does not pause the item.
Once the file is done downloading and makes it to the queue, you have no idea it was a duplicate.
You would have to use the 'Warning' and search for that.. and then assume the newer one was the dupe... we really should expose in the History that an item was a duplicate. Right now the 'search' box only looks at the name.. and I dont think we should be messing with the name to add duplicate there. I'd rather us just add a tag/flag whatever to notate this.. and can filter/sort on it. We also should do the same to the Queue as well.. since changing the name of the item to be `DUPLICATE / XXXXX` is a little jarring.

Now if we just exposed the dupe flag in the history, then the whole 'tag' option really isnt needed as it was just a means to an end.
2017-07-24 10:00:15 +02:00
Safihre 6e350f30fc Add midnight auto history-purge and modify texts 2017-07-24 09:47:05 +02:00
Safihre 169137c631 Implement History Retention setting
Closes #678
2017-07-24 09:47:05 +02:00
Safihre 6393dc0dca Remove history_limit from Specials 2017-07-24 09:47:05 +02:00
Safihre 1a27b4824b Style improvements to Queue and Server Graphs
Closes #977
2017-07-24 09:36:48 +02:00
Safihre 2b59a383cf Change label in History-status to 'Direct Unpack' 2017-07-24 09:05:02 +02:00
Safihre efbaaade22 Correct server graph timezone effects and only show months that we have 2017-07-23 23:14:10 +02:00
Safihre cf7e7b1f62 Only show usage data for days that have passed 2017-07-23 23:14:10 +02:00
Safihre 2c1746a92d Show Montly usage graphs per server in Config 2017-07-23 23:14:10 +02:00
Travis a2e57fd3d8 Automatic translation update 2017-07-22 17:27:02 +00:00
Safihre 932f8d9176 Wizard access was not limited by login and external access rules
Bad bad 
Closes #972
2017-07-22 18:54:33 +02:00
Safihre 5ffd82da89 Show vote up/down instead of Video/Audio score 2017-07-22 00:19:40 +02:00
Safihre 8b3de191d9 Add Retry All Failed button to Glitter 2017-07-22 00:11:13 +02:00
Travis 83d8a23e2c Automatic translation update 2017-07-20 22:13:49 +00:00
Safihre 58b107a4b5 Allow up to 5 missing/CRC'ed errors before cancelling Direct Unpack
Sometimes a CRC error is not so bad it turns out
2017-07-20 23:55:36 +02:00
Safihre a40609b39d Direct Unpack was started before whole file was written to disk
In case part 2 came in before part 1.
2017-07-20 23:55:36 +02:00
Sander Jo 0faa5d3dff Print applied permissions in octal 2017-07-20 21:04:00 +02:00
Travis 374239777e Automatic translation update 2017-07-19 07:55:50 +00:00
Safihre 9a7701d7e6 Update text files for 2.2.0Beta1 2017-07-19 09:33:00 +02:00
Safihre 01ff04f338 Allow Aborting of Direct Unpack during PP and add Completed label 2017-07-19 09:27:24 +02:00
Safihre eac39767dd Renames on Retry only when defined
Otherwise if it's None, later this will happen:
original_filename = self.renames.get(nzf.filename, '')
AttributeError: 'NoneType' object has no attribute 'get'
2017-07-19 09:23:58 +02:00
Safihre 0d0adf99fa Proper counting of bad articles for DirectUnpack & Prospective Par2 2017-07-18 22:07:56 +02:00
Safihre 16905ce34f Show filename for Unzip instead of Path and show start of Verification 2017-07-18 21:16:05 +02:00
Safihre 5287fa8a0c Stability improvements for Direct Unpack
Now shows the time spent in unpacking and many other bugs squased.
2017-07-18 21:15:30 +02:00
Safihre b72ab4fb8e Allow concurrent unpacking 2017-07-18 15:14:28 +02:00
Safihre 81054c675c Mimimum speed for Direct Unpack lowered to 40MB/s
It is tested during downloading, so if 40MB/s is still possible then we should be good to go.
2017-07-18 13:51:13 +02:00
Safihre 7362be8748 Group cfg settings by Config section
It was a big mess. 
Now they still could be sorted within each section.. next time.
2017-07-17 20:42:54 +02:00
Travis b4ba2b3463 Automatic translation update 2017-07-17 18:33:42 +00:00
Safihre 8bed6938c1 Change text in DirectUnpack Enabled message
See also #966
2017-07-17 20:11:50 +02:00
Safihre ecf16f6201 Show DirectUnpack progress the same as Unpack progress: xx/xx 2017-07-17 17:07:44 +02:00
Safihre bf240357df Regressions in preparation of extraction path
Thanks @Cpuroast
2017-07-17 16:45:58 +02:00
Safihre ddcf447957 Add missing save_config after modifying settings
Closes #966
2017-07-17 10:10:20 +02:00
Safihre d9642611e2 Correct error in missing notify options
#966
2017-07-16 20:28:15 +02:00
Safihre 0018c6f263 Move regex to top and increase save-timeout 2017-07-16 19:26:35 +02:00
Safihre 6398bfa12f Use speed from download-log instead of re-calculating
Closes #829
2017-07-16 19:22:52 +02:00
Safihre 01dfb7538d Correct FileList Move to Top/Bottom CSS for Firefox 2017-07-16 14:28:04 +02:00
Safihre 3f0d4675b6 Fix CSS for Direct Unpack and Move to Top/Bottom 2017-07-16 14:18:50 +02:00
Safihre f23c5caf80 Fix typo in DirectRenamer for non-Windows 2017-07-16 13:55:36 +02:00
Safihre bd22430b26 Update text files for 2.2.0Alpha3 2017-07-16 11:04:17 +02:00
Safihre 1189a7fdbc Use tuple in endswith for Direct Unpack
Thanks @hellowlol
2017-07-16 10:59:00 +02:00
Safihre f3aa4f84fc Remove waiting-time between URLGrab's
Other newsreaders grab multiple URL's at once, so no need for us to wait.
2017-07-16 10:40:39 +02:00
Safihre ea26ce4700 Remove non-seperator RSS-url commas by detecting if they are valid URLs
Closes #965
2017-07-16 10:30:09 +02:00
Safihre a1e649b7e2 Correct error in PAR_Verify with renames 2017-07-15 23:43:32 +02:00
Safihre 3b9f2b2cf0 Remove par2classic/cmdline for Windows and macOS 2017-07-15 23:33:20 +02:00
Safihre 7333d19e1c Notifications selection based on Categories
Closes #716
2017-07-15 22:22:20 +02:00
Safihre 232d537d23 Correct Direct Unpack locking behavior for multisets 2017-07-15 17:02:20 +02:00
Safihre c6e17e7bcb Duplicate par2-16k values need force-remove 2017-07-15 17:02:20 +02:00
Safihre 54c6fd55dd Detection of forbidden-Windows names altered
Now we already sanatize the name during Assembler and when we have to make decisions for Unrar/Par2 we need to know if they might create something unsafe.
2017-07-15 17:02:20 +02:00
Safihre 0625aa1ca8 Make sure all Par2-16k signatures are unique, also in multisets 2017-07-15 17:02:20 +02:00
Safihre 83643f3298 Remove allow_streaming
Bit redundant now we have DirectUnpack
2017-07-15 17:02:20 +02:00
Safihre ff3c46fe1f Remove enable_meta 2017-07-15 17:02:20 +02:00
Safihre 0930f0dcee Test disk-speed first time DirectUnpack is called 2017-07-15 17:02:20 +02:00
Safihre 3221257310 UnRar's ERROR is also an error
And add starting file to log.
2017-07-15 17:02:20 +02:00
Safihre 8048a73156 Handle active DirectUnpacker in postproc better 2017-07-15 17:02:20 +02:00
Safihre ea552cd402 Cancel DirectUnpack when the final name changes 2017-07-15 17:02:20 +02:00
Safihre dcb925f621 Case insensitive matching for DirectUnpack sets 2017-07-15 17:02:20 +02:00
Safihre cce91e1985 DirectUnpacker should stay to listen to new sets 2017-07-15 17:02:20 +02:00
Safihre e17d417c2e Re-introduce locks for TryList
After studying everything, it really needs it. Closes #738
2017-07-15 17:02:20 +02:00
Safihre a69f5bd2df Prevent DirectUnpack locking the PostProcessing 2017-07-15 17:02:20 +02:00
Safihre 97e53eb4d3 Better DirectUnpack percentage counter 2017-07-15 17:02:20 +02:00
Safihre a6da2b7bee Prevent possible crash in par2_repair 2017-07-15 17:02:20 +02:00
Safihre 4a21e7c217 Show percentage of DirectUnpack, when available 2017-07-15 17:02:20 +02:00
Safihre 9bd3c7be44 Increase maximum number of unpackers
Unrar takes almost no memory anyway
2017-07-15 17:02:20 +02:00
Safihre 434f5c4b2d Remove Audio/Video quality rating icons from Queue 2017-07-15 17:02:20 +02:00
Safihre d3cc4f9f07 Direct Unpack indicator for Queue 2017-07-15 17:02:20 +02:00
Safihre a16aa17c17 Don't start when not set to +Unpack and abort if Category changed 2017-07-15 17:02:20 +02:00
Safihre 68445d0409 Full working implementation of DirectUnpack with multi-sets 2017-07-15 17:02:20 +02:00
Safihre 32b68a45cc Integrate with PostProc 2017-07-15 17:02:20 +02:00
Safihre 345f8359cc Unpack to the right directory (with Sorter support) 2017-07-15 17:02:20 +02:00
Safihre 81f9886584 Add Direct Unpack to Config 2017-07-15 17:02:20 +02:00
Safihre adbc618808 Improvements to detection of volumes 2017-07-15 17:02:20 +02:00
Safihre 41eafc6b4b Become set-specific 2017-07-15 17:02:20 +02:00
Safihre 9f18d8e8c1 Basic working Direct Unpack
Lots to do
2017-07-15 17:02:20 +02:00
Safihre 8c2c853166 Make sure to always have lowest part number 2017-07-15 17:02:20 +02:00
Safihre 97914906a0 Also handle GNTP errors during sending 2017-07-14 14:43:39 +02:00
Safihre f1ce4ed19b Correctly handle new GNTP errors 2017-07-14 14:41:03 +02:00
Safihre 99185d8151 Update GNTP to 1.0.3
Closes #334
2017-07-14 14:25:07 +02:00
Safihre 385b6b7ade Remove QCHECK_FILE again 2017-07-14 14:25:07 +02:00
gwyden 81ea513f8c Added buttons and logic to move to top and bottom of download queue (#962)
* added buttons and logic to move to top and bottom of queue
* allowed for a larger control box for the new buttons
* Cleanup of unnecessary code
* Simple top and bottom of queue using existing queue data
2017-07-13 23:52:43 +02:00
Safihre 336b1ddba3 Always remove forbidden Win-devices from filenames
This breaks support for par2cmdline on Windows with forbidden names. Assuming no users that have disabled both Multipar *and* par2_multicore
2017-07-12 18:38:19 +02:00
Safihre 7274973322 Shorten par_cleanup code 2017-07-12 18:38:19 +02:00
Safihre af132965de Revert "Remove QCHECK_FILE, not needed"
This reverts commit 4f8cc3f697.
2017-07-12 18:38:19 +02:00
Safihre 5586742886 Use RarFile.volumelist to get list of used rar-volumes 2017-07-12 18:38:19 +02:00
Safihre 5868b51490 Use fix to allow unicode arguments to POpen on Windows 2017-07-12 18:38:19 +02:00
Travis 7f17a38b9b Automatic translation update 2017-07-12 15:10:14 +00:00
Safihre 415e843ebb Remove 'WARNING:' label from Assembler warnings
It was inconsistent with other messages
2017-07-11 13:33:50 +02:00
Safihre 7ffc1192bb Only par2-rename when actually different 2017-07-11 12:00:36 +02:00
Safihre 945e769a03 Also performe prospective-par2 on renamed files 2017-07-10 23:06:05 +02:00
Safihre 86c7fb86cc Ignore first-16k par2 info if it's not unique 2017-07-10 22:51:17 +02:00
Safihre ff20f3f620 Fix possible unicode error in tvsort and typo in newsunpack
Closes #950
2017-07-10 21:56:07 +02:00
Safihre e8bef94706 Correctly handle renames on (multiple) retries 2017-07-10 21:03:37 +02:00
Safihre d05fe2d680 More uniform handeling of renames 2017-07-10 20:53:31 +02:00
Safihre 4f8cc3f697 Remove QCHECK_FILE, not needed 2017-07-10 19:54:59 +02:00
Safihre 6fa619fa37 More robust renaming based on par2 first-16k info
Also when the correct name is
2017-07-10 17:40:39 +02:00
Safihre a43f5369ea Do not rename .par2 filenames from NZB
They are usually correct, if mentioned at all
2017-07-10 17:29:34 +02:00
Safihre 2040173dc2 Rename parts of Assembler to be more coherent 2017-07-10 17:20:03 +02:00
Safihre a15b7ec7ac Remove Windows utf8 detection using par2
Obsolute now we have Multipar
2017-07-10 17:17:12 +02:00
Safihre 6adcf2ce10 Stylistic changes from previous commits 2017-07-10 17:11:32 +02:00
Safihre e756b9b5c1 Correct filenames while downloading using first-16kb par2 info
Maybe we can also do DirectUnpack!
2017-07-10 17:07:16 +02:00
Safihre b3de745849 Do not use article-filename if it looks obfuscated 2017-07-10 15:54:17 +02:00
Safihre 77f3dc18b5 Corrections of Move To Top for filelists 2017-07-09 19:51:18 +03:00
gwyden 6b2f15f82e Move To Top/Move To Bottom buttons for filelists (#959)
* Control creation

* JQuery to make the buttons work

* minor text fixes

* tab to spaces cleanup

* style additions and removed hard text from code

* Moved button control to modal finish render event, gave file details a little more room

* Moved control to replace age and size on mouseover

* Added margins and color corrected for the night theme

* resolved night theme readability

* move to working top and bottom

* controls would lose event bindings after the append.  Detach first then insert

* Move to Top and Bottom buttons for files in each NZB
2017-07-09 18:34:33 +02:00
Safihre 570e58611d Repair would fail if extrapars were deleted by previous run
Closes #961
2017-07-06 18:30:31 +03:00
Safihre 6b69010aec Add logging for missing NZF database to debug #952 2017-06-28 11:35:52 +02:00
Travis e3e2fb7057 Automatic translation update 2017-06-27 09:23:17 +00:00
Safihre ece04909e7 Add latest changes to changelog 2017-06-27 00:13:24 +02:00
Safihre 963920eb88 Semi-correct missing MB counter for Pre-check
It's still off (for Precheck only), but not sure why
2017-06-26 22:03:50 +02:00
Safihre cf5fa542b6 Don't show import errors when NZO is gone 2017-06-26 21:36:49 +02:00
Safihre 1be7e99754 Remove last hashlib workaround 2017-06-26 21:00:17 +02:00
Safihre 14e3334682 Correctly handle disk-space calculations
No more glitches in the interface during downloading.
2017-06-26 16:25:03 +02:00
Safihre b1e033dd55 Update text files for 2.2.0Alpa2 2017-06-26 14:28:22 +02:00
Safihre 111feb1b57 Show missing articles as MB instead of number of articles 2017-06-26 13:46:54 +02:00
Safihre 886b23d034 Update translatable texts 2017-06-26 10:40:02 +02:00
Safihre f2590792b3 Download all par2 always when enable_par_cleanup is disabled
https://forums.sabnzbd.org/viewtopic.php?f=2&t=22744
2017-06-26 09:19:05 +02:00
Safihre 02a497ed74 Only set Post-processing Completed/Failed at the very end
To prevent race-issues
2017-06-25 20:32:45 +02:00
Safihre 48df0eed84 Add logging for user-actions
We were missing way too many things
2017-06-24 23:18:14 +02:00
Travis 0f58cbb671 Automatic translation update 2017-06-24 18:51:32 +00:00
Safihre 9d71670f59 Full hearted 2017-06-24 20:33:16 +02:00
Safihre 7f838ebb38 Move Donate-link in Glitter 2017-06-24 13:51:19 +02:00
Safihre ef1cb05bc8 Store result of MultiPar verification
Just in case prospective par2 didn't catch them all
2017-06-24 10:47:33 +02:00
Safihre c14b3ed82a Prospective Par2 correct TryList reset
I think
2017-06-24 00:34:57 +02:00
Safihre 792e337936 Rename increase_last_history_update to history_updated 2017-06-23 22:58:29 +02:00
Safihre 6cd2e66052 Use actual counter for LAST_HISTORY_UPDATE
Would otherwise miss some updates
2017-06-23 12:49:40 +02:00
Safihre 728022b86d Show Verifying Repair stage for MultiPar 2017-06-23 11:10:47 +02:00
Safihre 7718446313 Convert HTML to text in warning messages
Related: #952
2017-06-23 08:30:54 +02:00
Travis 66dea54053 Automatic translation update 2017-06-22 20:34:50 +00:00
Safihre f19b60bd41 Also don't list line numbers for NSIS pot file 2017-06-22 21:46:55 +02:00
Safihre 09f1c92856 Move enable_multipar to Specials
Moving forward to make MultiPar the only used par2-solution on Windows.
2017-06-22 11:03:53 +02:00
Safihre 589715901d Correct counting during Checking/Verification in MultiPar 2017-06-22 10:50:00 +02:00
Safihre 3f1a5ff5e0 Fix typo in extract_pot.py 2017-06-22 09:32:59 +02:00
Safihre 49cd956d4c Do not list line-number for POT files
To avoid commit-overhead when updating texts
2017-06-21 22:17:49 +02:00
Safihre f9acde862f Correct counting in MultiPar Checking 2017-06-21 21:40:24 +02:00
Safihre 503e1dd899 Re-work NZO_LOCK to actually lock when saving 2017-06-21 20:54:00 +02:00
Safihre c8e12b948d Mixed up application of use_pickle 2017-06-21 17:18:16 +02:00
Safihre 18949d68c0 Fix wrong addition to en.po
Thx @thezoggy!
2017-06-21 17:12:19 +02:00
Safihre 0c51b6c016 Add Donate links to main Config page and Glitter help modal 2017-06-21 09:57:56 +02:00
Safihre 63a5c22c1f Don't continue when fetching failed
Possibly: #914
2017-06-21 09:19:25 +02:00
Safihre f76e2a7b56 All links to sabnzbd.org should be HTTPS 2017-06-20 23:15:15 +02:00
Safihre bab151d6f5 Properly fix redirect after enabeling/disabeling HTTPS 2017-06-20 22:46:48 +02:00
Safihre d43fec088b Fix typo in Correct redirect when enabeling HTTPS 2017-06-20 19:48:18 +02:00
Safihre a8ca1cbcd7 Correct redirect when enabeling HTTPS 2017-06-20 19:47:45 +02:00
Safihre ada3494483 Fix typo in Config JavaScript 2017-06-20 19:04:38 +02:00
Safihre 43c238b7f1 Update translations 2017-06-17 11:23:50 +02:00
Safihre 128d10c51e Restart-text was always shown in English 2017-06-17 11:19:59 +02:00
Safihre 1a1e01f9f6 Correct upgrade-notice 2017-06-17 11:13:53 +02:00
Safihre 8483e4ab8a Add last minor change to changelog 2017-06-17 09:01:53 +02:00
Safihre f6c163b505 CherryPy 8.1.2 - Catch OSX "Protocol wrong type for socket" 2017-06-17 08:40:14 +02:00
Safihre 8f30173db0 Update text files for 2.2.0Alpha1 2017-06-16 15:38:17 +02:00
Brendan Ball 0372ff95bb Added support for systemd power controls
added systemd support to powersup.linux_shutdown, linux_hibernate, linux_standby
2017-06-16 15:19:32 +02:00
Safihre 6fa29c7877 Update translatable texts 2017-06-15 20:58:48 +02:00
Safihre d4c9121593 Remove NZB_LOCK
Not required
2017-06-15 20:54:13 +02:00
Safihre 76a8df0282 Make it more clear that Hostname verification is a server problem 2017-06-15 20:51:27 +02:00
Safihre 0b6d8309a0 Format the SSL certificate messages more for humans 2017-06-15 20:51:21 +02:00
Safihre 10a9bc0817 Don't show Advanded on Config>General if HTTPS disabled 2017-06-15 16:09:01 +02:00
Safihre 2a14af4ffa Firefox doesn't suck at animations anymore 2017-06-15 14:47:04 +02:00
Safihre d1a4a292e3 Prevent log-flooding when job is too old for server 2017-06-13 21:40:45 +02:00
Safihre 14c0efa151 Fix error in MultiPar repair when first .par2-file was broken 2017-06-13 21:40:45 +02:00
Safihre 4fc03f2581 Discard all articles at once when too old for server 2017-06-13 21:40:45 +02:00
Safihre 3205b9fda9 Don't fill anything for bandwith limit if nothing is set
Now it would just fill "M" when nothing was set.
2017-06-13 21:40:45 +02:00
Safihre 953e0d6c22 Remove DIR_LOCK
The operations it was locking were always performed from 1 thread anyway.
2017-06-13 21:40:45 +02:00
Safihre b50ce54ca9 Reformat IO_LOCK to really only protect against NZO-saving collisions
The main intent was not to read/write to same file, but this can (as far as I can see) never happen anyway. 
Before this change 2 threads could not be writing data at the same time, even if they were writing to completly different directories.
2017-06-13 21:40:45 +02:00
Safihre 5e7558ce4a Remove locks from ArticleCache
All operations on the list are atomic or modify objects in place that can't be read at the same time.
2017-06-13 21:40:45 +02:00
Safihre 8aa6362432 Remove NZBQueue wrapper functions
Direct-access!
2017-06-13 21:40:45 +02:00
Safihre 02ebb97a8b Remove NZBQUEUE_LOCK and only use synced wrappers when needed
Less locks = Less waiting
2017-06-13 21:40:45 +02:00
Safihre b36063403d Remove legacy asserts and work-a-rounds 2017-06-13 21:40:45 +02:00
Safihre 526ffa2afb Add new translation to Changelog 2017-06-13 17:26:37 +02:00
Safihre 5b3fd812d8 Don't break MO-creation on missing Email templates 2017-06-13 14:40:47 +02:00
Safihre af6dac9cdc Refresh Config > General when submitting language change 2017-06-13 14:14:26 +02:00
Safihre bc25d936bb Show Hebrew in language menu
Correction of previous commit: I intended to write that it's an experiment because Hebrew is RTL. It seems to work!
2017-06-13 14:14:01 +02:00
Safihre b497fe1444 Add Hebrew as language
This is an experiment, since Hebrew is LTR language. But a translator translated almost all the texts, so we want to use his efforts!
2017-06-13 11:43:18 +02:00
Safihre 3f456cce05 Move max_art_opt to Specials
Only for special cases (don't know which ones, but I can imagine it could be usefull..)
Deprecate later!
2017-06-13 00:40:02 +02:00
Safihre 4dd2f089ec Move replace_illegal to Specials
Who doesn't want that
2017-06-13 00:37:20 +02:00
Safihre b1b1bc248d Reformat startswith() to use tuples when testing multiple options 2017-06-11 22:01:45 +02:00
Safihre d9e675469c Don't throw errors when silent-saving fails 2017-06-11 22:01:45 +02:00
Safihre ede0ca1772 Catch new way of par2 reporting bad parameters
See: https://forums.sabnzbd.org/viewtopic.php?f=2&t=22713&p=112209#p112209
2017-06-11 22:01:45 +02:00
Safihre 2d098a1477 Defend against possible NTFS crash
Closes #930
2017-06-11 22:01:45 +02:00
Safihre e5f014b68e Replace spaces/dots in the order written in the Config 2017-06-11 22:01:45 +02:00
Safihre b3a9dc9eeb Fix old code throughout 2017-06-11 22:01:45 +02:00
Safihre 2a06cec27c Seperate compatibility-check logic in NZBQueue 2017-06-11 22:01:45 +02:00
Safihre 19230c889d Remove unused functions and constants (vulture) 2017-06-11 22:01:45 +02:00
Safihre c969ce552c Remove unused constants (vulture) 2017-06-11 22:01:45 +02:00
Safihre 2def600d21 Remove unused imports and functions (pyflakes) 2017-06-11 22:01:45 +02:00
Safihre 02aa8f18c8 Trylist doesn't need locks, all atomic operations 2017-06-11 22:01:45 +02:00
Safihre fcd9522dae Remove unused import and define NzbQueue as proper class 2017-06-11 22:01:45 +02:00
Safihre 72d3ce885e Remove un-used version definitions from __init__ 2017-06-11 22:01:45 +02:00
Safihre b428996eb7 Convert pickles and keep queue order
Also restore future jobs


dewd
2017-06-11 22:01:45 +02:00
Safihre 2b4eb58fad Correctly show message about old Queue-version
Now it's also upgrade-proof, and not just works for 1 version.
2017-06-11 22:01:45 +02:00
Safihre 240e8dff60 Bump Queue-Version to force Queue-Repair 2017-06-11 22:01:45 +02:00
Safihre 1c286afde6 Implement __slots__ to conserve memory
Objects such as Article() get created a lot. By using the __slots__ property, python will only reserve space for the give keywords instead of a whole diectonary. 

Testing showed that (on WinX64) 1 job now takes between 2-3MB of memory when loaded, compared to 4MB before.
2017-06-11 22:01:45 +02:00
Safihre 2eeb908540 Revert "Remove enable_par_cleanup"
This reverts commit f5ab4a2253.
2017-06-11 16:10:25 +02:00
Safihre 562e6ecce9 Fix Untill typos in texts and comments
Closes #943
2017-06-11 11:51:50 +02:00
Safihre 4bd0d32508 Bump version to 2.2.0-develop 2017-06-11 11:46:36 +02:00
Safihre 6f2ccbef80 Always show Par2-Multicore status on first Config page on Linux 2017-06-09 16:04:30 +02:00
Safihre 61a6cb6d96 Update translations 2017-06-09 11:27:46 +02:00
Safihre 443efb5eda Update text files for 2.1.0 2017-06-09 11:10:48 +02:00
Safihre ba3c731fee Correctly switch HTTPS port if occupied on first start 2017-06-08 10:50:26 +02:00
Safihre e55f72dd1d Limit the maxium extension of the bps_list with zeros
If the last bps measurment was very long ago, this could cause thousands of zeros to be pre-loaded. This could cause MemoryErrors on low-spec devices: 
https://forums.sabnzbd.org/viewtopic.php?f=3&t=22709&p=112149
2017-06-07 18:36:36 +02:00
Safihre b28c0a60a1 Remove option to disable Par2 Multicore on macOS
In the hope to remove par2-classic from the macOS packages in later stage.
2017-06-04 20:30:54 +02:00
Safihre b7a80bf026 Remove nr_decoders
2 seems to be perfect for now, it was always intended as a temporary setting.
2017-06-04 00:14:52 +02:00
Safihre fe7218e64b Remove log_new
Not used anywhere..
2017-06-04 00:11:15 +02:00
Safihre 0857a9046d Remove login_realm
Never used nor shown to users to be able to modify it
2017-06-04 00:08:02 +02:00
Safihre 354131b78a Remove prio_sort_list
Nobody seems to care about it
2017-06-04 00:05:06 +02:00
Fish2 e3ae91a4f8 lossless compression of images saved 40KB (87%) 2017-06-04 00:00:25 +02:00
Safihre 52cc5e2e4f Remove create_group_folder NZO attribute 2017-06-03 23:53:48 +02:00
Safihre 0c04451442 Remove create_group_folder
Latest forum topic about this was in 2008
2017-06-03 23:52:54 +02:00
Safihre f5ab4a2253 Remove enable_par_cleanup 2017-06-03 23:48:51 +02:00
Safihre 1303dfe17a Remove allow_64bit_tools
None of the tools have ever shown problems on their intended platform. We can opt to remove them from their respective builds for the releases, to reduce download-size.
2017-06-03 23:38:57 +02:00
Safihre 55d80f26fa Update wiki links for 2.1
Only tiny changes, but still different from 2.0.
2017-06-03 23:30:52 +02:00
Safihre 7aee585748 Remove depricated functionality to remove samples when adding NZB
Was removed long ago because removing Sample files before downloading could give verification problems.
2017-06-03 14:07:47 +02:00
Safihre 196858409c Update text files for 2.1.0RC1 2017-06-01 22:44:34 +02:00
Safihre 21467dd62f Only download all par2 when there was a problem 2017-06-01 22:43:24 +02:00
Safihre bb30eb7d11 Always enable QuickCheck 2017-06-01 22:35:54 +02:00
Safihre fb9f4a7373 User Par2 Parameters for Multipar 2017-06-01 22:35:29 +02:00
Safihre ccd5c1c75e Show par2 Extra Parameter problems as Error 2017-06-01 22:02:00 +02:00
Safihre 015c578cdd Add par2_multicore to Specials (just in case)
#902
2017-05-30 23:04:18 +02:00
Safihre 8eb4ce2914 On Windows, only show MultiPar 2017-05-30 22:45:26 +02:00
Safihre a6b8108ee6 Support macOS >= 10.9 (removes 32bit support) 2017-05-30 14:16:34 +02:00
Safihre 181a56218a Support macOS >= 10.8 2017-05-30 14:01:34 +02:00
Safihre 24feaaebd6 Remove macOS PPC support 2017-05-30 13:56:13 +02:00
Safihre e1945e7a35 Add license file and README for MultiPar 2017-05-28 10:15:12 +02:00
Safihre 072f65dd9c Show history status if par2 was aborted due to Disk full 2017-05-27 19:04:22 +02:00
Safihre bde03ecc63 False-positive showing disk-full in Glitter 2017-05-27 17:10:00 +02:00
Safihre 73c2e23da4 Remove 'never_repair'
Should just use only Download feature
2017-05-27 14:32:17 +02:00
Safihre e6233831d1 Fix typo in Multipar text 2017-05-27 01:15:20 +02:00
Safihre 82ccbdaa7b Make human-readable dates the default date-format 2017-05-25 12:24:34 +02:00
Safihre bf5212a81c Allow aborting running PP-script
Closes #313
2017-05-25 12:23:46 +02:00
Safihre 5c6cc932cf Update text files for 2.1.0Beta1 2017-05-25 11:02:03 +02:00
Safihre ed4430a7e0 Update translatable texts 2017-05-24 12:39:29 +02:00
Safihre eb73f78b1f Remove unused try/except and allow cancelling of MultiPar 2017-05-24 12:39:29 +02:00
Safihre 314aad0009 Bring back unique-unpack staging 2017-05-24 12:39:29 +02:00
Safihre 98d0c5c52f Update versioning 2017-05-24 12:39:29 +02:00
Safihre 4d4da889ec Clear text in Config > Switches about Multipar 2017-05-24 12:39:29 +02:00
Safihre 4a622f59ba Updates to Multipar code and unpack-info saving 2017-05-24 12:39:29 +02:00
Safihre 913b92088a Update Multipar to v1.2.9 2017-05-24 12:39:29 +02:00
Safihre 80f4690df8 Enable and notify users about Multipar 2017-05-24 12:39:29 +02:00
Safihre f552531703 Adding MultiPar interpreter 2017-05-24 12:39:29 +02:00
Safihre 707d4a7a0c Adding Multipar executables 2017-05-24 12:39:29 +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
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 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
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 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 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 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
shypike d53cf598a4 Update translations 2017-02-23 19:55:51 +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 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 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 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 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 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 1be50ebe59 Fix the workings of RSS matching 2017-01-13 15:11:47 +01:00
Safihre 80404b3148 Set hard limit on Decoder-queue 2017-01-13 10:28:32 +01:00
shypike 0f3c4d7363 Update translations 2017-01-13 06:58:57 +01:00
Safihre 939a5ab7c0 Update text files for 1.2.0 Final 2017-01-12 12:48:53 +01:00
Safihre 722161b6cd Handle Server prio-coloring in template instead of Javascript 2017-01-12 12:48:53 +01:00
Safihre 7c8dc6e0d9 Small improvement of icon-loading in RSS window 2017-01-12 12:48:53 +01:00
Safihre efdd06b3e9 Add a link with DMCA information to failure messages
So we avoid people asking us on the forum
2017-01-12 12:48:53 +01:00
Safihre ee689c231c Not having _yenc is bad
The difference in CPU load is just too big (3x fold)
2017-01-12 12:48:53 +01:00
Safihre 9e3db377fb Combine 'Skip' column to get more space for Title in RSS 2017-01-12 12:48:53 +01:00
Safihre 6cb4c93e89 Also support nntmux tags in RSS parser
On top of nZEDb and newznab
2017-01-12 12:48:53 +01:00
Safihre 15dcb274c3 Remove old jQuery
Closes #766
2017-01-12 12:48:53 +01:00
Safihre d721476986 Update text files for 1.2.0RC1 2017-01-12 12:48:53 +01:00
Safihre 850a762905 Don't error on drag-and-drop Folders
Closes #765
2017-01-12 12:48:53 +01:00
shypike 7c1e4f8e41 Update main POT file 2017-01-08 12:44:51 +01:00
shypike 37182f07fb Update translations 2017-01-08 12:40:42 +01:00
Safihre 30519d92fc Huge recv() size can cause MemoryError
Seems there is some limit here.
2017-01-06 14:36:12 +01:00
Safihre a1cbd76985 Hide Multicore par2 switch when not available
Only works on Win/OSX.
2017-01-06 14:36:12 +01:00
Safihre 6df8d7a30a Pause-countdown was broken in Glitter 2017-01-06 14:36:12 +01:00
Safihre 09568274b3 Update jQuery to 3.1.1 for Config
Not yet in Glitter, there it causes some side-effects
2017-01-06 14:36:12 +01:00
Safihre fa6e02ee8e Try to avoid javascript error with many RSS Filters 2017-01-06 14:36:12 +01:00
Safihre 56346a3a64 Update Changelog 2017-01-06 14:36:12 +01:00
Safihre fb1d9842d4 Fix link to set Max line speed in Glitter 2017-01-06 14:36:12 +01:00
Safihre 4642ff0cdb Update Translatable texts 2017-01-06 14:36:12 +01:00
Safihre 508ca1dbc0 Correct URL's for Config Search pre-loader
Otherwise it does a redirect
2017-01-06 14:36:12 +01:00
Safihre 997ed57188 Tweak to detect size in more cases for RSS 2017-01-06 14:36:12 +01:00
Safihre 8630c712bb Only enable CherryPy logging when enabled from command-line 2017-01-06 14:36:12 +01:00
Safihre 8b7134a650 Config Search improvements 2017-01-06 14:36:12 +01:00
Safihre 1850f47c13 Remove lower() from Notifications Config page
Closes #732
2017-01-06 14:36:12 +01:00
Safihre 2fb44827b0 Add Config Search function 2016-12-27 22:09:53 +01:00
Safihre f0683e101b Combine RSS feeds to share filters
Closes #680
2016-12-27 22:09:53 +01:00
Safihre e2fda89ab3 Account for menu-offset when jumping to Config items directly 2016-12-27 22:09:53 +01:00
Safihre a8b3f721c9 Fix cherrypy_logging
Closes #758
2016-12-27 22:09:53 +01:00
Safihre 5948d6cf33 Use season/episode information from Newznab and store results
Getting it from the feed is much faster (50x)
2016-12-24 22:17:27 +01:00
Safihre 17b71bc7fb Use Newznab 'usenetdate' to determine age
RSS publish date is usually behind
2016-12-24 22:17:27 +01:00
Safihre dd5fe708ae Don't re-download RSS feed when clicking Apply Filters 2016-12-24 22:17:27 +01:00
Safihre d705ccbe80 Add support for Newznab/nZEDd attributes to feedparser 2016-12-24 22:17:27 +01:00
Safihre b53f46e0a0 Adding URL using PP=Default would set PP to only Download
Because pp="" is not the same as pp=None, so the right PP-setting from the Default setting was never applied
2016-12-24 22:17:27 +01:00
Safihre b497d1ab9a Need new translations for number of functional texts
It also needs to make sense in other languages, not just English
2016-12-24 22:17:27 +01:00
Safihre 2f86292503 Don't check port twice if HTTPS is served on HTTP port 2016-12-24 22:17:27 +01:00
Safihre e804b157c8 Update text files for 1.2.0Beta1 2016-12-24 22:17:27 +01:00
Safihre 0ac801d00d Catch IncompleteRead in URLGRabber
Closes #754
2016-12-24 22:17:27 +01:00
Safihre df6b9341ac Move UpdateCheck/AutoBrowser/CertificateCheck to General
That's where they belong
2016-12-24 22:17:27 +01:00
Safihre 749d18308b Catch more ways to correctly redirect after Config update 2016-12-24 22:17:27 +01:00
Safihre d16166c27a Make 'Security' section in Config General 2016-12-24 22:17:27 +01:00
Safihre edbd1e7e8f Add default Categories for new users
Same as newznab categories, so it's more clear for user's what is possible
2016-12-24 22:17:27 +01:00
Safihre 8855ce9414 Hide non-regular Server settings behind 'Advanced' button 2016-12-24 22:17:27 +01:00
Safihre 61b2a57925 Move 'Disable API-key' to Specials
Closes #755
2016-12-24 22:17:27 +01:00
shypike f8d6286d2a Manual update of NL translation 2016-12-24 18:59:36 +01:00
shypike 389e8b6a48 Update translations 2016-12-24 18:32:34 +01:00
Safihre ae39e754b0 Fixes for new OSX building 2016-12-19 21:48:44 +01:00
Will 3b398a7643 Fixes Tiny 1 Letter Typo (#752)
Fixes the tiniest of a Typo

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

Closes #733
2016-11-25 15:06:31 +01:00
Safihre 2d091568f3 no_penalties now applies to all penalties
Closes #734
2016-11-25 15:06:31 +01:00
Safihre a449b4b001 work_dir should not end on .par(2)
Otherwise par2cmdline gets angry.
2016-11-25 15:06:31 +01:00
Safihre aa71dc1103 Improve texts for External access settings
Closes #729
2016-11-25 15:06:31 +01:00
shypike 0b0ff448d5 Update translations 2016-11-10 22:36:00 +01:00
shypike b08e01f1c3 8th parameter for user-script wasn't passed correctly. 2016-11-03 10:51:26 +01:00
Safihre fe0439382e Change tabs to 4 spaces in Config/Glitter 2016-11-03 09:28:36 +01:00
Safihre bf89714926 Clean-up all par2 of a set
Closes #724
2016-11-03 09:28:36 +01:00
Safihre 25a9f9823d par2cmdline on Windows also needs -N 2016-11-03 09:28:36 +01:00
Safihre bfb23b1dd5 RarFile needs shortened and de-unicoded filepaths
Normally we do this in build_command, but RarFile doesn't.
2016-11-03 09:28:36 +01:00
Safihre f814f4cd8f QuickCheck would fail unicode files 2016-11-03 09:28:36 +01:00
Safihre 83ada782b8 Only shorten pathname on Windows for par2
Otherwise unicode paths are misformed and not detected. This is also how we do it for unrar.
2016-11-03 09:28:36 +01:00
Safihre 09ae033196 Unicode failed downloads are seen as Orphans due to xml_encoding
Need to force use of unicode, otherwise it incorrectly says they are orphaned
2016-11-03 09:28:36 +01:00
Safihre ad1a8c302c Improve HTTPS on Config->General and improve Duplicates text 2016-11-03 09:28:36 +01:00
Safihre c2f861146b Reduce Notifications template even more 2016-11-03 09:28:36 +01:00
Safihre 48ac57281c Significantly simplify Notifications template
They all have the same options..
2016-11-03 09:28:36 +01:00
Safihre 88f557f829 Update Categories page and texts 2016-11-03 09:28:36 +01:00
Safihre 8e0dae4d51 Implement user-sorting of Categories 2016-11-03 09:28:36 +01:00
Safihre bfdd8e840d Adapt Glitter so it's not overwriting Categories 2016-11-03 09:28:36 +01:00
shypike e1039f52e5 Improve category matching
When no other matches, try partial match of category name and indexer category.
Add comments and improve code.
2016-11-03 09:28:36 +01:00
shypike a3e7076073 Update translations 2016-11-02 23:17:47 +01:00
shypike 3e2bded3c3 Update main POT file. 2016-11-02 23:16:50 +01:00
Safihre a2a851d17f Detect and use all obfuscated par2 files
Using a trick to get handle_par2() to pick them up correctly.
2016-10-27 19:45:14 +01:00
Safihre 580e25de6d Use NZF's to do obfuscated check 2016-10-27 19:45:14 +01:00
Jonathon Saine 658544c305 When no parset defined (possible obfuscated), check for par2 signature.. rename first match. Repair, then repeat logic until all parsets have been processed.. then continue with processing as before. 2016-10-27 19:45:14 +01:00
Jonathon Saine a872a2d2ee spelling fixes 2016-10-27 19:45:14 +01:00
Safihre b1c5a28241 cryptography also nessecary to let RarFile open encrypted files 2016-10-27 19:45:21 +02:00
Safihre df8046287e Check all passwords on first rar for definitive check
Closes #185
2016-10-27 19:45:21 +02:00
Safihre 627f998d25 Implement verification using unrar, when par2 and sfv not available
Closes #621
2016-10-27 19:45:21 +02:00
Safihre 52a30f5804 Log the executed command for Unzip and 7Zip 2016-10-27 19:45:21 +02:00
Safihre b910574cdb Combine Encrypted/Unwanted files check 2016-10-27 19:45:21 +02:00
Safihre de6d642b0d Adapt new rarfile for usage within SABnzbd
Closes #594
2016-10-27 19:45:21 +02:00
Safihre 7eb9dddf22 Update RarFile 2016-10-27 19:45:21 +02:00
Safihre 47b5f25dcf Merge pull request #711 from sabnzbd/bugfixes
Bugfixes and new features
2016-10-21 15:21:27 +02:00
Safihre f5bb7d3cbe Glitter didn't allow removal of a set job-password
Closes #720
2016-10-20 11:04:06 +02:00
Safihre b4a1d72013 Servertest broke on SSLError
For example when user had selected a bad cipher
2016-10-20 09:10:08 +02:00
Safihre 2faf951592 Revert UnRar licence to original 2016-10-20 08:58:39 +02:00
Safihre 063b2bcc5f Correct RegEx for par2-file detection 2016-10-19 15:26:16 +02:00
Safihre fc2f527b15 Move SSL Ciphers option to Switches page
People should know it's possible. We put the explenation on the Wiki!
2016-10-19 10:35:29 +02:00
Safihre 47fa33a2a2 Fix on how aborted Par2/Unrar is reported 2016-10-14 15:32:27 +02:00
Safihre 6731d7be59 Enable certificate validation by default
When user has incorrect setup, the connect-to-well-known-host check will disable it anyway.
Closes #465 and #468
2016-10-14 15:03:25 +02:00
Safihre 91ce734d32 Cleanup of whitespace in Glitter/Config/wizzard 2016-10-14 14:54:58 +02:00
Safihre b89394c819 Allow aborting of Par2/Unrar (Glitter only)
Closes #703
2016-10-14 14:48:53 +02:00
Safihre 2732edce09 Allow also "vol01-03.par" on top of "vol01+03.par" 2016-10-14 14:37:41 +02:00
Safihre 68f1b9234b Make sure we show results when less than 1 page
Closes #710
2016-10-14 08:42:16 +02:00
Safihre 81497e19ce Improve text on Delete-page button 2016-10-14 08:42:16 +02:00
Safihre a0d3f8aa86 Update 7zip for Windows to 16.04 2016-10-14 08:42:16 +02:00
Safihre 4074ae3271 Update 7zip for macOS to 16.02
Best available for macOS
2016-10-14 08:42:16 +02:00
Safihre 7b197d93bc Update UnRar to 5.40 for Windows and Mac 2016-10-14 08:42:11 +02:00
Safihre 6b5cca4bb9 Avoid duplicates in build_filelists() that break 7zip support
Closes #706
2016-10-12 11:06:51 +02:00
Safihre 62971c0fd0 Remove Solaris Manifest
We don't really support it, nor does it seem in high demand
2016-10-12 10:18:47 +02:00
Safihre 8007d5a5d6 Remove Dockerfile (beter available from https://hub.docker.com) 2016-10-11 22:25:33 +02:00
Safihre eb9421255d Check for Retry-After header when fetching fails
Closes #707
2016-10-11 09:25:17 +02:00
Safihre 8e97097dfd Only allow binding to IPv6 when ipv6_hosting enabled
For some reason it was always binding to IPv6, even when disabled (on Windows)
2016-10-10 15:59:38 +02:00
Safihre b6972db5a7 CherryPy update long ago broke correct HTTPS port binding
The attach_server() could only bind HTTP ports because some change in CherryPy. Resulting of serving of HTTP on HTTPS port when set to localhost.
2016-10-10 15:31:07 +02:00
Safihre 755d904136 SSL handshake is performed automatically on connect() 2016-10-10 13:07:04 +02:00
Safihre 1052208d9e Verify the quality of certificate validation 2016-10-10 11:58:04 +02:00
Safihre e259f624ad Detect when par2cmdline needs -N
Closes #705
2016-10-10 09:01:04 +02:00
Safihre a77483ee31 Fix retry_all API-call 2016-10-09 17:55:58 +02:00
Safihre 201181ee6f Tiny change to Sorting debug logging
Closes #671
2016-10-07 14:42:41 +02:00
Safihre 2968b3c30e Change par2-classic to par2cmdline on Windows
Closes #702
2016-10-07 14:42:41 +02:00
Safihre 49a9fc7682 Avoid javascript error in Servers page 2016-10-07 14:42:41 +02:00
Safihre 0ff0c63903 Updates to INSTALL/ISSUES/README
Closes #676
2016-10-07 14:42:41 +02:00
shypike 0af084b4eb Fix emailer by converting account data to UTF-8 (#700)
It seems that the used libraries cannot handle Unicode, but only UTF-8.
2016-10-06 15:14:43 +02:00
Safihre d75278b52b CSS fix for Config menu on mobile 2016-10-06 15:14:43 +02:00
Safihre cf94729830 "Max line speed" more clear, using a select 2016-10-06 15:14:43 +02:00
Safihre 15d3ad5a96 Don't error out on Retry database errors
In case the job is somehow already gone
2016-10-06 15:14:43 +02:00
Safihre bb231c7416 Fix ciphers input for older Python 2016-10-06 15:14:43 +02:00
shypike a75b93699c Update .gitignore 2016-10-04 13:38:45 +02:00
Safihre bf3e08868b Allow setting SSL cipher-string in Specials
For users who know what they are doing
2016-10-03 13:35:13 +02:00
Safihre db9db1981c Log when creating new certificates
Closes #695
2016-10-03 13:35:13 +02:00
Safihre 4dff051927 Show SSL protocol and cipher in status window 2016-10-03 13:35:13 +02:00
Safihre bb92059343 CherryPy 8.1.2 - LF in MIME records and don't crash on IPv6 in error msg
Even though it's against the protocol.
2016-10-03 13:35:13 +02:00
Safihre 1e72304c25 CherryPy 8.1.2 - Fix last SSL-bug 2016-10-03 13:35:13 +02:00
Safihre 692ed8fce8 CherryPy 8.1.2 - Update and set version 2016-10-03 13:35:13 +02:00
Safihre 6a12224b21 Don't choke when cryptography-module is not installed 2016-10-03 13:35:13 +02:00
Safihre e8c7155c69 Update text-files and fix small CSS mistake 2016-10-03 13:35:13 +02:00
Safihre a2e7e917d1 Custom error message when server doesn't speak SSL on specified port 2016-10-03 13:35:13 +02:00
Safihre c73baaa3d2 Make "Certificate validation" option for each server 2016-10-03 13:35:13 +02:00
Safihre 09924a0f9c Fallback to non-verified connection when SSLContext is not available
For example on Python <2.7.9
2016-10-03 13:35:13 +02:00
Safihre 2a9a28cdb4 Improve error when bad certificate 2016-10-03 13:35:13 +02:00
Safihre db6256c720 Use the correct hostname in case of happyeyeballs 2016-10-03 13:35:13 +02:00
Safihre 440a80c552 Generate certificates using Cryptography
Bye bye pyOpenSSL!
2016-10-03 13:35:13 +02:00
Safihre a0e5eae244 Add option to set verification level 2016-10-03 13:35:13 +02:00
Safihre 6346ad96e4 First catch SSL errors, then general socket errors 2016-10-03 13:35:13 +02:00
Safihre 886803112d Remove option to choose SSL-level 2016-10-03 13:35:13 +02:00
Safihre f277b8c534 Log SSL/TLS level and cipher after connecting 2016-10-03 13:35:13 +02:00
Safihre a8d8db3a10 Remove pyOpenSSL from Config 2016-10-03 13:35:13 +02:00
Safihre 21549ab842 Switch CherryPy to use Python ssl instead of PyOpenSSL 2016-10-03 13:35:13 +02:00
Safihre e8e2adcee0 Implement NNTPS using python ssl module
Removes need for PyOpenSSL for NNTPS connections, does require OpenSSL to be installed. This is the case on most modern systems (Linux/Windows/OSX), according to the Python docs!
2016-10-03 13:35:13 +02:00
345 changed files with 60126 additions and 51897 deletions

No files matched your search

+2 -5
View File
@@ -10,17 +10,14 @@ srcdist/
# Generated email templates # Generated email templates
email/*.tmpl email/*.tmpl
# Romanian ro.po is generated from ro.px, due to mapping to latin-1
ro.po
# Build results # Build results
SABnzbd*.zip SABnzbd*.zip
SABnzbd*.exe SABnzbd*.exe
SABnzbd*.gz SABnzbd*.gz
SABnzbd*.dmg SABnzbd*.dmg
# WingIDE project file # WingIDE project files
*.wpr *.wp[ru]
# General junk # General junk
*.keep *.keep
+1 -4
View File
@@ -1,5 +1,5 @@
******************************************* *******************************************
*** This is SABnzbd 1.1.x *** *** This is SABnzbd 2.2.0 ***
******************************************* *******************************************
SABnzbd is an open-source cross-platform binary newsreader. SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically, It simplifies the process of downloading from Usenet dramatically,
@@ -12,9 +12,6 @@ and offers a complete API for third-party applications to hook into.
There is an extensive Wiki on the use of SABnzbd. There is an extensive Wiki on the use of SABnzbd.
https://sabnzbd.org/wiki/ https://sabnzbd.org/wiki/
IMPORTANT INFORMATION about release 1.0.0:
https://sabnzbd.org/wiki/introducing-1-0
Please also read the file "ISSUES.txt" Please also read the file "ISSUES.txt"
The organization of the download queue is different from 0.7.x (and older). The organization of the download queue is different from 0.7.x (and older).
+1 -1
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: The SABnzbd-team is:
-18
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" ]
+29 -34
View File
@@ -1,10 +1,10 @@
SABnzbd 1.1.0 SABnzbd 2.3.0
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
0) LICENSE 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 This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License modify it under the terms of the GNU General Public License
@@ -21,85 +21,80 @@ along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 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. Just run the downloaded EXE file and the installer will start.
It's just a simple standard installer. It's just a simple standard installer.
After installation, find the SABnzbd program in the Start menu and start it. After installation, find the SABnzbd program in the Start menu and start it.
Within 5-10 seconds your web browser will start and show the user interface. Within a few 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. 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. Unzip pre-built version to any folder of your liking.
Start the SABnzbd.exe program. Start the SABnzbd.exe program.
Within 5-10 seconds your web browser will start and show the user interface. Within a few 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. 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. Download the DMG file, mount and drag the SABnzbd icon to Programs.
Just like you do with so many apps. Just like you do with so many apps.
Make sure you pick the right folder, depending on your OSX version.
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
4) INSTALL with only sources 4) INSTALL with only sources
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
Specific guides to install from source are available for Windows and macOS:
https://sabnzbd.org/wiki/installation/install-macos
https://sabnzbd.org/wiki/installation/install-from-source-windows
You need to have Python installed plus some non-standard Python modules You need to have Python installed plus some non-standard Python modules
and a few tools. and a few tools.
Unix/Linux/OSX All platforms
Python-2.7 http://www.python.org (2.7.9+ recommended) Python-2.7.latest http://www.python.org (2.7.9+ recommended)
OSX Mavericks or newer
Apple Python 2.7 Included in OSX (default)
Windows Windows
Python-2.7.latest http://www.python.com
Most versions will work: 2.7.10+ recommended
PyWin32 use "pip install pypiwin32" PyWin32 use "pip install pypiwin32"
Essential modules Essential modules
cheetah-2.0.1+ use "pip install cheetah" cheetah-2.0.1+ use "pip install cheetah"
par2cmdline >= 0.4 http://parchive.sourceforge.net/ par2cmdline >= 0.4 https://github.com/Parchive/par2cmdline/releases
See also: https://sabnzbd.org/wiki/installation/multicore-par2
unrar >= 5.00+ http://www.rarlab.com/rar_add.htm unrar >= 5.00+ http://www.rarlab.com/rar_add.htm
openssl >= 1.0.0 http://www.openssl.org/
Optional modules Optional modules
unzip >= 6.00 http://www.info-zip.org/ unzip >= 6.00 http://www.info-zip.org/
7zip >= 9.20 http://www.7zip.org/ 7zip >= 9.20 http://www.7zip.org/
yenc module >= 0.3 http://sabnzbd.sourceforge.net/yenc-0.3.tar.gz sabyenc == 3.0.2 use "pip install sabyenc"
http://sabnzbd.sourceforge.net/yenc-0.3-w32fixed.zip (Win32-only) More information: https://sabnzbd.org/sabyenc
cryptography >= 1.0 use "pip install cryptography"
Enables certificate generation and detection of encrypted RAR-files
Optional modules Windows Optional modules Linux
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
pynotify Should be part of GTK for Python support on Debian/Ubuntu pynotify Should be part of GTK for Python support on Debian/Ubuntu
If not, you cannot use the NotifyOSD feature. 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) Embedded modules (preferably use the included version)
CherryPy-6.0.2 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. Unpack the ZIP-file containing the SABnzbd sources to any folder of your liking.
If you want multiple languages, you need to compile the translations. If you want multiple languages, you need to compile the translations.
Start this from a shell terminal (or command prompt): Start this from a shell terminal (or command prompt):
python tools/make_mo.py python tools/make_mo.py
Start this from a shell terminal (or command prompt): Start this from a shell terminal (or command prompt):
python SABnzbd.py python SABnzbd.py
Within 5-10 seconds your web browser will start and show the user interface.
Within a few 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. Use the "Help" button in the web-interface to be directed to the Help Wiki.
@@ -117,7 +112,7 @@ or
You may of course try other port numbers too. You may of course try other port numbers too.
For troubleshooting you can use the program SABnzbd-console.exe. For troubleshooting on Windows you can use the program SABnzbd-console.exe.
This will show a black window where logging information will be shown. This This will show a black window where logging information will be shown. This
may help you solve problems easier. may help you solve problems easier.
@@ -125,7 +120,7 @@ may help you solve problems easier.
6) MORE INFORMATION 6) MORE INFORMATION
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
Visit the WIKI site: Visit our wiki:
https://sabnzbd.org/wiki/ https://sabnzbd.org/wiki/
@@ -135,4 +130,4 @@ Visit the WIKI site:
Several parts of SABnzbd were built by other people, illustrating the Several parts of SABnzbd were built by other people, illustrating the
wonderful world of Free Open Source Software. 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.
+5 -5
View File
@@ -12,7 +12,7 @@
Windows-only: Windows-only:
If you keep having trouble with par2 multicore you can disable it If you keep having trouble with par2 multicore you can disable it
in Config->Switches. 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 - A bug in Windows 7 may cause severe memory leaks when you use SABnzbd in
combination with some virus scanners and firewalls. combination with some virus scanners and firewalls.
@@ -24,13 +24,13 @@
For these the server blocking method is not very favourable. For these the server blocking method is not very favourable.
There is an INI-only option that will limit blocks to 1 minute. There is an INI-only option that will limit blocks to 1 minute.
no_penalties = 1 no_penalties = 1
See: https://sabnzbd.org/wiki/configuration/1.0/special See: https://sabnzbd.org/wiki/configuration/2.2/special
- Some third-party utilties try to probe SABnzbd API in such a way that you will - Some third-party utilties try to probe SABnzbd API in such a way that you will
often see warnings about unauthenticated access. often see warnings about unauthenticated access.
If you are sure these probes are harmless, you can suppress the warnings by If you are sure these probes are harmless, you can suppress the warnings by
setting the option "api_warnings" to 0. setting the option "api_warnings" to 0.
See: https://sabnzbd.org/wiki/configuration/1.0/special See: https://sabnzbd.org/wiki/configuration/2.2/special
- On OSX you may encounter downloaded files with foreign characters. - On OSX you may encounter downloaded files with foreign characters.
The par2 repair may fail when the files were created on a Windows system. 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 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). 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: Possible the file system override setting 'fsys_type' might be solve things:
See: https://sabnzbd.org/wiki/configuration/1.0/special See: https://sabnzbd.org/wiki/configuration/2.2/special
- The "Watched Folder" sometimes fails to delete the NZB files it has - The "Watched Folder" sometimes fails to delete the NZB files it has
processed. This happens when other software still accesses these files. processed. This happens when other software still accesses these files.
@@ -81,4 +81,4 @@
- Squeeze Linux - Squeeze Linux
There is a "special" option that will allow you to select an alternative library. There is a "special" option that will allow you to select an alternative library.
use_pickle = 1 use_pickle = 1
See: https://sabnzbd.org/wiki/configuration/1.0/special See: https://sabnzbd.org/wiki/configuration/2.2/special
+1 -1
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 This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License modify it under the terms of the GNU General Public License
+3 -3
View File
@@ -1,8 +1,8 @@
Metadata-Version: 1.0 Metadata-Version: 1.0
Name: SABnzbd Name: SABnzbd
Version: 1.2.0 Version: 2.3.0Alpha2
Summary: SABnzbd-1.2.0 Summary: SABnzbd-2.3.0Alpha1
Home-page: http://sabnzbd.org Home-page: https://sabnzbd.org
Author: The SABnzbd Team Author: The SABnzbd Team
Author-email: team@sabnzbd.org Author-email: team@sabnzbd.org
License: GNU General Public License 2 (GPL2 or later) License: GNU General Public License 2 (GPL2 or later)
+25 -35
View File
@@ -1,37 +1,28 @@
SABnzbd - The automated Usenet download tool 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. SABnzbd is an Open Source Binary Newsreader written in Python.
It's totally free, incredibly easy to use, and works practically everywhere. 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: https://sabnzbd.org.
If you want to know more you can head over to our website: http://sabnzbd.org.
## Resolving Dependencies ## 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` (only 2.7.9 and higher, but not 3.x.x) - `python` (only 2.7.x and higher, but not 3.x.x)
- `python-cheetah` - `python-cheetah`
- `python-dbus`
- `python-openssl`
- `python-support` - `python-support`
- `python-yenc` - `par2` (Multi-threaded par2 installation guide can be found [here](https://sabnzbd.org/wiki/installation/multicore-par2))
- `par2` (Multi-threaded par2 installation guide can be found [here](https://forums.sabnzbd.org/viewtopic.php?f=16&t=18793#p99702) )
- `unrar` (Make sure you get the "official" non-free version of unrar) - `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` - `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). 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: 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: 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: If you want multi-language support, run:
@@ -61,18 +52,17 @@ Our many other command line options are explained in depth [here](https://sabnzb
The workflow we use, is a simplified form of "GitFlow". The workflow we use, is a simplified form of "GitFlow".
Basically: Basically:
- "master" contains only stable releases (which have been merged to "master") - `master` contains only stable releases (which have been merged to `master`) and is intended for end-users.
- "develop" is the target for integration - `develop` is the target for integration and is **not** intended for end-users.
- "1.0.x" is a release and maintenance branch for 1.0.x: 1.0.0 -> 1.0.1 -> 1.0.2 - `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.
- "1.1.x" is a release and maintenance branch for 1.1.x: 1.1.0 -> 1.1.1 -> 1.1.2 - `feature/my_feature` is a temporary feature branch based on `develop`.
- "feature/my_feature" is a temporary feature branch - `bugfix/my_bugfix` is an optional temporary branch for bugfix(es) based on `develop`.
- "hotfix/my_hotfix is an optional temporary branch for bugfix(es)
Condtions: Conditions:
- Merging of a stable release into "master" will be simple: the release branch is always right. - Merging of a stable release into `master` will be simple: the release branch is always right.
- "master" is not merged back to "develop" - `master` is not merged back to `develop`.
- "develop" is not re-based on "master". - `develop` is not re-based on `master`.
- Release branches branch from "develop" only. - 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 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. - 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. - We will not release a 1.0.2 if a 1.1.0 has already been released.
+57 -30
View File
@@ -1,40 +1,67 @@
Release Notes - SABnzbd 1.2.0 Release Notes - SABnzbd 2.3.0 Alpha 2
=============================== =========================================================
## What's new in 1.2.0 ## Changes and bugfixes since 2.3.0 Alpha 1
- Specials Config page could not be loaded
- Crash when adding new jobs
- Further stalling-detection improvements
- Crash when a job was set to only Download
- Display of download progress and missing data improved
- Retried jobs would show incorrect download progress
- Different icon for downloads with Force priority
- Show progress during verification of extra files
- API: 'missing' field removed, use 'mbmissing'
## Changes: ## Changes since 2.2.1
- Option to limit Servers to specific Categories removed
- Improved par2 handling and obfuscated files detection
- Duplicate filenames in NZB's no longer rejected by default
- Set custom URL instead of /sabnzbd/ (in Config > Specials)
- Article-state (which servers are tried) is preserved after restart
- Auto disconnect (if enabled) only after verification of last item
- Slight performance improvement when fetching RSS-feeds
- RSS-feed title is shown for URLs being grabbed
- URL grabbing can now be individually paused
- Scheduler can pause/unpause jobs in specific category
- Series Duplicate Checker can allow PROPER/REAL/REPACK versions
- Refresh-icon in Glitter when refresh rate is above 2 seconds
- macOS: Bundled new OpenSSL version with support for TLSv1.2
- macOS: No longer linked to system certificate store
- macOS and Windows: Installers include Mozilla CA certificates
## Bugfixes since 2.2.1
- Reduce CPU usage with multiple servers
- Fix yet another potential stalling issue
- Remove Timeout tracebacks
- Only warn if number of actual passwords is larger than 30
- Unexpected behavior when diskspace becomes critically low
- MacOS: Direct Unpack could hang in case of special charters in names
- API: Correct listing of downloaded and queued files in get_files
- API: Jobs with Force priority should always have status 'Downloading'
## Bug fixes ## Upgrading from 2.1.x and older
## 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+
<https://sabnzbd.org/wiki/introducing-1-0>
### Known problems and solutions
- Read the file "ISSUES.txt"
### Upgrading from 0.7.x and older
- Finish queue - Finish queue
- Stop SABnzbd - Stop SABnzbd
- Install new version - Install new version
- Start SABnzbd - Start SABnzbd
The organization of the download queue is different from older versions. ## Upgrade notices
1.0.x will not see the existing queue, but you can go to - When upgrading from 2.1.0 or older the queue will be converted. Job order,
Status->QueueRepair and "Repair" the old queue. settings and data will be preserved, but all jobs will be unpaused and
Also, your sabnzbd.ini file will be upgraded, making it URL's that did not finish fetching before the upgrade will be lost.
incompatible with releases older than 0.7.9 - The organization of the download queue is different from 0.7.x releases.
This version will not see the 0.7.x queue, but you can restore the jobs
by going to Status page and using Queue Repair.
## Known problems and solutions
- Read the file "ISSUES.txt"
## 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\>
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/python -OO #!/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 # This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License # modify it under the terms of the GNU General Public License
+230 -386
View File
File diff suppressed because it is too large. Load diff
-25
View File
@@ -1,25 +0,0 @@
Copyright (c) 2004-2016, 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.
+2 -1
View File
@@ -1,4 +1,5 @@
CherryPy 6.0.2 Official distribution: https://pypi.python.org/packages/source/C/CherryPy/CherryPy-6.0.2.tar.gz CherryPy 8.1.2
Official distribution: https://github.com/cherrypy/cherrypy/releases
The folders 'tutorial', 'test' and 'scaffold' have been removed. The folders 'tutorial', 'test' and 'scaffold' have been removed.
This file has been added. This file has been added.
+27 -31
View File
@@ -61,23 +61,26 @@ try:
except ImportError: except ImportError:
pass pass
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect from threading import local as _local
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
from cherrypy import _cpdispatch as dispatch from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect # noqa
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError # noqa
from cherrypy import _cplogging
from cherrypy import _cpdispatch as dispatch # noqa
from cherrypy import _cptools from cherrypy import _cptools
tools = _cptools.default_toolbox from cherrypy._cptools import default_toolbox as tools, Tool
Tool = _cptools.Tool
from cherrypy import _cprequest from cherrypy import _cprequest
from cherrypy.lib import httputil as _httputil from cherrypy.lib import httputil as _httputil
from cherrypy import _cptree from cherrypy import _cptree
tree = _cptree.Tree() from cherrypy._cptree import Application # noqa
from cherrypy._cptree import Application from cherrypy import _cpwsgi as wsgi # noqa
from cherrypy import _cpwsgi as wsgi
from cherrypy import _cpserver
from cherrypy import process from cherrypy import process
try: try:
from cherrypy.process import win32 from cherrypy.process import win32
@@ -88,11 +91,11 @@ except ImportError:
engine = process.bus engine = process.bus
try: tree = _cptree.Tree()
__version__ = pkg_resources.require('cherrypy')[0].version
except Exception:
__version__ = 'unknown' __version__ = '8.1.2'
__version__ = '6.0.2'
# Timeout monitor. We add two channels to the engine # Timeout monitor. We add two channels to the engine
# to which cherrypy.Application will publish. # to which cherrypy.Application will publish.
@@ -141,20 +144,19 @@ class _HandleSignalsPlugin(object):
def subscribe(self): def subscribe(self):
"""Add the handlers based on the platform""" """Add the handlers based on the platform"""
if hasattr(self.bus, "signal_handler"): if hasattr(self.bus, 'signal_handler'):
self.bus.signal_handler.subscribe() 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() self.bus.console_control_handler.subscribe()
engine.signals = _HandleSignalsPlugin(engine) engine.signals = _HandleSignalsPlugin(engine)
from cherrypy import _cpserver
server = _cpserver.Server() server = _cpserver.Server()
server.subscribe() 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. """Mount the given root, start the builtin server (and engine), then block.
root: an instance of a "controller class" (a collection of page handler root: an instance of a "controller class" (a collection of page handler
@@ -181,9 +183,6 @@ def quickstart(root=None, script_name="", config=None):
engine.block() engine.block()
from cherrypy._cpcompat import threadlocal as _local
class _Serving(_local): class _Serving(_local):
"""An interface for registering request and response objects. """An interface for registering request and response objects.
@@ -196,8 +195,8 @@ class _Serving(_local):
thread-safe way. thread-safe way.
""" """
request = _cprequest.Request(_httputil.Host("127.0.0.1", 80), request = _cprequest.Request(_httputil.Host('127.0.0.1', 80),
_httputil.Host("127.0.0.1", 1111)) _httputil.Host('127.0.0.1', 1111))
""" """
The request object for the current thread. In the main thread, The request object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None.""" and any threads which are not receiving HTTP requests, this is None."""
@@ -230,7 +229,7 @@ class _ThreadLocalProxy(object):
return getattr(child, name) return getattr(child, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name in ("__attrname__", ): if name in ('__attrname__', ):
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
else: else:
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
@@ -306,9 +305,6 @@ except ImportError:
pass pass
from cherrypy import _cplogging
class _GlobalLogManager(_cplogging.LogManager): class _GlobalLogManager(_cplogging.LogManager):
"""A site-wide LogManager; routes to app.log or global log as appropriate. """A site-wide LogManager; routes to app.log or global log as appropriate.
@@ -352,10 +348,10 @@ def _buslog(msg, level):
log.error(msg, 'ENGINE', severity=level) log.error(msg, 'ENGINE', severity=level)
engine.subscribe('log', _buslog) engine.subscribe('log', _buslog)
from cherrypy._helper import expose, popargs, url from cherrypy._helper import expose, popargs, url # noqa
# import _cpconfig last so it can reference other top-level objects # 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 # Use _global_conf_alias so quickstart can use 'config' as an arg
# without shadowing cherrypy.config. # without shadowing cherrypy.config.
config = _global_conf_alias = _cpconfig.Config() config = _global_conf_alias = _cpconfig.Config()
@@ -365,11 +361,11 @@ config.defaults = {
'tools.trailing_slash.on': True, 'tools.trailing_slash.on': True,
'tools.encode.on': True 'tools.encode.on': True
} }
config.namespaces["log"] = lambda k, v: setattr(log, k, v) config.namespaces['log'] = lambda k, v: setattr(log, k, v)
config.namespaces["checker"] = lambda k, v: setattr(checker, k, v) config.namespaces['checker'] = lambda k, v: setattr(checker, k, v)
# Must reset to get our defaults applied. # Must reset to get our defaults applied.
config.reset() config.reset()
from cherrypy import _cpchecker from cherrypy import _cpchecker # noqa
checker = _cpchecker.Checker() checker = _cpchecker.Checker()
engine.subscribe('start', checker) engine.subscribe('start', checker)
+73 -73
View File
@@ -33,7 +33,7 @@ class Checker(object):
warnings.formatwarning = self.formatwarning warnings.formatwarning = self.formatwarning
try: try:
for name in dir(self): for name in dir(self):
if name.startswith("check_"): if name.startswith('check_'):
method = getattr(self, name) method = getattr(self, name)
if method and hasattr(method, '__call__'): if method and hasattr(method, '__call__'):
method() method()
@@ -42,7 +42,7 @@ class Checker(object):
def formatwarning(self, message, category, filename, lineno, line=None): def formatwarning(self, message, category, filename, lineno, line=None):
"""Function to format a warning.""" """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. # This value should be set inside _cpconfig.
global_config_contained_paths = False global_config_contained_paths = False
@@ -57,13 +57,13 @@ class Checker(object):
continue continue
if sn == '': if sn == '':
continue continue
sn_atoms = sn.strip("/").split("/") sn_atoms = sn.strip('/').split('/')
for key in app.config.keys(): for key in app.config.keys():
key_atoms = key.strip("/").split("/") key_atoms = key.strip('/').split('/')
if key_atoms[:len(sn_atoms)] == sn_atoms: if key_atoms[:len(sn_atoms)] == sn_atoms:
warnings.warn( warnings.warn(
"The application mounted at %r has config " 'The application mounted at %r has config '
"entries that start with its script name: %r" % (sn, 'entries that start with its script name: %r' % (sn,
key)) key))
def check_site_config_entries_in_app_config(self): def check_site_config_entries_in_app_config(self):
@@ -76,17 +76,17 @@ class Checker(object):
for section, entries in iteritems(app.config): for section, entries in iteritems(app.config):
if section.startswith('/'): if section.startswith('/'):
for key, value in iteritems(entries): for key, value in iteritems(entries):
for n in ("engine.", "server.", "tree.", "checker."): for n in ('engine.', 'server.', 'tree.', 'checker.'):
if key.startswith(n): if key.startswith(n):
msg.append("[%s] %s = %s" % msg.append('[%s] %s = %s' %
(section, key, value)) (section, key, value))
if msg: if msg:
msg.insert(0, msg.insert(0,
"The application mounted at %r contains the " 'The application mounted at %r contains the '
"following config entries, which are only allowed " 'following config entries, which are only allowed '
"in site-wide config. Move them to a [global] " 'in site-wide config. Move them to a [global] '
"section and pass them to cherrypy.config.update() " 'section and pass them to cherrypy.config.update() '
"instead of tree.mount()." % sn) 'instead of tree.mount().' % sn)
warnings.warn(os.linesep.join(msg)) warnings.warn(os.linesep.join(msg))
def check_skipped_app_config(self): def check_skipped_app_config(self):
@@ -95,13 +95,13 @@ class Checker(object):
if not isinstance(app, cherrypy.Application): if not isinstance(app, cherrypy.Application):
continue continue
if not app.config: 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: if self.global_config_contained_paths:
msg += (" It looks like the config you passed to " msg += (' It looks like the config you passed to '
"cherrypy.config.update() contains application-" 'cherrypy.config.update() contains application-'
"specific sections. You must explicitly pass " 'specific sections. You must explicitly pass '
"application config via " 'application config via '
"cherrypy.tree.mount(..., config=app_config)") 'cherrypy.tree.mount(..., config=app_config)')
warnings.warn(msg) warnings.warn(msg)
return return
@@ -115,12 +115,12 @@ class Checker(object):
if not app.config: if not app.config:
continue continue
for key in app.config.keys(): for key in app.config.keys():
if key.startswith("[") or key.endswith("]"): if key.startswith('[') or key.endswith(']'):
warnings.warn( warnings.warn(
"The application mounted at %r has config " 'The application mounted at %r has config '
"section names with extraneous brackets: %r. " 'section names with extraneous brackets: %r. '
"Config *files* need brackets; config *dicts* " 'Config *files* need brackets; config *dicts* '
"(e.g. passed to tree.mount) do not." % (sn, key)) '(e.g. passed to tree.mount) do not.' % (sn, key))
def check_static_paths(self): def check_static_paths(self):
"""Check Application config for incorrect static paths.""" """Check Application config for incorrect static paths."""
@@ -132,47 +132,47 @@ class Checker(object):
request.app = app request.app = app
for section in app.config: for section in app.config:
# get_resource will populate request.config # get_resource will populate request.config
request.get_resource(section + "/dummy.html") request.get_resource(section + '/dummy.html')
conf = request.config.get conf = request.config.get
if conf("tools.staticdir.on", False): if conf('tools.staticdir.on', False):
msg = "" msg = ''
root = conf("tools.staticdir.root") root = conf('tools.staticdir.root')
dir = conf("tools.staticdir.dir") dir = conf('tools.staticdir.dir')
if dir is None: if dir is None:
msg = "tools.staticdir.dir is not set." msg = 'tools.staticdir.dir is not set.'
else: else:
fulldir = "" fulldir = ''
if os.path.isabs(dir): if os.path.isabs(dir):
fulldir = dir fulldir = dir
if root: if root:
msg = ("dir is an absolute path, even " msg = ('dir is an absolute path, even '
"though a root is provided.") 'though a root is provided.')
testdir = os.path.join(root, dir[1:]) testdir = os.path.join(root, dir[1:])
if os.path.exists(testdir): if os.path.exists(testdir):
msg += ( msg += (
"\nIf you meant to serve the " '\nIf you meant to serve the '
"filesystem folder at %r, remove the " 'filesystem folder at %r, remove the '
"leading slash from dir." % (testdir,)) 'leading slash from dir.' % (testdir,))
else: else:
if not root: if not root:
msg = ( msg = (
"dir is a relative path and " 'dir is a relative path and '
"no root provided.") 'no root provided.')
else: else:
fulldir = os.path.join(root, dir) fulldir = os.path.join(root, dir)
if not os.path.isabs(fulldir): if not os.path.isabs(fulldir):
msg = ("%r is not an absolute path." % ( msg = ('%r is not an absolute path.' % (
fulldir,)) fulldir,))
if fulldir and not os.path.exists(fulldir): if fulldir and not os.path.exists(fulldir):
if msg: if msg:
msg += "\n" msg += '\n'
msg += ("%r (root + dir) is not an existing " msg += ('%r (root + dir) is not an existing '
"filesystem path." % fulldir) 'filesystem path.' % fulldir)
if msg: if msg:
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r" warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r'
% (msg, section, root, dir)) % (msg, section, root, dir))
# -------------------------- Compatibility -------------------------- # # -------------------------- Compatibility -------------------------- #
@@ -198,19 +198,19 @@ class Checker(object):
if isinstance(conf, dict): if isinstance(conf, dict):
for k, v in conf.items(): for k, v in conf.items():
if k in self.obsolete: if k in self.obsolete:
warnings.warn("%r is obsolete. Use %r instead.\n" warnings.warn('%r is obsolete. Use %r instead.\n'
"section: [%s]" % 'section: [%s]' %
(k, self.obsolete[k], section)) (k, self.obsolete[k], section))
elif k in self.deprecated: elif k in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead.\n" warnings.warn('%r is deprecated. Use %r instead.\n'
"section: [%s]" % 'section: [%s]' %
(k, self.deprecated[k], section)) (k, self.deprecated[k], section))
else: else:
if section in self.obsolete: 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])) % (section, self.obsolete[section]))
elif section in self.deprecated: 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])) % (section, self.deprecated[section]))
def check_compatibility(self): def check_compatibility(self):
@@ -225,7 +225,7 @@ class Checker(object):
extra_config_namespaces = [] extra_config_namespaces = []
def _known_ns(self, app): def _known_ns(self, app):
ns = ["wsgi"] ns = ['wsgi']
ns.extend(copykeys(app.toolboxes)) ns.extend(copykeys(app.toolboxes))
ns.extend(copykeys(app.namespaces)) ns.extend(copykeys(app.namespaces))
ns.extend(copykeys(app.request_class.namespaces)) ns.extend(copykeys(app.request_class.namespaces))
@@ -233,32 +233,32 @@ class Checker(object):
ns += self.extra_config_namespaces ns += self.extra_config_namespaces
for section, conf in app.config.items(): 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): if is_path_section and isinstance(conf, dict):
for k, v in conf.items(): for k, v in conf.items():
atoms = k.split(".") atoms = k.split('.')
if len(atoms) > 1: if len(atoms) > 1:
if atoms[0] not in ns: if atoms[0] not in ns:
# Spit out a special warning if a known # Spit out a special warning if a known
# namespace is preceded by "cherrypy." # namespace is preceded by "cherrypy."
if atoms[0] == "cherrypy" and atoms[1] in ns: if atoms[0] == 'cherrypy' and atoms[1] in ns:
msg = ( msg = (
"The config entry %r is invalid; " 'The config entry %r is invalid; '
"try %r instead.\nsection: [%s]" 'try %r instead.\nsection: [%s]'
% (k, ".".join(atoms[1:]), section)) % (k, '.'.join(atoms[1:]), section))
else: else:
msg = ( msg = (
"The config entry %r is invalid, " 'The config entry %r is invalid, '
"because the %r config namespace " 'because the %r config namespace '
"is unknown.\n" 'is unknown.\n'
"section: [%s]" % (k, atoms[0], section)) 'section: [%s]' % (k, atoms[0], section))
warnings.warn(msg) warnings.warn(msg)
elif atoms[0] == "tools": elif atoms[0] == 'tools':
if atoms[1] not in dir(cherrypy.tools): if atoms[1] not in dir(cherrypy.tools):
msg = ( msg = (
"The config entry %r may be invalid, " 'The config entry %r may be invalid, '
"because the %r tool was not found.\n" 'because the %r tool was not found.\n'
"section: [%s]" % (k, atoms[1], section)) 'section: [%s]' % (k, atoms[1], section))
warnings.warn(msg) warnings.warn(msg)
def check_config_namespaces(self): def check_config_namespaces(self):
@@ -282,17 +282,17 @@ class Checker(object):
continue continue
vtype = type(getattr(obj, name, None)) vtype = type(getattr(obj, name, None))
if vtype in b: if vtype in b:
self.known_config_types[namespace + "." + name] = vtype self.known_config_types[namespace + '.' + name] = vtype
traverse(cherrypy.request, "request") traverse(cherrypy.request, 'request')
traverse(cherrypy.response, "response") traverse(cherrypy.response, 'response')
traverse(cherrypy.server, "server") traverse(cherrypy.server, 'server')
traverse(cherrypy.engine, "engine") traverse(cherrypy.engine, 'engine')
traverse(cherrypy.log, "log") traverse(cherrypy.log, 'log')
def _known_types(self, config): def _known_types(self, config):
msg = ("The config entry %r in section %r is of type %r, " msg = ('The config entry %r in section %r is of type %r, '
"which does not match the expected type %r.") 'which does not match the expected type %r.')
for section, conf in config.items(): for section, conf in config.items():
if isinstance(conf, dict): if isinstance(conf, dict):
@@ -326,7 +326,7 @@ class Checker(object):
for k, v in cherrypy.config.items(): for k, v in cherrypy.config.items():
if k == 'server.socket_host' and v == 'localhost': if k == 'server.socket_host' and v == 'localhost':
warnings.warn("The use of 'localhost' as a socket host can " 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 " "'localhost' can map to either an IPv4 or an "
"IPv6 address. You should use '127.0.0.1' " "IPv6 address. You should use '127.0.0.1' "
"or '[::1]' instead.") "or '[::1]' instead.")
+52 -59
View File
@@ -1,6 +1,6 @@
"""Compatibility code for using CherryPy with various versions of Python. """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 useful abstraction over the differences between Python versions, sometimes by
preferring a newer idiom, sometimes an older one, and sometimes a custom one. preferring a newer idiom, sometimes an older one, and sometimes a custom one.
@@ -15,6 +15,8 @@ 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 It also provides a 'base64_decode' function with native strings as input and
output. output.
""" """
import binascii
import os import os
import re import re
import sys import sys
@@ -23,8 +25,6 @@ import threading
import six import six
if six.PY3: if six.PY3:
basestring = (bytes, str)
def ntob(n, encoding='ISO-8859-1'): def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given """Return the given native string as a byte string in the given
encoding. encoding.
@@ -49,8 +49,6 @@ if six.PY3:
return n return n
else: else:
# Python 2 # Python 2
basestring = basestring
def ntob(n, encoding='ISO-8859-1'): def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given """Return the given native string as a byte string in the given
encoding. encoding.
@@ -90,7 +88,7 @@ else:
def assert_native(n): def assert_native(n):
if not isinstance(n, str): if not isinstance(n, str):
raise TypeError("n must be a native str (got %s)" % type(n).__name__) raise TypeError('n must be a native str (got %s)' % type(n).__name__)
try: try:
# Python 3.1+ # Python 3.1+
@@ -140,16 +138,11 @@ try:
from urllib.request import parse_http_list, parse_keqv_list from urllib.request import parse_http_list, parse_keqv_list
except ImportError: except ImportError:
# Python 2 # Python 2
from urlparse import urljoin from urlparse import urljoin # noqa
from urllib import urlencode, urlopen from urllib import urlencode, urlopen # noqa
from urllib import quote, quote_plus from urllib import quote, quote_plus # noqa
from urllib import unquote from urllib import unquote # noqa
from urllib2 import parse_http_list, parse_keqv_list from urllib2 import parse_http_list, parse_keqv_list # noqa
try:
from threading import local as threadlocal
except ImportError:
from cherrypy._cpthreadinglocal import local as threadlocal
try: try:
dict.iteritems dict.iteritems
@@ -186,7 +179,7 @@ try:
import builtins import builtins
except ImportError: except ImportError:
# Python 2 # Python 2
import __builtin__ as builtins import __builtin__ as builtins # noqa
try: try:
# Python 2. We try Python 2 first clients on Python 2 # Python 2. We try Python 2 first clients on Python 2
@@ -197,10 +190,10 @@ try:
from BaseHTTPServer import BaseHTTPRequestHandler from BaseHTTPServer import BaseHTTPRequestHandler
except ImportError: except ImportError:
# Python 3 # Python 3
from http.cookies import SimpleCookie, CookieError from http.cookies import SimpleCookie, CookieError # noqa
from http.client import BadStatusLine, HTTPConnection, IncompleteRead from http.client import BadStatusLine, HTTPConnection, IncompleteRead # noqa
from http.client import NotConnected from http.client import NotConnected # noqa
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler # noqa
# Some platforms don't expose HTTPSConnection, so handle it separately # Some platforms don't expose HTTPSConnection, so handle it separately
if six.PY3: if six.PY3:
@@ -222,29 +215,6 @@ except NameError:
# Python 3 # Python 3
xrange = range 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: try:
# Python 3 # Python 3
from urllib.parse import unquote as parse_unquote from urllib.parse import unquote as parse_unquote
@@ -291,15 +261,14 @@ finally:
else: else:
json_encode = _json_encode json_encode = _json_encode
text_or_bytes = six.text_type, six.binary_type
try: try:
import cPickle as pickle import cPickle as pickle
except ImportError: except ImportError:
# In Python 2, pickle is a Python version. # In Python 2, pickle is a Python version.
# In Python 3, pickle is the sped-up C version. # In Python 3, pickle is the sped-up C version.
import pickle import pickle # noqa
import binascii
def random20(): def random20():
return binascii.hexlify(os.urandom(20)).decode('ascii') return binascii.hexlify(os.urandom(20)).decode('ascii')
@@ -307,7 +276,7 @@ def random20():
try: try:
from _thread import get_ident as get_thread_ident from _thread import get_ident as get_thread_ident
except ImportError: except ImportError:
from thread import get_ident as get_thread_ident from thread import get_ident as get_thread_ident # noqa
try: try:
# Python 3 # Python 3
@@ -325,17 +294,41 @@ else:
Timer = threading._Timer Timer = threading._Timer
Event = threading._Event Event = threading._Event
# Prior to Python 2.6, the Thread class did not have a .daemon property. try:
# This mix-in adds that property. # 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 args
return self.isDaemon()
def __set_daemon(self, daemon):
self.setDaemon(daemon)
if sys.version_info < (2, 6):
daemon = property(__get_daemon, __set_daemon)
File diff suppressed because it is too large. Load diff
+27 -53
View File
@@ -119,7 +119,7 @@ style) context manager.
""" """
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import reprconf from cherrypy.lib import reprconf
# Deprecated in CherryPy 3.2--remove in 3.3 # 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 If the given config is a filename, it will be appended to
the list of files to monitor for "autoreload" changes. 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) cherrypy.engine.autoreload.files.add(other)
# Load other into base # Load other into base
for section, value_map in reprconf.as_dict(other).items(): for section, value_map in reprconf.as_dict(other).items():
if not isinstance(value_map, dict): if not isinstance(value_map, dict):
raise ValueError( 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. " "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}.") "headers, for example: {'/': config}.")
base.setdefault(section, {}).update(value_map) base.setdefault(section, {}).update(value_map)
@@ -152,19 +152,19 @@ class Config(reprconf.Config):
def update(self, config): def update(self, config):
"""Update self from a dict, file or filename.""" """Update self from a dict, file or filename."""
if isinstance(config, basestring): if isinstance(config, text_or_bytes):
# Filename # Filename
cherrypy.engine.autoreload.files.add(config) cherrypy.engine.autoreload.files.add(config)
reprconf.Config.update(self, config) reprconf.Config.update(self, config)
def _apply(self, config): def _apply(self, config):
"""Update self from a dict.""" """Update self from a dict."""
if isinstance(config.get("global"), dict): if isinstance(config.get('global'), dict):
if len(config) > 1: if len(config) > 1:
cherrypy.checker.global_config_contained_paths = True cherrypy.checker.global_config_contained_paths = True
config = config["global"] config = config['global']
if 'tools.staticdir.dir' in config: if 'tools.staticdir.dir' in config:
config['tools.staticdir.section'] = "global" config['tools.staticdir.section'] = 'global'
reprconf.Config._apply(self, config) reprconf.Config._apply(self, config)
@staticmethod @staticmethod
@@ -172,8 +172,8 @@ class Config(reprconf.Config):
"""Decorator for page handlers to set _cp_config.""" """Decorator for page handlers to set _cp_config."""
if args: if args:
raise TypeError( raise TypeError(
"The cherrypy.config decorator does not accept positional " 'The cherrypy.config decorator does not accept positional '
"arguments; you must use keyword arguments.") 'arguments; you must use keyword arguments.')
def tool_decorator(f): def tool_decorator(f):
_Vars(f).setdefault('_cp_config', {}).update(kwargs) _Vars(f).setdefault('_cp_config', {}).update(kwargs)
@@ -197,14 +197,14 @@ class _Vars(object):
# Sphinx begin config.environments # Sphinx begin config.environments
Config.environments = environments = { Config.environments = environments = {
"staging": { 'staging': {
'engine.autoreload.on': False, 'engine.autoreload.on': False,
'checker.on': False, 'checker.on': False,
'tools.log_headers.on': False, 'tools.log_headers.on': False,
'request.show_tracebacks': False, 'request.show_tracebacks': False,
'request.show_mismatched_params': False, 'request.show_mismatched_params': False,
}, },
"production": { 'production': {
'engine.autoreload.on': False, 'engine.autoreload.on': False,
'checker.on': False, 'checker.on': False,
'tools.log_headers.on': False, 'tools.log_headers.on': False,
@@ -212,7 +212,7 @@ Config.environments = environments = {
'request.show_mismatched_params': False, 'request.show_mismatched_params': False,
'log.screen': False, 'log.screen': False,
}, },
"embedded": { 'embedded': {
# For use with CherryPy embedded in another deployment stack. # For use with CherryPy embedded in another deployment stack.
'engine.autoreload.on': False, 'engine.autoreload.on': False,
'checker.on': False, 'checker.on': False,
@@ -223,7 +223,7 @@ Config.environments = environments = {
'engine.SIGHUP': None, 'engine.SIGHUP': None,
'engine.SIGTERM': None, 'engine.SIGTERM': None,
}, },
"test_suite": { 'test_suite': {
'engine.autoreload.on': False, 'engine.autoreload.on': False,
'checker.on': False, 'checker.on': False,
'tools.log_headers.on': False, 'tools.log_headers.on': False,
@@ -237,11 +237,11 @@ Config.environments = environments = {
def _server_namespace_handler(k, v): def _server_namespace_handler(k, v):
"""Config handler for the "server" namespace.""" """Config handler for the "server" namespace."""
atoms = k.split(".", 1) atoms = k.split('.', 1)
if len(atoms) > 1: if len(atoms) > 1:
# Special-case config keys of the form 'server.servername.socket_port' # Special-case config keys of the form 'server.servername.socket_port'
# to configure additional HTTP servers. # to configure additional HTTP servers.
if not hasattr(cherrypy, "servers"): if not hasattr(cherrypy, 'servers'):
cherrypy.servers = {} cherrypy.servers = {}
servername, k = atoms servername, k = atoms
@@ -260,45 +260,19 @@ def _server_namespace_handler(k, v):
setattr(cherrypy.servers[servername], k, v) setattr(cherrypy.servers[servername], k, v)
else: else:
setattr(cherrypy.server, k, v) setattr(cherrypy.server, k, v)
Config.namespaces["server"] = _server_namespace_handler Config.namespaces['server'] = _server_namespace_handler
def _engine_namespace_handler(k, v): def _engine_namespace_handler(k, v):
"""Backward compatibility handler for the "engine" namespace.""" """Config handler for the "engine" namespace."""
engine = cherrypy.engine engine = cherrypy.engine
deprecated = { if k == 'SIGHUP':
'autoreload_on': 'autoreload.on', engine.subscribe('SIGHUP', v)
'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])
elif k == 'SIGTERM': elif k == 'SIGTERM':
engine.listeners['SIGTERM'] = set([v]) engine.subscribe('SIGTERM', v)
elif "." in k: elif '.' in k:
plugin, attrname = k.split(".", 1) plugin, attrname = k.split('.', 1)
plugin = getattr(engine, plugin) plugin = getattr(engine, plugin)
if attrname == 'on': if attrname == 'on':
if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'): if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
@@ -313,7 +287,7 @@ def _engine_namespace_handler(k, v):
setattr(plugin, attrname, v) setattr(plugin, attrname, v)
else: else:
setattr(engine, k, v) setattr(engine, k, v)
Config.namespaces["engine"] = _engine_namespace_handler Config.namespaces['engine'] = _engine_namespace_handler
def _tree_namespace_handler(k, v): def _tree_namespace_handler(k, v):
@@ -321,9 +295,9 @@ def _tree_namespace_handler(k, v):
if isinstance(v, dict): if isinstance(v, dict):
for script_name, app in v.items(): for script_name, app in v.items():
cherrypy.tree.graft(app, script_name) cherrypy.tree.graft(app, script_name)
msg = "Mounted: %s on %s" % (app, script_name or "/") msg = 'Mounted: %s on %s' % (app, script_name or '/')
cherrypy.engine.log(msg) cherrypy.engine.log(msg)
else: else:
cherrypy.tree.graft(v, v.script_name) cherrypy.tree.graft(v, v.script_name)
cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/")) cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/'))
Config.namespaces["tree"] = _tree_namespace_handler Config.namespaces['tree'] = _tree_namespace_handler
+42 -42
View File
@@ -39,7 +39,7 @@ class PageHandler(object):
args = property( args = property(
get_args, get_args,
set_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): def get_kwargs(self):
@@ -52,7 +52,7 @@ class PageHandler(object):
kwargs = property( kwargs = property(
get_kwargs, get_kwargs,
set_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): def __call__(self):
@@ -153,7 +153,7 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
# arguments it's definitely a 404. # arguments it's definitely a 404.
message = None message = None
if show_mismatched_params: if show_mismatched_params:
message = "Missing parameters: %s" % ",".join(missing_args) message = 'Missing parameters: %s' % ','.join(missing_args)
raise cherrypy.HTTPError(404, message=message) raise cherrypy.HTTPError(404, message=message)
# the extra positional arguments come from the path - 404 Not Found # the extra positional arguments come from the path - 404 Not Found
@@ -175,8 +175,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
message = None message = None
if show_mismatched_params: if show_mismatched_params:
message = "Multiple values for parameters: "\ message = 'Multiple values for parameters: '\
"%s" % ",".join(multiple_args) '%s' % ','.join(multiple_args)
raise cherrypy.HTTPError(error, message=message) raise cherrypy.HTTPError(error, message=message)
if not varkw and varkw_usage > 0: if not varkw and varkw_usage > 0:
@@ -186,8 +186,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_qs_params: if extra_qs_params:
message = None message = None
if show_mismatched_params: if show_mismatched_params:
message = "Unexpected query string "\ message = 'Unexpected query string '\
"parameters: %s" % ", ".join(extra_qs_params) 'parameters: %s' % ', '.join(extra_qs_params)
raise cherrypy.HTTPError(404, message=message) raise cherrypy.HTTPError(404, message=message)
# If there were any extra body parameters, it's a 400 Not Found # If there were any extra body parameters, it's a 400 Not Found
@@ -195,8 +195,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_body_params: if extra_body_params:
message = None message = None
if show_mismatched_params: if show_mismatched_params:
message = "Unexpected body parameters: "\ message = 'Unexpected body parameters: '\
"%s" % ", ".join(extra_body_params) '%s' % ', '.join(extra_body_params)
raise cherrypy.HTTPError(400, message=message) raise cherrypy.HTTPError(400, message=message)
@@ -244,14 +244,14 @@ if sys.version_info < (3, 0):
def validate_translator(t): def validate_translator(t):
if not isinstance(t, str) or len(t) != 256: if not isinstance(t, str) or len(t) != 256:
raise ValueError( raise ValueError(
"The translate argument must be a str of len 256.") 'The translate argument must be a str of len 256.')
else: else:
punctuation_to_underscores = str.maketrans( punctuation_to_underscores = str.maketrans(
string.punctuation, '_' * len(string.punctuation)) string.punctuation, '_' * len(string.punctuation))
def validate_translator(t): def validate_translator(t):
if not isinstance(t, dict): 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): class Dispatcher(object):
@@ -289,7 +289,7 @@ class Dispatcher(object):
if func: if func:
# Decode any leftover %2F in the virtual_path atoms. # 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) request.handler = LateParamPageHandler(func, *vpath)
else: else:
request.handler = cherrypy.NotFound() request.handler = cherrypy.NotFound()
@@ -323,10 +323,10 @@ class Dispatcher(object):
fullpath_len = len(fullpath) fullpath_len = len(fullpath)
segleft = fullpath_len segleft = fullpath_len
nodeconf = {} nodeconf = {}
if hasattr(root, "_cp_config"): if hasattr(root, '_cp_config'):
nodeconf.update(root._cp_config) nodeconf.update(root._cp_config)
if "/" in app.config: if '/' in app.config:
nodeconf.update(app.config["/"]) nodeconf.update(app.config['/'])
object_trail = [['root', root, nodeconf, segleft]] object_trail = [['root', root, nodeconf, segleft]]
node = root node = root
@@ -361,9 +361,9 @@ class Dispatcher(object):
if segleft > pre_len: if segleft > pre_len:
# No path segment was removed. Raise an error. # No path segment was removed. Raise an error.
raise cherrypy.CherryPyException( raise cherrypy.CherryPyException(
"A vpath segment was added. Custom dispatchers may only " 'A vpath segment was added. Custom dispatchers may only '
+ "remove elements. While trying to process " + 'remove elements. While trying to process '
+ "{0} in {1}".format(name, fullpath) + '{0} in {1}'.format(name, fullpath)
) )
elif segleft == pre_len: elif segleft == pre_len:
# Assume that the handler used the current path segment, but # Assume that the handler used the current path segment, but
@@ -375,7 +375,7 @@ class Dispatcher(object):
if node is not None: if node is not None:
# Get _cp_config attached to this node. # Get _cp_config attached to this node.
if hasattr(node, "_cp_config"): if hasattr(node, '_cp_config'):
nodeconf.update(node._cp_config) nodeconf.update(node._cp_config)
# Mix in values from app.config for this path. # Mix in values from app.config for this path.
@@ -414,16 +414,16 @@ class Dispatcher(object):
continue continue
# Try a "default" method on the current leaf. # Try a "default" method on the current leaf.
if hasattr(candidate, "default"): if hasattr(candidate, 'default'):
defhandler = candidate.default defhandler = candidate.default
if getattr(defhandler, 'exposed', False): if getattr(defhandler, 'exposed', False):
# Insert any extra _cp_config from the default handler. # Insert any extra _cp_config from the default handler.
conf = getattr(defhandler, "_cp_config", {}) conf = getattr(defhandler, '_cp_config', {})
object_trail.insert( object_trail.insert(
i + 1, ["default", defhandler, conf, segleft]) i + 1, ['default', defhandler, conf, segleft])
request.config = set_conf() request.config = set_conf()
# See https://github.com/cherrypy/cherrypy/issues/613 # See https://github.com/cherrypy/cherrypy/issues/613
request.is_index = path.endswith("/") request.is_index = path.endswith('/')
return defhandler, fullpath[fullpath_len - segleft:-1] return defhandler, fullpath[fullpath_len - segleft:-1]
# Uncomment the next line to restrict positional params to # Uncomment the next line to restrict positional params to
@@ -470,23 +470,23 @@ class MethodDispatcher(Dispatcher):
if resource: if resource:
# Set Allow header # Set Allow header
avail = [m for m in dir(resource) if m.isupper()] avail = [m for m in dir(resource) if m.isupper()]
if "GET" in avail and "HEAD" not in avail: if 'GET' in avail and 'HEAD' not in avail:
avail.append("HEAD") avail.append('HEAD')
avail.sort() avail.sort()
cherrypy.serving.response.headers['Allow'] = ", ".join(avail) cherrypy.serving.response.headers['Allow'] = ', '.join(avail)
# Find the subhandler # Find the subhandler
meth = request.method.upper() meth = request.method.upper()
func = getattr(resource, meth, None) func = getattr(resource, meth, None)
if func is None and meth == "HEAD": if func is None and meth == 'HEAD':
func = getattr(resource, "GET", None) func = getattr(resource, 'GET', None)
if func: if func:
# Grab any _cp_config on the subhandler. # Grab any _cp_config on the subhandler.
if hasattr(func, "_cp_config"): if hasattr(func, '_cp_config'):
request.config.update(func._cp_config) request.config.update(func._cp_config)
# Decode any leftover %2F in the virtual_path atoms. # 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) request.handler = LateParamPageHandler(func, *vpath)
else: else:
request.handler = cherrypy.HTTPError(405) request.handler = cherrypy.HTTPError(405)
@@ -554,28 +554,28 @@ class RoutesDispatcher(object):
# Get config for the root object/path. # Get config for the root object/path.
request.config = base = cherrypy.config.copy() request.config = base = cherrypy.config.copy()
curpath = "" curpath = ''
def merge(nodeconf): def merge(nodeconf):
if 'tools.staticdir.dir' in nodeconf: if 'tools.staticdir.dir' in nodeconf:
nodeconf['tools.staticdir.section'] = curpath or "/" nodeconf['tools.staticdir.section'] = curpath or '/'
base.update(nodeconf) base.update(nodeconf)
app = request.app app = request.app
root = app.root root = app.root
if hasattr(root, "_cp_config"): if hasattr(root, '_cp_config'):
merge(root._cp_config) merge(root._cp_config)
if "/" in app.config: if '/' in app.config:
merge(app.config["/"]) merge(app.config['/'])
# Mix in values from 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: if atoms:
last = atoms.pop() last = atoms.pop()
else: else:
last = None last = None
for atom in atoms: for atom in atoms:
curpath = "/".join((curpath, atom)) curpath = '/'.join((curpath, atom))
if curpath in app.config: if curpath in app.config:
merge(app.config[curpath]) merge(app.config[curpath])
@@ -587,14 +587,14 @@ class RoutesDispatcher(object):
if isinstance(controller, classtype): if isinstance(controller, classtype):
controller = controller() controller = controller()
# Get config from the controller. # Get config from the controller.
if hasattr(controller, "_cp_config"): if hasattr(controller, '_cp_config'):
merge(controller._cp_config) merge(controller._cp_config)
action = result.get('action') action = result.get('action')
if action is not None: if action is not None:
handler = getattr(controller, action, None) handler = getattr(controller, action, None)
# Get config from the handler # Get config from the handler
if hasattr(handler, "_cp_config"): if hasattr(handler, '_cp_config'):
merge(handler._cp_config) merge(handler._cp_config)
else: else:
handler = controller handler = controller
@@ -602,7 +602,7 @@ class RoutesDispatcher(object):
# Do the last path atom here so it can # Do the last path atom here so it can
# override the controller's _cp_config. # override the controller's _cp_config.
if last: if last:
curpath = "/".join((curpath, last)) curpath = '/'.join((curpath, last))
if curpath in app.config: if curpath in app.config:
merge(app.config[curpath]) merge(app.config[curpath])
@@ -666,9 +666,9 @@ def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
domain = header('Host', '') domain = header('Host', '')
if use_x_forwarded_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: if prefix:
path_info = httputil.urljoin(prefix, path_info) path_info = httputil.urljoin(prefix, path_info)
+44 -45
View File
@@ -115,13 +115,15 @@ Note that you have to explicitly set
and not simply return an error message as a result. and not simply return an error message as a result.
""" """
import contextlib
from cgi import escape as _escape from cgi import escape as _escape
from sys import exc_info as _exc_info from sys import exc_info as _exc_info
from traceback import format_exception as _format_exception from traceback import format_exception as _format_exception
from xml.sax import saxutils
import six import six
from cherrypy._cpcompat import basestring, iteritems, ntob from cherrypy._cpcompat import text_or_bytes, iteritems, ntob
from cherrypy._cpcompat import tonative, urljoin as _urljoin from cherrypy._cpcompat import tonative, urljoin as _urljoin
from cherrypy.lib import httputil as _httputil from cherrypy.lib import httputil as _httputil
@@ -148,14 +150,14 @@ class InternalRedirect(CherryPyException):
URL. URL.
""" """
def __init__(self, path, query_string=""): def __init__(self, path, query_string=''):
import cherrypy import cherrypy
self.request = cherrypy.serving.request self.request = cherrypy.serving.request
self.query_string = query_string self.query_string = query_string
if "?" in path: if '?' in path:
# Separate any params included in the 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: # Note that urljoin will "do the right thing" whether url is:
# 1. a URL relative to root (e.g. "/dummy") # 1. a URL relative to root (e.g. "/dummy")
@@ -209,21 +211,10 @@ class HTTPRedirect(CherryPyException):
import cherrypy import cherrypy
request = cherrypy.serving.request request = cherrypy.serving.request
if isinstance(urls, basestring): if isinstance(urls, text_or_bytes):
urls = [urls] urls = [urls]
abs_urls = [] self.urls = [tonative(url, encoding or self.encoding) for url in urls]
for url in urls:
url = tonative(url, encoding or self.encoding)
# Note that urljoin will "do the right thing" whether url is:
# 1. a complete URL with host (e.g. "http://www.example.com/test")
# 2. a URL relative to root (e.g. "/dummy")
# 3. a URL relative to the current path
# Note that any query string in cherrypy.request is discarded.
url = _urljoin(cherrypy.url(), url)
abs_urls.append(url)
self.urls = abs_urls
# RFC 2616 indicates a 301 response code fits our goal; however, # RFC 2616 indicates a 301 response code fits our goal; however,
# browser support for 301 is quite messy. Do 302/303 instead. See # browser support for 301 is quite messy. Do 302/303 instead. See
@@ -236,10 +227,10 @@ class HTTPRedirect(CherryPyException):
else: else:
status = int(status) status = int(status)
if status < 300 or status > 399: 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 self.status = status
CherryPyException.__init__(self, abs_urls, status) CherryPyException.__init__(self, self.urls, status)
def set_response(self): def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent """Modify cherrypy.response status, headers, and body to represent
@@ -253,7 +244,7 @@ class HTTPRedirect(CherryPyException):
response.status = status = self.status response.status = status = self.status
if status in (300, 301, 302, 303, 307): 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 # "The ... URI SHOULD be given by the Location field
# in the response." # in the response."
response.headers['Location'] = self.urls[0] response.headers['Location'] = self.urls[0]
@@ -262,16 +253,15 @@ class HTTPRedirect(CherryPyException):
# SHOULD contain a short hypertext note with a hyperlink to the # SHOULD contain a short hypertext note with a hyperlink to the
# new URI(s)." # new URI(s)."
msg = { msg = {
300: "This resource can be found at ", 300: 'This resource can be found at ',
301: "This resource has permanently moved to ", 301: 'This resource has permanently moved to ',
302: "This resource resides temporarily at ", 302: 'This resource resides temporarily at ',
303: "This resource can be found at ", 303: 'This resource can be found at ',
307: "This resource has moved temporarily to ", 307: 'This resource has moved temporarily to ',
}[status] }[status]
msg += '<a href=%s>%s</a>.' msg += '<a href=%s>%s</a>.'
from xml.sax import saxutils
msgs = [msg % (saxutils.quoteattr(u), u) for u in self.urls] 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 # Previous code may have set C-L, so we have to reset it
# (allow finalize to set it). # (allow finalize to set it).
response.headers.pop('Content-Length', None) response.headers.pop('Content-Length', None)
@@ -301,7 +291,7 @@ class HTTPRedirect(CherryPyException):
# Previous code may have set C-L, so we have to reset it. # Previous code may have set C-L, so we have to reset it.
response.headers.pop('Content-Length', None) response.headers.pop('Content-Length', None)
else: else:
raise ValueError("The %s status code is unknown." % status) raise ValueError('The %s status code is unknown.' % status)
def __call__(self): def __call__(self):
"""Use this exception as a request.handler (raise self).""" """Use this exception as a request.handler (raise self)."""
@@ -317,9 +307,9 @@ def clean_headers(status):
# Remove headers which applied to the original content, # Remove headers which applied to the original content,
# but do not apply to the error page. # but do not apply to the error page.
respheaders = response.headers respheaders = response.headers
for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After", for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After',
"Vary", "Content-Encoding", "Content-Length", "Expires", 'Vary', 'Content-Encoding', 'Content-Length', 'Expires',
"Content-Location", "Content-MD5", "Last-Modified"]: 'Content-Location', 'Content-MD5', 'Last-Modified']:
if key in respheaders: if key in respheaders:
del respheaders[key] del respheaders[key]
@@ -330,8 +320,8 @@ def clean_headers(status):
# specifies the current length of the selected resource. # specifies the current length of the selected resource.
# A response with status code 206 (Partial Content) MUST NOT # A response with status code 206 (Partial Content) MUST NOT
# include a Content-Range field with a byte-range- resp-spec of "*". # include a Content-Range field with a byte-range- resp-spec of "*".
if "Content-Range" in respheaders: if 'Content-Range' in respheaders:
del respheaders["Content-Range"] del respheaders['Content-Range']
class HTTPError(CherryPyException): class HTTPError(CherryPyException):
@@ -371,7 +361,7 @@ class HTTPError(CherryPyException):
raise self.__class__(500, _exc_info()[1].args[0]) raise self.__class__(500, _exc_info()[1].args[0])
if self.code < 400 or self.code > 599: 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/ # See http://www.python.org/dev/peps/pep-0352/
# self.message = message # self.message = message
@@ -413,6 +403,15 @@ class HTTPError(CherryPyException):
"""Use this exception as a request.handler (raise self).""" """Use this exception as a request.handler (raise self)."""
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): class NotFound(HTTPError):
@@ -480,7 +479,7 @@ def get_error_page(status, **kwargs):
# We can't use setdefault here, because some # We can't use setdefault here, because some
# callers send None for kwarg values. # callers send None for kwarg values.
if kwargs.get('status') is None: if kwargs.get('status') is None:
kwargs['status'] = "%s %s" % (code, reason) kwargs['status'] = '%s %s' % (code, reason)
if kwargs.get('message') is None: if kwargs.get('message') is None:
kwargs['message'] = message kwargs['message'] = message
if kwargs.get('traceback') is None: if kwargs.get('traceback') is None:
@@ -490,7 +489,7 @@ def get_error_page(status, **kwargs):
for k, v in iteritems(kwargs): for k, v in iteritems(kwargs):
if v is None: if v is None:
kwargs[k] = "" kwargs[k] = ''
else: else:
kwargs[k] = _escape(kwargs[k]) kwargs[k] = _escape(kwargs[k])
@@ -528,12 +527,12 @@ def get_error_page(status, **kwargs):
e = _format_exception(*_exc_info())[-1] e = _format_exception(*_exc_info())[-1]
m = kwargs['message'] m = kwargs['message']
if m: if m:
m += "<br />" m += '<br />'
m += "In addition, the custom error page failed:\n<br />%s" % e m += 'In addition, the custom error page failed:\n<br />%s' % e
kwargs['message'] = m kwargs['message'] = m
response = cherrypy.serving.response 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 result = template % kwargs
return result.encode('utf-8') return result.encode('utf-8')
@@ -565,7 +564,7 @@ def _be_ie_unfriendly(status):
if l and l < s: if l and l < s:
# IN ADDITION: the response must be written to IE # IN ADDITION: the response must be written to IE
# in one chunk or it will still get replaced! Bah. # in one chunk or it will still get replaced! Bah.
content = content + (ntob(" ") * (s - l)) content = content + (ntob(' ') * (s - l))
response.body = content response.body = content
response.headers['Content-Length'] = str(len(content)) response.headers['Content-Length'] = str(len(content))
@@ -576,9 +575,9 @@ def format_exc(exc=None):
if exc is None: if exc is None:
exc = _exc_info() exc = _exc_info()
if exc == (None, None, None): if exc == (None, None, None):
return "" return ''
import traceback import traceback
return "".join(traceback.format_exception(*exc)) return ''.join(traceback.format_exception(*exc))
finally: finally:
del exc del exc
@@ -600,13 +599,13 @@ def bare_error(extrabody=None):
# it cannot be allowed to fail. Therefore, don't add to it! # it cannot be allowed to fail. Therefore, don't add to it!
# In particular, don't call any other CP functions. # 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 extrabody is not None:
if not isinstance(extrabody, bytes): if not isinstance(extrabody, bytes):
extrabody = extrabody.encode('utf-8') 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-Type'), ntob('text/plain')),
(ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))], (ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))],
[body]) [body])
+28 -26
View File
@@ -109,9 +109,6 @@ the "log.error_file" config entry, for example).
import datetime import datetime
import logging 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 os
import sys import sys
@@ -122,6 +119,11 @@ from cherrypy import _cperror
from cherrypy._cpcompat import ntob 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): class NullHandler(logging.Handler):
"""A no-op logging handler to silence the logging.lastResort handler.""" """A no-op logging handler to silence the logging.lastResort handler."""
@@ -170,17 +172,17 @@ class LogManager(object):
cherrypy.access.<appid> 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.logger_root = logger_root
self.appid = appid self.appid = appid
if appid is None: if appid is None:
self.error_log = logging.getLogger("%s.error" % logger_root) self.error_log = logging.getLogger('%s.error' % logger_root)
self.access_log = logging.getLogger("%s.access" % logger_root) self.access_log = logging.getLogger('%s.access' % logger_root)
else: else:
self.error_log = logging.getLogger( self.error_log = logging.getLogger(
"%s.error.%s" % (logger_root, appid)) '%s.error.%s' % (logger_root, appid))
self.access_log = logging.getLogger( self.access_log = logging.getLogger(
"%s.access.%s" % (logger_root, appid)) '%s.access.%s' % (logger_root, appid))
self.error_log.setLevel(logging.INFO) self.error_log.setLevel(logging.INFO)
self.access_log.setLevel(logging.INFO) self.access_log.setLevel(logging.INFO)
@@ -244,19 +246,19 @@ class LogManager(object):
outheaders = response.headers outheaders = response.headers
inheaders = request.headers inheaders = request.headers
if response.output_status is None: if response.output_status is None:
status = "-" status = '-'
else: else:
status = response.output_status.split(ntob(" "), 1)[0] status = response.output_status.split(ntob(' '), 1)[0]
if six.PY3: if six.PY3:
status = status.decode('ISO-8859-1') status = status.decode('ISO-8859-1')
atoms = {'h': remote.name or remote.ip, atoms = {'h': remote.name or remote.ip,
'l': '-', 'l': '-',
'u': getattr(request, "login", None) or "-", 'u': getattr(request, 'login', None) or '-',
't': self.time(), 't': self.time(),
'r': request.request_line, 'r': request.request_line,
's': status, 's': status,
'b': dict.get(outheaders, 'Content-Length', '') or "-", 'b': dict.get(outheaders, 'Content-Length', '') or '-',
'f': dict.get(inheaders, 'Referer', ''), 'f': dict.get(inheaders, 'Referer', ''),
'a': dict.get(inheaders, 'User-Agent', ''), 'a': dict.get(inheaders, 'User-Agent', ''),
'o': dict.get(inheaders, 'Host', '-'), 'o': dict.get(inheaders, 'Host', '-'),
@@ -312,26 +314,26 @@ class LogManager(object):
def _get_builtin_handler(self, log, key): def _get_builtin_handler(self, log, key):
for h in log.handlers: for h in log.handlers:
if getattr(h, "_cpbuiltin", None) == key: if getattr(h, '_cpbuiltin', None) == key:
return h return h
# ------------------------- Screen handlers ------------------------- # # ------------------------- Screen handlers ------------------------- #
def _set_screen_handler(self, log, enable, stream=None): 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 enable:
if not h: if not h:
if stream is None: if stream is None:
stream = sys.stderr stream = sys.stderr
h = logging.StreamHandler(stream) h = logging.StreamHandler(stream)
h.setFormatter(logfmt) h.setFormatter(logfmt)
h._cpbuiltin = "screen" h._cpbuiltin = 'screen'
log.addHandler(h) log.addHandler(h)
elif h: elif h:
log.handlers.remove(h) log.handlers.remove(h)
def _get_screen(self): def _get_screen(self):
h = self._get_builtin_handler 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) return bool(has_h)
def _set_screen(self, newvalue): def _set_screen(self, newvalue):
@@ -349,11 +351,11 @@ class LogManager(object):
def _add_builtin_file_handler(self, log, fname): def _add_builtin_file_handler(self, log, fname):
h = logging.FileHandler(fname) h = logging.FileHandler(fname)
h.setFormatter(logfmt) h.setFormatter(logfmt)
h._cpbuiltin = "file" h._cpbuiltin = 'file'
log.addHandler(h) log.addHandler(h)
def _set_file_handler(self, log, filename): def _set_file_handler(self, log, filename):
h = self._get_builtin_handler(log, "file") h = self._get_builtin_handler(log, 'file')
if filename: if filename:
if h: if h:
if h.baseFilename != os.path.abspath(filename): if h.baseFilename != os.path.abspath(filename):
@@ -368,7 +370,7 @@ class LogManager(object):
log.handlers.remove(h) log.handlers.remove(h)
def _get_error_file(self): 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: if h:
return h.baseFilename return h.baseFilename
return '' return ''
@@ -383,7 +385,7 @@ class LogManager(object):
""") """)
def _get_access_file(self): 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: if h:
return h.baseFilename return h.baseFilename
return '' return ''
@@ -400,18 +402,18 @@ class LogManager(object):
# ------------------------- WSGI handlers ------------------------- # # ------------------------- WSGI handlers ------------------------- #
def _set_wsgi_handler(self, log, enable): def _set_wsgi_handler(self, log, enable):
h = self._get_builtin_handler(log, "wsgi") h = self._get_builtin_handler(log, 'wsgi')
if enable: if enable:
if not h: if not h:
h = WSGIErrorHandler() h = WSGIErrorHandler()
h.setFormatter(logfmt) h.setFormatter(logfmt)
h._cpbuiltin = "wsgi" h._cpbuiltin = 'wsgi'
log.addHandler(h) log.addHandler(h)
elif h: elif h:
log.handlers.remove(h) log.handlers.remove(h)
def _get_wsgi(self): 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): def _set_wsgi(self, newvalue):
self._set_wsgi_handler(self.error_log, newvalue) self._set_wsgi_handler(self.error_log, newvalue)
@@ -447,16 +449,16 @@ class WSGIErrorHandler(logging.Handler):
else: else:
try: try:
msg = self.format(record) msg = self.format(record)
fs = "%s\n" fs = '%s\n'
import types import types
# if no unicode support... # if no unicode support...
if not hasattr(types, "UnicodeType"): if not hasattr(types, 'UnicodeType'):
stream.write(fs % msg) stream.write(fs % msg)
else: else:
try: try:
stream.write(fs % msg) stream.write(fs % msg)
except UnicodeError: except UnicodeError:
stream.write(fs % msg.encode("UTF-8")) stream.write(fs % msg.encode('UTF-8'))
self.flush() self.flush()
except: except:
self.handleError(record) self.handleError(record)
+35 -35
View File
@@ -55,9 +55,11 @@ resides in the global site-package this won't be needed.
Then restart apache2 and access http://127.0.0.1:8080 Then restart apache2 and access http://127.0.0.1:8080
""" """
import logging
import sys
import io import io
import logging
import os
import re
import sys
import cherrypy import cherrypy
from cherrypy._cpcompat import copyitems, ntob from cherrypy._cpcompat import copyitems, ntob
@@ -86,14 +88,14 @@ def setup(req):
func() func()
cherrypy.config.update({'log.screen': False, cherrypy.config.update({'log.screen': False,
"tools.ignore_headers.on": True, 'tools.ignore_headers.on': True,
"tools.ignore_headers.headers": ['Range'], 'tools.ignore_headers.headers': ['Range'],
}) })
engine = cherrypy.engine engine = cherrypy.engine
if hasattr(engine, "signal_handler"): if hasattr(engine, 'signal_handler'):
engine.signal_handler.unsubscribe() engine.signal_handler.unsubscribe()
if hasattr(engine, "console_control_handler"): if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.unsubscribe() engine.console_control_handler.unsubscribe()
engine.autoreload.unsubscribe() engine.autoreload.unsubscribe()
cherrypy.server.unsubscribe() cherrypy.server.unsubscribe()
@@ -147,10 +149,10 @@ def handler(req):
# Obtain a Request object from CherryPy # Obtain a Request object from CherryPy
local = req.connection.local_addr local = req.connection.local_addr
local = httputil.Host( 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 = req.connection.remote_addr
remote = httputil.Host( 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' scheme = req.parsed_uri[0] or 'http'
req.get_basic_auth_pw() req.get_basic_auth_pw()
@@ -163,7 +165,7 @@ def handler(req):
except AttributeError: except AttributeError:
bad_value = ("You must provide a PythonOption '%s', " bad_value = ("You must provide a PythonOption '%s', "
"either 'on' or 'off', when running a version " "either 'on' or 'off', when running a version "
"of mod_python < 3.1") 'of mod_python < 3.1')
threaded = options.get('multithread', '').lower() threaded = options.get('multithread', '').lower()
if threaded == 'on': if threaded == 'on':
@@ -171,7 +173,7 @@ def handler(req):
elif threaded == 'off': elif threaded == 'off':
threaded = False threaded = False
else: else:
raise ValueError(bad_value % "multithread") raise ValueError(bad_value % 'multithread')
forked = options.get('multiprocess', '').lower() forked = options.get('multiprocess', '').lower()
if forked == 'on': if forked == 'on':
@@ -179,16 +181,16 @@ def handler(req):
elif forked == 'off': elif forked == 'off':
forked = False forked = False
else: 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: if sn is None:
send_response(req, '404 Not Found', [], '') send_response(req, '404 Not Found', [], '')
else: else:
app = cherrypy.tree.apps[sn] app = cherrypy.tree.apps[sn]
method = req.method method = req.method
path = req.uri path = req.uri
qs = req.args or "" qs = req.args or ''
reqproto = req.protocol reqproto = req.protocol
headers = copyitems(req.headers_in) headers = copyitems(req.headers_in)
rfile = _ReadOnlyRequest(req) rfile = _ReadOnlyRequest(req)
@@ -198,7 +200,7 @@ def handler(req):
redirections = [] redirections = []
while True: while True:
request, response = app.get_serving(local, remote, scheme, request, response = app.get_serving(local, remote, scheme,
"HTTP/1.1") 'HTTP/1.1')
request.login = req.user request.login = req.user
request.multithread = bool(threaded) request.multithread = bool(threaded)
request.multiprocess = bool(forked) request.multiprocess = bool(forked)
@@ -217,17 +219,17 @@ def handler(req):
if not recursive: if not recursive:
if ir.path in redirections: if ir.path in redirections:
raise RuntimeError( raise RuntimeError(
"InternalRedirector visited the same URL " 'InternalRedirector visited the same URL '
"twice: %r" % ir.path) 'twice: %r' % ir.path)
else: else:
# Add the *previous* path_info + qs to # Add the *previous* path_info + qs to
# redirections. # redirections.
if qs: if qs:
qs = "?" + qs qs = '?' + qs
redirections.append(sn + path + qs) redirections.append(sn + path + qs)
# Munge environment and try again. # Munge environment and try again.
method = "GET" method = 'GET'
path = ir.path path = ir.path
qs = ir.query_string qs = ir.query_string
rfile = io.BytesIO() rfile = io.BytesIO()
@@ -250,7 +252,7 @@ def send_response(req, status, headers, body, stream=False):
req.status = int(status[:3]) req.status = int(status[:3])
# Set response headers # Set response headers
req.content_type = "text/plain" req.content_type = 'text/plain'
for header, value in headers: for header, value in headers:
if header.lower() == 'content-type': if header.lower() == 'content-type':
req.content_type = value req.content_type = value
@@ -262,7 +264,7 @@ def send_response(req, status, headers, body, stream=False):
req.flush() req.flush()
# Set response body # Set response body
if isinstance(body, basestring): if isinstance(body, text_or_bytes):
req.write(body) req.write(body)
else: else:
for seg in body: for seg in body:
@@ -270,8 +272,6 @@ def send_response(req, status, headers, body, stream=False):
# --------------- Startup tools for CherryPy + mod_python --------------- # # --------------- Startup tools for CherryPy + mod_python --------------- #
import os
import re
try: try:
import subprocess import subprocess
@@ -286,13 +286,13 @@ except ImportError:
return pipeout return pipeout
def read_process(cmd, args=""): def read_process(cmd, args=''):
fullcmd = "%s %s" % (cmd, args) fullcmd = '%s %s' % (cmd, args)
pipeout = popen(fullcmd) pipeout = popen(fullcmd)
try: try:
firstline = pipeout.readline() firstline = pipeout.readline()
cmd_not_found = re.search( cmd_not_found = re.search(
ntob("(not recognized|No such file|not found)"), ntob('(not recognized|No such file|not found)'),
firstline, firstline,
re.IGNORECASE re.IGNORECASE
) )
@@ -321,8 +321,8 @@ LoadModule python_module modules/mod_python.so
</Location> </Location>
""" """
def __init__(self, loc="/", port=80, opts=None, apache_path="apache", def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
handler="cherrypy._cpmodpy::handler"): handler='cherrypy._cpmodpy::handler'):
self.loc = loc self.loc = loc
self.port = port self.port = port
self.opts = opts self.opts = opts
@@ -330,25 +330,25 @@ LoadModule python_module modules/mod_python.so
self.handler = handler self.handler = handler
def start(self): 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]) for k, v in self.opts])
conf_data = self.template % {"port": self.port, conf_data = self.template % {'port': self.port,
"loc": self.loc, 'loc': self.loc,
"opts": opts, 'opts': opts,
"handler": self.handler, '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') f = open(mpconf, 'wb')
try: try:
f.write(conf_data) f.write(conf_data)
finally: finally:
f.close() 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 self.ready = True
return response return response
def stop(self): def stop(self):
os.popen("apache -k stop") os.popen('apache -k stop')
self.ready = False self.ready = False
+10 -10
View File
@@ -19,19 +19,19 @@ class NativeGateway(wsgiserver.Gateway):
try: try:
# Obtain a Request object from CherryPy # Obtain a Request object from CherryPy
local = req.server.bind_addr 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 = 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 scheme = req.scheme
sn = cherrypy.tree.script_name(req.uri or "/") sn = cherrypy.tree.script_name(req.uri or '/')
if sn is None: if sn is None:
self.send_response('404 Not Found', [], ['']) self.send_response('404 Not Found', [], [''])
else: else:
app = cherrypy.tree.apps[sn] app = cherrypy.tree.apps[sn]
method = req.method method = req.method
path = req.path path = req.path
qs = req.qs or "" qs = req.qs or ''
headers = req.inheaders.items() headers = req.inheaders.items()
rfile = req.rfile rfile = req.rfile
prev = None prev = None
@@ -40,7 +40,7 @@ class NativeGateway(wsgiserver.Gateway):
redirections = [] redirections = []
while True: while True:
request, response = app.get_serving( request, response = app.get_serving(
local, remote, scheme, "HTTP/1.1") local, remote, scheme, 'HTTP/1.1')
request.multithread = True request.multithread = True
request.multiprocess = False request.multiprocess = False
request.app = app request.app = app
@@ -60,17 +60,17 @@ class NativeGateway(wsgiserver.Gateway):
if not self.recursive: if not self.recursive:
if ir.path in redirections: if ir.path in redirections:
raise RuntimeError( raise RuntimeError(
"InternalRedirector visited the same " 'InternalRedirector visited the same '
"URL twice: %r" % ir.path) 'URL twice: %r' % ir.path)
else: else:
# Add the *previous* path_info + qs to # Add the *previous* path_info + qs to
# redirections. # redirections.
if qs: if qs:
qs = "?" + qs qs = '?' + qs
redirections.append(sn + path + qs) redirections.append(sn + path + qs)
# Munge environment and try again. # Munge environment and try again.
method = "GET" method = 'GET'
path = ir.path path = ir.path
qs = ir.query_string qs = ir.query_string
rfile = io.BytesIO() rfile = io.BytesIO()
@@ -91,7 +91,7 @@ class NativeGateway(wsgiserver.Gateway):
req = self.req req = self.req
# Set response status # Set response status
req.status = str(status or "500 Server Error") req.status = str(status or '500 Server Error')
# Set response headers # Set response headers
for header, value in headers: for header, value in headers:
+31 -43
View File
@@ -132,7 +132,7 @@ except ImportError:
return ntob('').join(atoms) return ntob('').join(atoms)
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring, ntob, ntou from cherrypy._cpcompat import text_or_bytes, ntob, ntou
from cherrypy.lib import httputil from cherrypy.lib import httputil
@@ -169,8 +169,8 @@ def process_urlencoded(entity):
break break
else: else:
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
400, "The request entity could not be decoded. The following " 400, 'The request entity could not be decoded. The following '
"charsets were attempted: %s" % repr(entity.attempt_charsets)) 'charsets were attempted: %s' % repr(entity.attempt_charsets))
# Now that all values have been successfully parsed and decoded, # Now that all values have been successfully parsed and decoded,
# apply them to the entity.params dict. # apply them to the entity.params dict.
@@ -185,7 +185,7 @@ def process_urlencoded(entity):
def process_multipart(entity): def process_multipart(entity):
"""Read all multipart parts into entity.parts.""" """Read all multipart parts into entity.parts."""
ib = "" ib = ''
if 'boundary' in entity.content_type.params: if 'boundary' in entity.content_type.params:
# http://tools.ietf.org/html/rfc2046#section-5.1.1 # http://tools.ietf.org/html/rfc2046#section-5.1.1
# "The grammar for parameters on the Content-type field is such that it # "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" # on the Content-type line"
ib = entity.content_type.params['boundary'].strip('"') 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,)) raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
ib = ('--' + ib).encode('ascii') ib = ('--' + ib).encode('ascii')
@@ -428,7 +428,7 @@ class Entity(object):
# Copy the class 'attempt_charsets', prepending any Content-Type # Copy the class 'attempt_charsets', prepending any Content-Type
# charset # charset
dec = self.content_type.params.get("charset", None) dec = self.content_type.params.get('charset', None)
if dec: if dec:
self.attempt_charsets = [dec] + [c for c in self.attempt_charsets self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
if c != dec] if c != dec]
@@ -469,8 +469,8 @@ class Entity(object):
# The 'type' attribute is deprecated in 3.2; remove it in 3.3. # The 'type' attribute is deprecated in 3.2; remove it in 3.3.
type = property( type = property(
lambda self: self.content_type, lambda self: self.content_type,
doc="A deprecated alias for " doc='A deprecated alias for '
":attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`." ':attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`.'
) )
def read(self, size=None, fp_out=None): def read(self, size=None, fp_out=None):
@@ -536,8 +536,8 @@ class Entity(object):
else: else:
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
400, 400,
"The request entity could not be decoded. The following " 'The request entity could not be decoded. The following '
"charsets were attempted: %s" % repr(self.attempt_charsets) 'charsets were attempted: %s' % repr(self.attempt_charsets)
) )
def process(self): def process(self):
@@ -613,18 +613,19 @@ class Part(Entity):
self.file = None self.file = None
self.value = None self.value = None
@classmethod
def from_fp(cls, fp, boundary): def from_fp(cls, fp, boundary):
headers = cls.read_headers(fp) headers = cls.read_headers(fp)
return cls(fp, headers, boundary) return cls(fp, headers, boundary)
from_fp = classmethod(from_fp)
@classmethod
def read_headers(cls, fp): def read_headers(cls, fp):
headers = httputil.HeaderMap() headers = httputil.HeaderMap()
while True: while True:
line = fp.readline() line = fp.readline()
if not line: if not line:
# No more data--illegal end of headers # 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'): if line == ntob('\r\n') or line == ntob('\n'):
# Normal end of headers # Normal end of headers
@@ -636,17 +637,16 @@ class Part(Entity):
# It's a continuation line. # It's a continuation line.
v = line.strip().decode('ISO-8859-1') v = line.strip().decode('ISO-8859-1')
else: else:
k, v = line.split(ntob(":"), 1) k, v = line.split(ntob(':'), 1)
k = k.strip().decode('ISO-8859-1') k = k.strip().decode('ISO-8859-1')
v = v.strip().decode('ISO-8859-1') v = v.strip().decode('ISO-8859-1')
existing = headers.get(k) existing = headers.get(k)
if existing: if existing:
v = ", ".join((existing, v)) v = ', '.join((existing, v))
headers[k] = v headers[k] = v
return headers return headers
read_headers = classmethod(read_headers)
def read_lines_to_boundary(self, fp_out=None): def read_lines_to_boundary(self, fp_out=None):
"""Read bytes from self.fp and return or write them to a file. """Read bytes from self.fp and return or write them to a file.
@@ -658,16 +658,16 @@ class Part(Entity):
object that supports the 'write' method; all bytes read will be object that supports the 'write' method; all bytes read will be
written to the fp, and that fp is returned. written to the fp, and that fp is returned.
""" """
endmarker = self.boundary + ntob("--") endmarker = self.boundary + ntob('--')
delim = ntob("") delim = ntob('')
prev_lf = True prev_lf = True
lines = [] lines = []
seen = 0 seen = 0
while True: while True:
line = self.fp.readline(1 << 16) line = self.fp.readline(1 << 16)
if not line: if not line:
raise EOFError("Illegal end of multipart body.") raise EOFError('Illegal end of multipart body.')
if line.startswith(ntob("--")) and prev_lf: if line.startswith(ntob('--')) and prev_lf:
strippedline = line.strip() strippedline = line.strip()
if strippedline == self.boundary: if strippedline == self.boundary:
break break
@@ -677,16 +677,16 @@ class Part(Entity):
line = delim + line line = delim + line
if line.endswith(ntob("\r\n")): if line.endswith(ntob('\r\n')):
delim = ntob("\r\n") delim = ntob('\r\n')
line = line[:-2] line = line[:-2]
prev_lf = True prev_lf = True
elif line.endswith(ntob("\n")): elif line.endswith(ntob('\n')):
delim = ntob("\n") delim = ntob('\n')
line = line[:-1] line = line[:-1]
prev_lf = True prev_lf = True
else: else:
delim = ntob("") delim = ntob('')
prev_lf = False prev_lf = False
if fp_out is None: if fp_out is None:
@@ -715,7 +715,7 @@ class Part(Entity):
self.file = self.read_into_file() self.file = self.read_into_file()
else: else:
result = self.read_lines_to_boundary() result = self.read_lines_to_boundary()
if isinstance(result, basestring): if isinstance(result, text_or_bytes):
self.value = result self.value = result
else: else:
self.file = result self.file = result
@@ -732,19 +732,7 @@ class Part(Entity):
Entity.part_class = Part Entity.part_class = Part
try: inf = float('inf')
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()
comma_separated_headers = [ comma_separated_headers = [
'Accept', 'Accept-Charset', 'Accept-Encoding', 'Accept', 'Accept-Charset', 'Accept-Encoding',
@@ -839,7 +827,7 @@ class SizedReader:
if e.__class__.__name__ == 'MaxSizeExceeded': if e.__class__.__name__ == 'MaxSizeExceeded':
# Post data is too big # Post data is too big
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
413, "Maximum request length: %r" % e.args[1]) 413, 'Maximum request length: %r' % e.args[1])
else: else:
raise raise
if not data: if not data:
@@ -915,23 +903,23 @@ class SizedReader:
v = line.strip() v = line.strip()
else: else:
try: try:
k, v = line.split(ntob(":"), 1) k, v = line.split(ntob(':'), 1)
except ValueError: except ValueError:
raise ValueError("Illegal header line.") raise ValueError('Illegal header line.')
k = k.strip().title() k = k.strip().title()
v = v.strip() v = v.strip()
if k in comma_separated_headers: if k in comma_separated_headers:
existing = self.trailers.get(envname) existing = self.trailers.get(envname)
if existing: if existing:
v = ntob(", ").join((existing, v)) v = ntob(', ').join((existing, v))
self.trailers[k] = v self.trailers[k] = v
except Exception: except Exception:
e = sys.exc_info()[1] e = sys.exc_info()[1]
if e.__class__.__name__ == 'MaxSizeExceeded': if e.__class__.__name__ == 'MaxSizeExceeded':
# Post data is too big # Post data is too big
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
413, "Maximum request length: %r" % e.args[1]) 413, 'Maximum request length: %r' % e.args[1])
else: else:
raise raise
+55 -58
View File
@@ -5,7 +5,7 @@ import warnings
import six import six
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring, copykeys, ntob from cherrypy._cpcompat import text_or_bytes, copykeys, ntob
from cherrypy._cpcompat import SimpleCookie, CookieError from cherrypy._cpcompat import SimpleCookie, CookieError
from cherrypy import _cpreqbody, _cpconfig from cherrypy import _cpreqbody, _cpconfig
from cherrypy._cperror import format_exc, bare_error from cherrypy._cperror import format_exc, bare_error
@@ -41,11 +41,11 @@ class Hook(object):
self.callback = callback self.callback = callback
if failsafe is None: if failsafe is None:
failsafe = getattr(callback, "failsafe", False) failsafe = getattr(callback, 'failsafe', False)
self.failsafe = failsafe self.failsafe = failsafe
if priority is None: if priority is None:
priority = getattr(callback, "priority", 50) priority = getattr(callback, 'priority', 50)
self.priority = priority self.priority = priority
self.kwargs = kwargs self.kwargs = kwargs
@@ -64,10 +64,10 @@ class Hook(object):
def __repr__(self): def __repr__(self):
cls = self.__class__ 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, % (cls.__module__, cls.__name__, self.callback,
self.failsafe, self.priority, self.failsafe, self.priority,
", ".join(['%s=%r' % (k, v) ', '.join(['%s=%r' % (k, v)
for k, v in self.kwargs.items()]))) for k, v in self.kwargs.items()])))
@@ -124,7 +124,7 @@ class HookMap(dict):
def __repr__(self): def __repr__(self):
cls = self.__class__ cls = self.__class__
return "%s.%s(points=%r)" % ( return '%s.%s(points=%r)' % (
cls.__module__, cls.__module__,
cls.__name__, cls.__name__,
copykeys(self) copykeys(self)
@@ -138,8 +138,8 @@ def hooks_namespace(k, v):
# Use split again to allow multiple hooks for a single # Use split again to allow multiple hooks for a single
# hookpoint per path (e.g. "hooks.before_handler.1"). # hookpoint per path (e.g. "hooks.before_handler.1").
# Little-known fact you only get from reading source ;) # Little-known fact you only get from reading source ;)
hookpoint = k.split(".", 1)[0] hookpoint = k.split('.', 1)[0]
if isinstance(v, basestring): if isinstance(v, text_or_bytes):
v = cherrypy.lib.attributes(v) v = cherrypy.lib.attributes(v)
if not isinstance(v, Hook): if not isinstance(v, Hook):
v = Hook(v) v = Hook(v)
@@ -199,23 +199,23 @@ class Request(object):
unless we are processing an InternalRedirect.""" unless we are processing an InternalRedirect."""
# Conversation/connection attributes # Conversation/connection attributes
local = httputil.Host("127.0.0.1", 80) local = httputil.Host('127.0.0.1', 80)
"An httputil.Host(ip, port, hostname) object for the server socket." 'An httputil.Host(ip, port, hostname) object for the server socket.'
remote = httputil.Host("127.0.0.1", 1111) remote = httputil.Host('127.0.0.1', 1111)
"An httputil.Host(ip, port, hostname) object for the client socket." '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, The protocol used between client and server. In most cases,
this will be either 'http' or 'https'.""" 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 The HTTP version for which the HTTP server is at least
conditionally compliant.""" conditionally compliant."""
base = "" base = ''
"""The (scheme://host) portion of the requested URL. """The (scheme://host) portion of the requested URL.
In some cases (e.g. when proxying via mod_rewrite), this may contain In some cases (e.g. when proxying via mod_rewrite), this may contain
path segments which cherrypy.url uses when constructing url's, but path segments which cherrypy.url uses when constructing url's, but
@@ -223,13 +223,13 @@ class Request(object):
MUST NOT end in a slash.""" MUST NOT end in a slash."""
# Request-Line attributes # Request-Line attributes
request_line = "" request_line = ''
""" """
The complete Request-Line received from the client. This is a The complete Request-Line received from the client. This is a
single string consisting of the request method, URI, and protocol single string consisting of the request method, URI, and protocol
version (joined by spaces). Any final CRLF is removed.""" version (joined by spaces). Any final CRLF is removed."""
method = "GET" method = 'GET'
""" """
Indicates the HTTP method to be performed on the resource identified Indicates the HTTP method to be performed on the resource identified
by the Request-URI. Common methods include GET, HEAD, POST, PUT, and 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. servers and gateways may restrict the set of allowable methods.
CherryPy applications SHOULD restrict the set (on a per-URI basis).""" 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 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 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, If True, the rfile (if any) is automatically read and parsed,
and the result placed into request.params or request.body.""" 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 A sequence of HTTP methods for which CherryPy will automatically
attempt to read a body from the rfile. If you are going to change 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. to a hierarchical arrangement of objects, starting at request.app.root.
See help(cherrypy.dispatch) for more information.""" See help(cherrypy.dispatch) for more information."""
script_name = "" script_name = ''
""" """
The 'mount point' of the application which is handling this request. 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 "/"). 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 The 'relative path' portion of the Request-URI. This is relative
to the script_name ('mount point') of the application which is 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.""" This is useful when debugging a live server with hung requests."""
namespaces = _cpconfig.NamespaceSet( namespaces = _cpconfig.NamespaceSet(
**{"hooks": hooks_namespace, **{'hooks': hooks_namespace,
"request": request_namespace, 'request': request_namespace,
"response": response_namespace, 'response': response_namespace,
"error_page": error_page_namespace, 'error_page': error_page_namespace,
"tools": cherrypy.tools, 'tools': cherrypy.tools,
}) })
def __init__(self, local_host, remote_host, scheme="http", def __init__(self, local_host, remote_host, scheme='http',
server_protocol="HTTP/1.1"): server_protocol='HTTP/1.1'):
"""Populate a new Request object. """Populate a new Request object.
local_host should be an httputil.Host object with the server info. 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.error_response = cherrypy.HTTPError(500).set_response
self.method = method self.method = method
path = path or "/" path = path or '/'
self.query_string = query_string or '' self.query_string = query_string or ''
self.params = {} self.params = {}
@@ -600,11 +600,11 @@ class Request(object):
if self.show_tracebacks: if self.show_tracebacks:
body = format_exc() body = format_exc()
else: else:
body = "" body = ''
r = bare_error(body) r = bare_error(body)
response.output_status, response.header_list, response.body = r 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. # HEAD requests MUST NOT return a message-body in the response.
response.body = [] response.body = []
@@ -696,8 +696,8 @@ class Request(object):
self.query_string, encoding=self.query_string_encoding) self.query_string, encoding=self.query_string_encoding)
except UnicodeDecodeError: except UnicodeDecodeError:
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
404, "The given query string could not be processed. Query " 404, 'The given query string could not be processed. Query '
"strings for this resource must be encoded with %r." % 'strings for this resource must be encoded with %r.' %
self.query_string_encoding) self.query_string_encoding)
# Python 2 only: keyword arguments must be byte strings (type 'str'). # Python 2 only: keyword arguments must be byte strings (type 'str').
@@ -722,7 +722,7 @@ class Request(object):
# (AFAIK, only Konqueror does that), only the last one will # (AFAIK, only Konqueror does that), only the last one will
# remain in headers (but they will be correctly stored in # remain in headers (but they will be correctly stored in
# request.cookie). # request.cookie).
if "=?" in value: if '=?' in value:
dict.__setitem__(headers, name, httputil.decode_TEXT(value)) dict.__setitem__(headers, name, httputil.decode_TEXT(value))
else: else:
dict.__setitem__(headers, name, value) dict.__setitem__(headers, name, value)
@@ -733,7 +733,7 @@ class Request(object):
try: try:
self.cookie.load(value) self.cookie.load(value)
except CookieError: except CookieError:
msg = "Illegal cookie name %s" % value.split('=')[0] msg = 'Illegal cookie name %s' % value.split('=')[0]
raise cherrypy.HTTPError(400, msg) raise cherrypy.HTTPError(400, msg)
if not dict.__contains__(headers, 'Host'): if not dict.__contains__(headers, 'Host'):
@@ -746,7 +746,7 @@ class Request(object):
host = dict.get(headers, 'Host') host = dict.get(headers, 'Host')
if not host: if not host:
host = self.local.name or self.local.ip 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): def get_resource(self, path):
"""Call a dispatcher (which sets self.handler and .config). (Core)""" """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 # dispatchers can only be specified in app.config, not in _cp_config
# (since custom dispatchers may not even have an app.root). # (since custom dispatchers may not even have an app.root).
dispatch = self.app.find_config( dispatch = self.app.find_config(
path, "request.dispatch", self.dispatch) path, 'request.dispatch', self.dispatch)
# dispatch() should set self.handler and self.config # dispatch() should set self.handler and self.config
dispatch(path) dispatch(path)
@@ -762,10 +762,10 @@ class Request(object):
def handle_error(self): def handle_error(self):
"""Handle the last unanticipated exception. (Core)""" """Handle the last unanticipated exception. (Core)"""
try: try:
self.hooks.run("before_error_response") self.hooks.run('before_error_response')
if self.error_response: if self.error_response:
self.error_response() self.error_response()
self.hooks.run("after_error_response") self.hooks.run('after_error_response')
cherrypy.serving.response.finalize() cherrypy.serving.response.finalize()
except cherrypy.HTTPRedirect: except cherrypy.HTTPRedirect:
inst = sys.exc_info()[1] inst = sys.exc_info()[1]
@@ -776,8 +776,8 @@ class Request(object):
def _get_body_params(self): def _get_body_params(self):
warnings.warn( warnings.warn(
"body_params is deprecated in CherryPy 3.2, will be removed in " 'body_params is deprecated in CherryPy 3.2, will be removed in '
"CherryPy 3.3.", 'CherryPy 3.3.',
DeprecationWarning DeprecationWarning
) )
return self.body.params return self.body.params
@@ -800,8 +800,8 @@ class ResponseBody(object):
"""The body of the HTTP response (the response entity).""" """The body of the HTTP response (the response entity)."""
if six.PY3: if six.PY3:
unicode_err = ("Page handlers MUST return bytes. Use tools.encode " unicode_err = ('Page handlers MUST return bytes. Use tools.encode '
"if you wish to return unicode.") 'if you wish to return unicode.')
def __get__(self, obj, objclass=None): def __get__(self, obj, objclass=None):
if obj is None: if obj is None:
@@ -815,7 +815,7 @@ class ResponseBody(object):
if six.PY3 and isinstance(value, str): if six.PY3 and isinstance(value, str):
raise ValueError(self.unicode_err) 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 # strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character # item list is much faster than iterating over every character
# in a long string. # in a long string.
@@ -842,7 +842,7 @@ class Response(object):
"""An HTTP Response, including status, headers, and body.""" """An HTTP Response, including status, headers, and body."""
status = "" status = ''
"""The HTTP Status-Code and Reason-Phrase.""" """The HTTP Status-Code and Reason-Phrase."""
header_list = [] header_list = []
@@ -893,15 +893,15 @@ class Response(object):
# Since we know all our keys are titled strings, we can # Since we know all our keys are titled strings, we can
# bypass HeaderMap.update and get a big speed boost. # bypass HeaderMap.update and get a big speed boost.
dict.update(self.headers, { dict.update(self.headers, {
"Content-Type": 'text/html', 'Content-Type': 'text/html',
"Server": "CherryPy/" + cherrypy.__version__, 'Server': 'CherryPy/' + cherrypy.__version__,
"Date": httputil.HTTPDate(self.time), 'Date': httputil.HTTPDate(self.time),
}) })
self.cookie = SimpleCookie() self.cookie = SimpleCookie()
def collapse_body(self): def collapse_body(self):
"""Collapse self.body to a single string; replace it and return it.""" """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 return self.body
newbody = [] newbody = []
@@ -924,9 +924,9 @@ class Response(object):
headers = self.headers headers = self.headers
self.status = "%s %s" % (code, reason) self.status = '%s %s' % (code, reason)
self.output_status = ntob(str(code), 'ascii') + \ self.output_status = ntob(str(code), 'ascii') + \
ntob(" ") + headers.encode(reason) ntob(' ') + headers.encode(reason)
if self.stream: if self.stream:
# The upshot: wsgiserver will chunk the response if # The upshot: wsgiserver will chunk the response if
@@ -939,7 +939,7 @@ class Response(object):
# and 304 (not modified) responses MUST NOT # and 304 (not modified) responses MUST NOT
# include a message-body." # include a message-body."
dict.pop(headers, 'Content-Length', None) dict.pop(headers, 'Content-Length', None)
self.body = ntob("") self.body = ntob('')
else: else:
# Responses which are not streamed should have a Content-Length, # Responses which are not streamed should have a Content-Length,
# but allow user code to set Content-Length if desired. # but allow user code to set Content-Length if desired.
@@ -952,13 +952,10 @@ class Response(object):
cookie = self.cookie.output() cookie = self.cookie.output()
if cookie: if cookie:
for line in cookie.split("\n"): for line in cookie.split('\r\n'):
if line.endswith("\r"): name, value = line.split(': ', 1)
# Python 2.4 emits cookies joined by LF but 2.5+ by CRLF.
line = line[:-1]
name, value = line.split(": ", 1)
if isinstance(name, six.text_type): if isinstance(name, six.text_type):
name = name.encode("ISO-8859-1") name = name.encode('ISO-8859-1')
if isinstance(value, six.text_type): if isinstance(value, six.text_type):
value = headers.encode(value) value = headers.encode(value)
h.append((name, value)) h.append((name, value))
+13 -13
View File
@@ -3,8 +3,8 @@
import six import six
import cherrypy import cherrypy
from cherrypy.lib import attributes from cherrypy.lib.reprconf import attributes
from cherrypy._cpcompat import basestring from cherrypy._cpcompat import text_or_bytes
# We import * because we want to export check_port # We import * because we want to export check_port
# et al as attributes of this module. # et al as attributes of this module.
@@ -35,7 +35,7 @@ class Server(ServerAdapter):
if value == '': if value == '':
raise ValueError("The empty string ('') is not an allowed value. " raise ValueError("The empty string ('') is not an allowed value. "
"Use '0.0.0.0' instead to listen on all active " "Use '0.0.0.0' instead to listen on all active "
"interfaces (INADDR_ANY).") 'interfaces (INADDR_ANY).')
self._socket_host = value self._socket_host = value
socket_host = property( socket_host = property(
_get_socket_host, _get_socket_host,
@@ -156,7 +156,7 @@ class Server(ServerAdapter):
if httpserver is None: if httpserver is None:
from cherrypy import _cpwsgi_server from cherrypy import _cpwsgi_server
httpserver = _cpwsgi_server.CPWSGIServer(self) httpserver = _cpwsgi_server.CPWSGIServer(self)
if isinstance(httpserver, basestring): if isinstance(httpserver, text_or_bytes):
# Is anyone using this? Can I add an arg? # Is anyone using this? Can I add an arg?
httpserver = attributes(httpserver)(self) httpserver = attributes(httpserver)(self)
return httpserver, self.bind_addr return httpserver, self.bind_addr
@@ -180,7 +180,7 @@ class Server(ServerAdapter):
self.socket_file = None self.socket_file = None
self.socket_host = None self.socket_host = None
self.socket_port = None self.socket_port = None
elif isinstance(value, basestring): elif isinstance(value, text_or_bytes):
self.socket_file = value self.socket_file = value
self.socket_host = None self.socket_host = None
self.socket_port = None self.socket_port = None
@@ -189,9 +189,9 @@ class Server(ServerAdapter):
self.socket_host, self.socket_port = value self.socket_host, self.socket_port = value
self.socket_file = None self.socket_file = None
except ValueError: except ValueError:
raise ValueError("bind_addr must be a (host, port) tuple " raise ValueError('bind_addr must be a (host, port) tuple '
"(for TCP sockets) or a string (for Unix " '(for TCP sockets) or a string (for Unix '
"domain sockets), not %r" % value) 'domain sockets), not %r' % value)
bind_addr = property( bind_addr = property(
_get_bind_addr, _get_bind_addr,
_set_bind_addr, _set_bind_addr,
@@ -215,12 +215,12 @@ class Server(ServerAdapter):
port = self.socket_port port = self.socket_port
if self.ssl_certificate: if self.ssl_certificate:
scheme = "https" scheme = 'https'
if port != 443: if port != 443:
host += ":%s" % port host += ':%s' % port
else: else:
scheme = "http" scheme = 'http'
if port != 80: if port != 80:
host += ":%s" % port host += ':%s' % port
return "%s://%s" % (scheme, host) return '%s://%s' % (scheme, host)
-241
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
+37 -36
View File
@@ -28,6 +28,11 @@ import warnings
import cherrypy import cherrypy
from cherrypy._helper import expose 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): def _getargs(func):
"""Return the names of all static arguments to the given function.""" """Return the names of all static arguments to the given function."""
@@ -45,8 +50,8 @@ def _getargs(func):
_attr_error = ( _attr_error = (
"CherryPy Tools cannot be turned on directly. Instead, turn them " 'CherryPy Tools cannot be turned on directly. Instead, turn them '
"on via config, or use them as decorators on your page handlers." 'on via config, or use them as decorators on your page handlers.'
) )
@@ -57,7 +62,7 @@ class Tool(object):
help(tool.callable) should give you more information about this Tool. help(tool.callable) should give you more information about this Tool.
""" """
namespace = "tools" namespace = 'tools'
def __init__(self, point, callable, name=None, priority=50): def __init__(self, point, callable, name=None, priority=50):
self._point = point self._point = point
@@ -80,7 +85,7 @@ class Tool(object):
for arg in _getargs(self.callable): for arg in _getargs(self.callable):
setattr(self, arg, None) setattr(self, arg, None)
except (TypeError, AttributeError): except (TypeError, AttributeError):
if hasattr(self.callable, "__call__"): if hasattr(self.callable, '__call__'):
for arg in _getargs(self.callable.__call__): for arg in _getargs(self.callable.__call__):
setattr(self, arg, None) setattr(self, arg, None)
# IronPython 1.0 raises NotImplementedError because # IronPython 1.0 raises NotImplementedError because
@@ -104,8 +109,8 @@ class Tool(object):
if self._name in tm: if self._name in tm:
conf.update(tm[self._name]) conf.update(tm[self._name])
if "on" in conf: if 'on' in conf:
del conf["on"] del conf['on']
return conf return conf
@@ -120,15 +125,15 @@ class Tool(object):
return cherrypy.request.base return cherrypy.request.base
""" """
if args: if args:
raise TypeError("The %r Tool does not accept positional " raise TypeError('The %r Tool does not accept positional '
"arguments; you must use keyword arguments." 'arguments; you must use keyword arguments.'
% self._name) % self._name)
def tool_decorator(f): def tool_decorator(f):
if not hasattr(f, "_cp_config"): if not hasattr(f, '_cp_config'):
f._cp_config = {} f._cp_config = {}
subspace = self.namespace + "." + self._name + "." subspace = self.namespace + '.' + self._name + '.'
f._cp_config[subspace + "on"] = True f._cp_config[subspace + 'on'] = True
for k, v in kwargs.items(): for k, v in kwargs.items():
f._cp_config[subspace + k] = v f._cp_config[subspace + k] = v
return f return f
@@ -141,9 +146,9 @@ class Tool(object):
method when the tool is "turned on" in config. method when the tool is "turned on" in config.
""" """
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop('priority', None)
if p is 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, cherrypy.serving.request.hooks.attach(self._point, self.callable,
priority=p, **conf) priority=p, **conf)
@@ -191,9 +196,9 @@ class HandlerTool(Tool):
method when the tool is "turned on" in config. method when the tool is "turned on" in config.
""" """
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop('priority', None)
if p is 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, cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
priority=p, **conf) priority=p, **conf)
@@ -254,11 +259,6 @@ class ErrorTool(Tool):
# Builtin tools # # 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): class SessionTool(Tool):
@@ -296,9 +296,9 @@ class SessionTool(Tool):
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop('priority', None)
if p is 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) hooks.attach(self._point, self.callable, priority=p, **conf)
@@ -374,7 +374,7 @@ class XMLRPCController(object):
for attr in str(rpcmethod).split('.'): for attr in str(rpcmethod).split('.'):
subhandler = getattr(subhandler, attr, None) subhandler = getattr(subhandler, attr, None)
if subhandler and getattr(subhandler, "exposed", False): if subhandler and getattr(subhandler, 'exposed', False):
body = subhandler(*(vpath + rpcparams), **params) body = subhandler(*(vpath + rpcparams), **params)
else: else:
@@ -384,7 +384,7 @@ class XMLRPCController(object):
# cherrypy.lib.xmlrpcutil.on_error # cherrypy.lib.xmlrpcutil.on_error
raise Exception('method "%s" is not supported' % attr) 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, _xmlrpc.respond(body,
conf.get('encoding', 'utf-8'), conf.get('encoding', 'utf-8'),
conf.get('allow_none', 0)) conf.get('allow_none', 0))
@@ -395,7 +395,7 @@ class SessionAuthTool(HandlerTool):
def _setargs(self): def _setargs(self):
for name in dir(cptools.SessionAuth): for name in dir(cptools.SessionAuth):
if not name.startswith("__"): if not name.startswith('__'):
setattr(self, name, None) setattr(self, name, None)
@@ -418,7 +418,7 @@ class CachingTool(Tool):
"""Hook caching into cherrypy.request.""" """Hook caching into cherrypy.request."""
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop('priority', None)
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
priority=p, **conf) priority=p, **conf)
@@ -447,7 +447,7 @@ class Toolbox(object):
cherrypy.serving.request.toolmaps[self.namespace] = map = {} cherrypy.serving.request.toolmaps[self.namespace] = map = {}
def populate(k, v): def populate(k, v):
toolname, arg = k.split(".", 1) toolname, arg = k.split('.', 1)
bucket = map.setdefault(toolname, {}) bucket = map.setdefault(toolname, {})
bucket[arg] = v bucket[arg] = v
return populate return populate
@@ -457,7 +457,7 @@ class Toolbox(object):
map = cherrypy.serving.request.toolmaps.get(self.namespace) map = cherrypy.serving.request.toolmaps.get(self.namespace)
if map: if map:
for name, settings in map.items(): for name, settings in map.items():
if settings.get("on", False): if settings.get('on', False):
tool = getattr(self, name) tool = getattr(self, name)
tool._setup() tool._setup()
@@ -472,7 +472,7 @@ class Toolbox(object):
class DeprecatedTool(Tool): class DeprecatedTool(Tool):
_name = None _name = None
warnmsg = "This Tool is deprecated." warnmsg = 'This Tool is deprecated.'
def __init__(self, point, warnmsg=None): def __init__(self, point, warnmsg=None):
self.point = point self.point = point
@@ -490,7 +490,7 @@ class DeprecatedTool(Tool):
warnings.warn(self.warnmsg) warnings.warn(self.warnmsg)
default_toolbox = _d = Toolbox("tools") default_toolbox = _d = Toolbox('tools')
_d.session_auth = SessionAuthTool(cptools.session_auth) _d.session_auth = SessionAuthTool(cptools.session_auth)
_d.allow = Tool('on_start_resource', cptools.allow) _d.allow = Tool('on_start_resource', cptools.allow)
_d.proxy = Tool('before_request_body', cptools.proxy, priority=30) _d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
@@ -512,14 +512,14 @@ _d.caching = CachingTool('before_handler', _caching.get, 'caching')
_d.expires = Tool('before_finalize', _caching.expires) _d.expires = Tool('before_finalize', _caching.expires)
_d.tidy = DeprecatedTool( _d.tidy = DeprecatedTool(
'before_finalize', 'before_finalize',
"The tidy tool has been removed from the standard distribution of " 'The tidy tool has been removed from the standard distribution of '
"CherryPy. The most recent version can be found at " 'CherryPy. The most recent version can be found at '
"http://tools.cherrypy.org/browser.") 'http://tools.cherrypy.org/browser.')
_d.nsgmls = DeprecatedTool( _d.nsgmls = DeprecatedTool(
'before_finalize', 'before_finalize',
"The nsgmls tool has been removed from the standard distribution of " 'The nsgmls tool has been removed from the standard distribution of '
"CherryPy. The most recent version can be found at " 'CherryPy. The most recent version can be found at '
"http://tools.cherrypy.org/browser.") 'http://tools.cherrypy.org/browser.')
_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers) _d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
_d.referer = Tool('before_request_body', cptools.referer) _d.referer = Tool('before_request_body', cptools.referer)
_d.basic_auth = Tool('on_start_resource', auth.basic_auth) _d.basic_auth = Tool('on_start_resource', auth.basic_auth)
@@ -533,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.json_out = Tool('before_handler', jsontools.json_out, priority=30)
_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1) _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.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 del _d, cptools, encoding, auth, static
+30 -30
View File
@@ -46,22 +46,22 @@ class Application(object):
relative_urls = False 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.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
self.root = root self.root = root
self.script_name = script_name self.script_name = script_name
self.wsgiapp = _cpwsgi.CPWSGIApp(self) self.wsgiapp = _cpwsgi.CPWSGIApp(self)
self.namespaces = self.namespaces.copy() self.namespaces = self.namespaces.copy()
self.namespaces["log"] = lambda k, v: setattr(self.log, k, v) self.namespaces['log'] = lambda k, v: setattr(self.log, k, v)
self.namespaces["wsgi"] = self.wsgiapp.namespace_handler self.namespaces['wsgi'] = self.wsgiapp.namespace_handler
self.config = self.__class__.config.copy() self.config = self.__class__.config.copy()
if config: if config:
self.merge(config) self.merge(config)
def __repr__(self): 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) self.root, self.script_name)
script_name_doc = """The URI "mount point" for this app. A mount point script_name_doc = """The URI "mount point" for this app. A mount point
@@ -86,11 +86,11 @@ class Application(object):
# A `_script_name` with a value of None signals that the script name # A `_script_name` with a value of None signals that the script name
# should be pulled from WSGI environ. # 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): def _set_script_name(self, value):
if value: if value:
value = value.rstrip("/") value = value.rstrip('/')
self._script_name = value self._script_name = value
script_name = property(fget=_get_script_name, fset=_set_script_name, script_name = property(fget=_get_script_name, fset=_set_script_name,
doc=script_name_doc) doc=script_name_doc)
@@ -100,22 +100,22 @@ class Application(object):
_cpconfig.merge(self.config, config) _cpconfig.merge(self.config, config)
# Handle namespaces specified in config. # Handle namespaces specified in config.
self.namespaces(self.config.get("/", {})) self.namespaces(self.config.get('/', {}))
def find_config(self, path, key, default=None): def find_config(self, path, key, default=None):
"""Return the most-specific value for key along path, or default.""" """Return the most-specific value for key along path, or default."""
trail = path or "/" trail = path or '/'
while trail: while trail:
nodeconf = self.config.get(trail, {}) nodeconf = self.config.get(trail, {})
if key in nodeconf: if key in nodeconf:
return nodeconf[key] return nodeconf[key]
lastslash = trail.rfind("/") lastslash = trail.rfind('/')
if lastslash == -1: if lastslash == -1:
break break
elif lastslash == 0 and trail != "/": elif lastslash == 0 and trail != '/':
trail = "/" trail = '/'
else: else:
trail = trail[:lastslash] trail = trail[:lastslash]
@@ -172,7 +172,7 @@ class Tree(object):
def __init__(self): def __init__(self):
self.apps = {} 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. """Mount a new app from a root object, script_name, and config.
root root
@@ -197,29 +197,29 @@ class Tree(object):
if script_name is None: if script_name is None:
raise TypeError( raise TypeError(
"The 'script_name' argument may not be None. Application " "The 'script_name' argument may not be None. Application "
"objects may, however, possess a script_name of None (in " 'objects may, however, possess a script_name of None (in '
"order to inpect the WSGI environ for SCRIPT_NAME upon each " 'order to inpect the WSGI environ for SCRIPT_NAME upon each '
"request). You cannot mount such Applications on this Tree; " 'request). You cannot mount such Applications on this Tree; '
"you must pass them to a WSGI server interface directly.") 'you must pass them to a WSGI server interface directly.')
# Next line both 1) strips trailing slash and 2) maps "/" -> "". # Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/") script_name = script_name.rstrip('/')
if isinstance(root, Application): if isinstance(root, Application):
app = root app = root
if script_name != "" and script_name != app.script_name: if script_name != '' and script_name != app.script_name:
raise ValueError( raise ValueError(
"Cannot specify a different script name and pass an " 'Cannot specify a different script name and pass an '
"Application instance to cherrypy.mount") 'Application instance to cherrypy.mount')
script_name = app.script_name script_name = app.script_name
else: else:
app = Application(root, script_name) app = Application(root, script_name)
# If mounted at "", add favicon.ico # If mounted at "", add favicon.ico
if (script_name == "" and root is not None if (script_name == '' and root is not None
and not hasattr(root, "favicon_ico")): and not hasattr(root, 'favicon_ico')):
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__), favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
"favicon.ico") 'favicon.ico')
root.favicon_ico = tools.staticfile.handler(favicon) root.favicon_ico = tools.staticfile.handler(favicon)
if config: if config:
@@ -229,10 +229,10 @@ class Tree(object):
return app return app
def graft(self, wsgi_callable, script_name=""): def graft(self, wsgi_callable, script_name=''):
"""Mount a wsgi callable at the given script_name.""" """Mount a wsgi callable at the given script_name."""
# Next line both 1) strips trailing slash and 2) maps "/" -> "". # 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 self.apps[script_name] = wsgi_callable
def script_name(self, path=None): def script_name(self, path=None):
@@ -252,11 +252,11 @@ class Tree(object):
if path in self.apps: if path in self.apps:
return path return path
if path == "": if path == '':
return None return None
# Move one node up the tree and try again. # Move one node up the tree and try again.
path = path[:path.rfind("/")] path = path[:path.rfind('/')]
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
# If you're calling this, then you're probably setting SCRIPT_NAME # If you're calling this, then you're probably setting SCRIPT_NAME
@@ -267,7 +267,7 @@ class Tree(object):
env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
env1x.get('PATH_INFO', '')) env1x.get('PATH_INFO', ''))
sn = self.script_name(path or "/") sn = self.script_name(path or '/')
if sn is None: if sn is None:
start_response('404 Not Found', []) start_response('404 Not Found', [])
return [] return []
@@ -280,8 +280,8 @@ class Tree(object):
# Python 2/WSGI u.0: all strings MUST be of type unicode # Python 2/WSGI u.0: all strings MUST be of type unicode
enc = environ[ntou('wsgi.url_encoding')] enc = environ[ntou('wsgi.url_encoding')]
environ[ntou('SCRIPT_NAME')] = sn.decode(enc) environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
environ[ntou('PATH_INFO')] = path[len(sn.rstrip("/")):].decode(enc) environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc)
else: else:
environ['SCRIPT_NAME'] = sn environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip("/")):] environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
return app(environ, start_response) return app(environ, start_response)
+95 -71
View File
@@ -46,10 +46,13 @@ class VirtualHost(object):
Domain2App = cherrypy.Application(root) Domain2App = cherrypy.Application(root)
SecureApp = cherrypy.Application(Secure()) SecureApp = cherrypy.Application(Secure())
vhost = cherrypy._cpwsgi.VirtualHost(RootApp, vhost = cherrypy._cpwsgi.VirtualHost(
domains={'www.domain2.example': Domain2App, RootApp,
'www.domain2.example:443': SecureApp, domains={
}) 'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
},
)
cherrypy.tree.graft(vhost) cherrypy.tree.graft(vhost)
""" """
@@ -78,7 +81,7 @@ class VirtualHost(object):
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
domain = environ.get('HTTP_HOST', '') domain = environ.get('HTTP_HOST', '')
if self.use_x_forwarded_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) nextapp = self.domains.get(domain)
if nextapp is None: if nextapp is None:
@@ -109,7 +112,7 @@ class InternalRedirector(object):
# Add the *previous* path_info + qs to redirections. # Add the *previous* path_info + qs to redirections.
old_uri = sn + path old_uri = sn + path
if qs: if qs:
old_uri += "?" + qs old_uri += '?' + qs
redirections.append(old_uri) redirections.append(old_uri)
if not self.recursive: if not self.recursive:
@@ -117,18 +120,20 @@ class InternalRedirector(object):
# already # already
new_uri = sn + ir.path new_uri = sn + ir.path
if ir.query_string: if ir.query_string:
new_uri += "?" + ir.query_string new_uri += '?' + ir.query_string
if new_uri in redirections: if new_uri in redirections:
ir.request.close() ir.request.close()
raise RuntimeError("InternalRedirector visited the " tmpl = (
"same URL twice: %r" % new_uri) 'InternalRedirector visited the same URL twice: %r'
)
raise RuntimeError(tmpl % new_uri)
# Munge the environment and try again. # Munge the environment and try again.
environ['REQUEST_METHOD'] = "GET" environ['REQUEST_METHOD'] = 'GET'
environ['PATH_INFO'] = ir.path environ['PATH_INFO'] = ir.path
environ['QUERY_STRING'] = ir.query_string environ['QUERY_STRING'] = ir.query_string
environ['wsgi.input'] = io.BytesIO() environ['wsgi.input'] = io.BytesIO()
environ['CONTENT_LENGTH'] = "0" environ['CONTENT_LENGTH'] = '0'
environ['cherrypy.previous_request'] = ir.request environ['cherrypy.previous_request'] = ir.request
@@ -160,7 +165,8 @@ class _TrappedResponse(object):
self.throws = throws self.throws = throws
self.started_response = False self.started_response = False
self.response = self.trap( self.response = self.trap(
self.nextapp, self.environ, self.start_response) self.nextapp, self.environ, self.start_response,
)
self.iter_response = iter(self.response) self.iter_response = iter(self.response)
def __iter__(self): def __iter__(self):
@@ -187,16 +193,17 @@ class _TrappedResponse(object):
raise raise
except: except:
tb = _cperror.format_exc() tb = _cperror.format_exc()
#print('trapped (started %s):' % self.started_response, tb)
_cherrypy.log(tb, severity=40) _cherrypy.log(tb, severity=40)
if not _cherrypy.request.show_tracebacks: if not _cherrypy.request.show_tracebacks:
tb = "" tb = ''
s, h, b = _cperror.bare_error(tb) s, h, b = _cperror.bare_error(tb)
if six.PY3: if six.PY3:
# What fun. # What fun.
s = s.decode('ISO-8859-1') s = s.decode('ISO-8859-1')
h = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) h = [
for k, v in h] (k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in h
]
if self.started_response: if self.started_response:
# Empty our iterable (so future calls raise StopIteration) # Empty our iterable (so future calls raise StopIteration)
self.iter_response = iter([]) self.iter_response = iter([])
@@ -215,7 +222,7 @@ class _TrappedResponse(object):
raise raise
if self.started_response: if self.started_response:
return ntob("").join(b) return ntob('').join(b)
else: else:
return b return b
@@ -240,18 +247,18 @@ class AppResponse(object):
outstatus = r.output_status outstatus = r.output_status
if not isinstance(outstatus, bytes): if not isinstance(outstatus, bytes):
raise TypeError("response.output_status is not a byte string.") raise TypeError('response.output_status is not a byte string.')
outheaders = [] outheaders = []
for k, v in r.header_list: for k, v in r.header_list:
if not isinstance(k, bytes): if not isinstance(k, bytes):
raise TypeError( tmpl = 'response.header_list key %r is not a byte string.'
"response.header_list key %r is not a byte string." % raise TypeError(tmpl % k)
k)
if not isinstance(v, bytes): if not isinstance(v, bytes):
raise TypeError( tmpl = (
"response.header_list value %r is not a byte string." % 'response.header_list value %r is not a byte string.'
v) )
raise TypeError(tmpl % v)
outheaders.append((k, v)) outheaders.append((k, v))
if six.PY3: if six.PY3:
@@ -260,8 +267,10 @@ class AppResponse(object):
# that is, they must be of type "str" but are restricted to # that is, they must be of type "str" but are restricted to
# code points in the "latin-1" set. # code points in the "latin-1" set.
outstatus = outstatus.decode('ISO-8859-1') outstatus = outstatus.decode('ISO-8859-1')
outheaders = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) outheaders = [
for k, v in outheaders] (k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in outheaders
]
self.iter_response = iter(r.body) self.iter_response = iter(r.body)
self.write = start_response(outstatus, outheaders) self.write = start_response(outstatus, outheaders)
@@ -299,14 +308,18 @@ class AppResponse(object):
"""Create a Request object using environ.""" """Create a Request object using environ."""
env = self.environ.get env = self.environ.get
local = httputil.Host('', local = httputil.Host(
int(env('SERVER_PORT', 80) or -1), '',
env('SERVER_NAME', '')) int(env('SERVER_PORT', 80) or -1),
remote = httputil.Host(env('REMOTE_ADDR', ''), env('SERVER_NAME', ''),
int(env('REMOTE_PORT', -1) or -1), )
env('REMOTE_HOST', '')) remote = httputil.Host(
env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1) or -1),
env('REMOTE_HOST', ''),
)
scheme = env('wsgi.url_scheme') 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) request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
# LOGON_USER is served by IIS, and is the name of the # LOGON_USER is served by IIS, and is the name of the
@@ -320,44 +333,54 @@ class AppResponse(object):
meth = self.environ['REQUEST_METHOD'] meth = self.environ['REQUEST_METHOD']
path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''), path = httputil.urljoin(
self.environ.get('PATH_INFO', '')) self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', ''),
)
qs = self.environ.get('QUERY_STRING', '') qs = self.environ.get('QUERY_STRING', '')
if six.PY3: path, qs = self.recode_path_qs(path, qs) or (path, qs)
# 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
rproto = self.environ.get('SERVER_PROTOCOL') rproto = self.environ.get('SERVER_PROTOCOL')
headers = self.translate_headers(self.environ) headers = self.translate_headers(self.environ)
rfile = self.environ['wsgi.input'] rfile = self.environ['wsgi.input']
request.run(meth, path, qs, rproto, headers, rfile) request.run(meth, path, qs, rproto, headers, rfile)
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', headerNames = {
'CONTENT_LENGTH': 'Content-Length', 'HTTP_CGI_AUTHORIZATION': 'Authorization',
'CONTENT_TYPE': 'Content-Type', 'CONTENT_LENGTH': 'Content-Length',
'REMOTE_HOST': 'Remote-Host', 'CONTENT_TYPE': 'Content-Type',
'REMOTE_ADDR': 'Remote-Addr', '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): def translate_headers(self, environ):
"""Translate CGI-environ header names to HTTP header names.""" """Translate CGI-environ header names to HTTP header names."""
@@ -365,9 +388,9 @@ class AppResponse(object):
# We assume all incoming header keys are uppercase already. # We assume all incoming header keys are uppercase already.
if cgiName in self.headerNames: if cgiName in self.headerNames:
yield self.headerNames[cgiName], environ[cgiName] yield self.headerNames[cgiName], environ[cgiName]
elif cgiName[:5] == "HTTP_": elif cgiName[:5] == 'HTTP_':
# Hackish attempt at recovering original header names. # Hackish attempt at recovering original header names.
translatedHeader = cgiName[5:].replace("_", "-") translatedHeader = cgiName[5:].replace('_', '-')
yield translatedHeader, environ[cgiName] yield translatedHeader, environ[cgiName]
@@ -375,9 +398,10 @@ class CPWSGIApp(object):
"""A WSGI application object for a CherryPy Application.""" """A WSGI application object for a CherryPy Application."""
pipeline = [('ExceptionTrapper', ExceptionTrapper), pipeline = [
('InternalRedirector', InternalRedirector), ('ExceptionTrapper', ExceptionTrapper),
] ('InternalRedirector', InternalRedirector),
]
"""A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
constructor that takes an initial, positional 'nextapp' argument, constructor that takes an initial, positional 'nextapp' argument,
plus optional keyword arguments, and returns a WSGI application plus optional keyword arguments, and returns a WSGI application
@@ -427,16 +451,16 @@ class CPWSGIApp(object):
def namespace_handler(self, k, v): def namespace_handler(self, k, v):
"""Config handler for the 'wsgi' namespace.""" """Config handler for the 'wsgi' namespace."""
if k == "pipeline": if k == 'pipeline':
# Note this allows multiple 'wsgi.pipeline' config entries # Note this allows multiple 'wsgi.pipeline' config entries
# (but each entry will be processed in a 'random' order). # (but each entry will be processed in a 'random' order).
# It should also allow developers to set default middleware # It should also allow developers to set default middleware
# in code (passed to self.__init__) that deployers can add to # in code (passed to self.__init__) that deployers can add to
# (but not remove) via config. # (but not remove) via config.
self.pipeline.extend(v) self.pipeline.extend(v)
elif k == "response_class": elif k == 'response_class':
self.response_class = v self.response_class = v
else: else:
name, arg = k.split(".", 1) name, arg = k.split('.', 1)
bucket = self.config.setdefault(name, {}) bucket = self.config.setdefault(name, {})
bucket[arg] = v bucket[arg] = v
+1 -1
View File
@@ -66,5 +66,5 @@ class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
self.stats['Enabled'] = getattr( self.stats['Enabled'] = getattr(
self.server_adapter, 'statistics', False) 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) cherrypy.engine.log(msg, level, traceback)
+9 -9
View File
@@ -5,7 +5,7 @@ Helper functions for CP apps
import six import six
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
from cherrypy._cpcompat import basestring from cherrypy._cpcompat import text_or_bytes
import cherrypy import cherrypy
@@ -17,11 +17,11 @@ def expose(func=None, alias=None):
def expose_(func): def expose_(func):
func.exposed = True func.exposed = True
if alias is not None: if alias is not None:
if isinstance(alias, basestring): if isinstance(alias, text_or_bytes):
parents[alias.replace(".", "_")] = func parents[alias.replace('.', '_')] = func
else: else:
for a in alias: for a in alias:
parents[a.replace(".", "_")] = func parents[a.replace('.', '_')] = func
return func return func
import sys import sys
@@ -191,7 +191,7 @@ def popargs(*args, **kwargs):
return decorated return decorated
def url(path="", qs="", script_name=None, base=None, relative=None): def url(path='', qs='', script_name=None, base=None, relative=None):
"""Create an absolute URL for the given path. """Create an absolute URL for the given path.
If 'path' starts with a slash ('/'), this will return If 'path' starts with a slash ('/'), this will return
@@ -224,7 +224,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
qs = '?' + qs qs = '?' + qs
if cherrypy.request.app: if cherrypy.request.app:
if not path.startswith("/"): if not path.startswith('/'):
# Append/remove trailing slash from path_info as needed # Append/remove trailing slash from path_info as needed
# (this is to support mistyped URL's without redirecting; # (this is to support mistyped URL's without redirecting;
# if you want to redirect, use tools.trailing_slash). # if you want to redirect, use tools.trailing_slash).
@@ -236,7 +236,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
if pi.endswith('/') and pi != '/': if pi.endswith('/') and pi != '/':
pi = pi[:-1] pi = pi[:-1]
if path == "": if path == '':
path = pi path = pi
else: else:
path = _urljoin(pi, path) path = _urljoin(pi, path)
@@ -255,7 +255,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
if base is None: if base is None:
base = cherrypy.server.base() base = cherrypy.server.base()
path = (script_name or "") + path path = (script_name or '') + path
newurl = base + path + qs newurl = base + path + qs
if './' in newurl: if './' in newurl:
@@ -273,7 +273,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
# At this point, we should have a fully-qualified absolute URL. # At this point, we should have a fully-qualified absolute URL.
if relative is None: if relative is None:
relative = getattr(cherrypy.request.app, "relative_urls", False) relative = getattr(cherrypy.request.app, 'relative_urls', False)
# See http://www.ietf.org/rfc/rfc2396.txt # See http://www.ietf.org/rfc/rfc2396.txt
if relative == 'server': if relative == 'server':
+23 -23
View File
@@ -13,7 +13,7 @@ def start(configfiles=None, daemonize=False, environment=None,
"""Subscribe all engine plugins and start the engine.""" """Subscribe all engine plugins and start the engine."""
sys.path = [''] + sys.path sys.path = [''] + sys.path
for i in imports or []: for i in imports or []:
exec("import %s" % i) exec('import %s' % i)
for c in configfiles or []: for c in configfiles or []:
cherrypy.config.update(c) cherrypy.config.update(c)
@@ -37,18 +37,18 @@ def start(configfiles=None, daemonize=False, environment=None,
if pidfile: if pidfile:
plugins.PIDFile(engine, pidfile).subscribe() plugins.PIDFile(engine, pidfile).subscribe()
if hasattr(engine, "signal_handler"): if hasattr(engine, 'signal_handler'):
engine.signal_handler.subscribe() engine.signal_handler.subscribe()
if hasattr(engine, "console_control_handler"): if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.subscribe() engine.console_control_handler.subscribe()
if (fastcgi and (scgi or cgi)) or (scgi and cgi): if (fastcgi and (scgi or cgi)) or (scgi and cgi):
cherrypy.log.error("You may only specify one of the cgi, fastcgi, and " cherrypy.log.error('You may only specify one of the cgi, fastcgi, and '
"scgi options.", 'ENGINE') 'scgi options.', 'ENGINE')
sys.exit(1) sys.exit(1)
elif fastcgi or scgi or cgi: elif fastcgi or scgi or cgi:
# Turn off autoreload when using *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). # Turn off the default HTTP server (which is subscribed by default).
cherrypy.server.unsubscribe() cherrypy.server.unsubscribe()
@@ -76,25 +76,25 @@ def run():
from optparse import OptionParser from optparse import OptionParser
p = OptionParser() p = OptionParser()
p.add_option('-c', '--config', action="append", dest='config', p.add_option('-c', '--config', action='append', dest='config',
help="specify config file(s)") help='specify config file(s)')
p.add_option('-d', action="store_true", dest='daemonize', p.add_option('-d', action='store_true', dest='daemonize',
help="run the server as a daemon") help='run the server as a daemon')
p.add_option('-e', '--environment', dest='environment', default=None, p.add_option('-e', '--environment', dest='environment', default=None,
help="apply the given config environment") help='apply the given config environment')
p.add_option('-f', action="store_true", dest='fastcgi', p.add_option('-f', action='store_true', dest='fastcgi',
help="start a fastcgi server instead of the default HTTP " help='start a fastcgi server instead of the default HTTP '
"server") 'server')
p.add_option('-s', action="store_true", dest='scgi', p.add_option('-s', action='store_true', dest='scgi',
help="start a scgi server instead of the default HTTP server") help='start a scgi server instead of the default HTTP server')
p.add_option('-x', action="store_true", dest='cgi', p.add_option('-x', action='store_true', dest='cgi',
help="start a cgi server instead of the default HTTP server") help='start a cgi server instead of the default HTTP server')
p.add_option('-i', '--import', action="append", dest='imports', p.add_option('-i', '--import', action='append', dest='imports',
help="specify modules to import") help='specify modules to import')
p.add_option('-p', '--pidfile', dest='pidfile', default=None, p.add_option('-p', '--pidfile', dest='pidfile', default=None,
help="store the process id in the given file") help='store the process id in the given file')
p.add_option('-P', '--Path', action="append", dest='Path', p.add_option('-P', '--Path', action='append', dest='Path',
help="add the given paths to sys.path") help='add the given paths to sys.path')
options, args = p.parse_args() options, args = p.parse_args()
if options.Path: if options.Path:
+11 -11
View File
@@ -1,7 +1,5 @@
"""CherryPy Library""" """CherryPy Library"""
# Deprecated in CherryPy 3.2 -- remove in CherryPy 3.3
from cherrypy.lib.reprconf import unrepr, modules, attributes
def is_iterator(obj): def is_iterator(obj):
'''Returns a boolean indicating if the object provided implements '''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 # Types which implement the protocol must return themselves when
# invoking 'iter' upon them. # invoking 'iter' upon them.
return iter(obj) is obj return iter(obj) is obj
def is_closable_iterator(obj): def is_closable_iterator(obj):
# Not an iterator. # Not an iterator.
if not is_iterator(obj): if not is_iterator(obj):
return False return False
# A generator - the easiest thing to deal with. # A generator - the easiest thing to deal with.
import inspect import inspect
if inspect.isgenerator(obj): if inspect.isgenerator(obj):
return True return True
# A custom iterator. Look for a close method... # A custom iterator. Look for a close method...
if not (hasattr(obj, 'close') and callable(obj.close)): if not (hasattr(obj, 'close') and callable(obj.close)):
return False return False
# ... which doesn't require any arguments. # ... which doesn't require any arguments.
try: try:
inspect.getcallargs(obj.close) inspect.getcallargs(obj.close)
@@ -40,6 +39,7 @@ def is_closable_iterator(obj):
else: else:
return True return True
class file_generator(object): class file_generator(object):
"""Yield the given input (a file object) in chunks (default 64k). (Core)""" """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): def set_vary_header(response, header_name):
"Add a Vary header to a response" 'Add a Vary header to a response'
varies = response.headers.get("Vary", "") varies = response.headers.get('Vary', '')
varies = [x.strip() for x in varies.split(",") if x.strip()] varies = [x.strip() for x in varies.split(',') if x.strip()]
if header_name not in varies: if header_name not in varies:
varies.append(header_name) varies.append(header_name)
response.headers['Vary'] = ", ".join(varies) response.headers['Vary'] = ', '.join(varies)
+8 -8
View File
@@ -22,25 +22,25 @@ def check_auth(users, encrypt=None, realm=None):
if not isinstance(users, dict): if not isinstance(users, dict):
raise ValueError( raise ValueError(
"Authentication users must be a dictionary") 'Authentication users must be a dictionary')
# fetch the user password # fetch the user password
password = users.get(ah["username"], None) password = users.get(ah['username'], None)
except TypeError: except TypeError:
# returns a password (encrypted or clear text) # returns a password (encrypted or clear text)
password = users(ah["username"]) password = users(ah['username'])
else: else:
if not isinstance(users, dict): 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 # fetch the user password
password = users.get(ah["username"], None) password = users.get(ah['username'], None)
# validate the authorization by re-computing it here # validate the authorization by re-computing it here
# and compare it with what the user-agent provided # and compare it with what the user-agent provided
if httpauth.checkResponse(ah, password, method=request.method, if httpauth.checkResponse(ah, password, method=request.method,
encrypt=encrypt, realm=realm): encrypt=encrypt, realm=realm):
request.login = ah["username"] request.login = ah['username']
return True return True
request.login = False request.login = False
@@ -72,7 +72,7 @@ def basic_auth(realm, users, encrypt=None, debug=False):
'www-authenticate'] = httpauth.basicAuth(realm) 'www-authenticate'] = httpauth.basicAuth(realm)
raise cherrypy.HTTPError( 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): def digest_auth(realm, users, debug=False):
@@ -94,4 +94,4 @@ def digest_auth(realm, users, debug=False):
'www-authenticate'] = httpauth.digestAuth(realm) 'www-authenticate'] = httpauth.digestAuth(realm)
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
401, "You are not authorized to access that resource") 401, 'You are not authorized to access that resource')
+9 -9
View File
@@ -2,6 +2,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=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 __doc__ = """This module provides a CherryPy 3.x tool which implements
the server-side of HTTP Basic Access Authentication, as described in the server-side of HTTP Basic Access Authentication, as described in
:rfc:`2617`. :rfc:`2617`.
@@ -22,10 +28,6 @@ as the credentials store::
__author__ = 'visteya' __author__ = 'visteya'
__date__ = 'April 2009' __date__ = 'April 2009'
import binascii
from cherrypy._cpcompat import base64_decode
import cherrypy
def checkpassword_dict(user_password_dict): def checkpassword_dict(user_password_dict):
"""Returns a checkpassword function which checks credentials """Returns a checkpassword function which checks credentials
@@ -70,7 +72,8 @@ def basic_auth(realm, checkpassword, debug=False):
auth_header = request.headers.get('authorization') auth_header = request.headers.get('authorization')
if auth_header is not None: 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) scheme, params = auth_header.split(' ', 1)
if scheme.lower() == 'basic': if scheme.lower() == 'basic':
username, password = base64_decode(params).split(':', 1) 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') cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
request.login = username request.login = username
return # successful authentication 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 # Respond with 401 status and a WWW-Authenticate header
cherrypy.serving.response.headers[ cherrypy.serving.response.headers[
'www-authenticate'] = 'Basic realm="%s"' % realm 'www-authenticate'] = 'Basic realm="%s"' % realm
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
401, "You are not authorized to access that resource") 401, 'You are not authorized to access that resource')
+25 -26
View File
@@ -2,6 +2,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=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 __doc__ = """An implementation of the server-side of HTTP Digest Access
Authentication, which is described in :rfc:`2617`. Authentication, which is described in :rfc:`2617`.
@@ -22,12 +29,6 @@ __author__ = 'visteya'
__date__ = 'April 2009' __date__ = 'April 2009'
import time
from hashlib import md5
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
import cherrypy
from cherrypy._cpcompat import ntob
md5_hex = lambda s: md5(ntob(s)).hexdigest() md5_hex = lambda s: md5(ntob(s)).hexdigest()
qop_auth = 'auth' qop_auth = 'auth'
@@ -142,7 +143,7 @@ class HttpDigestAuthorization (object):
def __init__(self, auth_header, http_method, debug=False): def __init__(self, auth_header, http_method, debug=False):
self.http_method = http_method self.http_method = http_method
self.debug = debug self.debug = debug
scheme, params = auth_header.split(" ", 1) scheme, params = auth_header.split(' ', 1)
self.scheme = scheme.lower() self.scheme = scheme.lower()
if self.scheme != 'digest': if self.scheme != 'digest':
raise ValueError('Authorization scheme is not "Digest"') raise ValueError('Authorization scheme is not "Digest"')
@@ -180,7 +181,7 @@ class HttpDigestAuthorization (object):
) )
if not has_reqd: if not has_reqd:
raise ValueError( raise ValueError(
self.errmsg("Not all required parameters are present.")) self.errmsg('Not all required parameters are present.'))
if self.qop: if self.qop:
if self.qop not in valid_qops: if self.qop not in valid_qops:
@@ -188,13 +189,13 @@ class HttpDigestAuthorization (object):
self.errmsg("Unsupported value for qop: '%s'" % self.qop)) self.errmsg("Unsupported value for qop: '%s'" % self.qop))
if not (self.cnonce and self.nc): if not (self.cnonce and self.nc):
raise ValueError( raise ValueError(
self.errmsg("If qop is sent then " self.errmsg('If qop is sent then '
"cnonce and nc MUST be present")) 'cnonce and nc MUST be present'))
else: else:
if self.cnonce or self.nc: if self.cnonce or self.nc:
raise ValueError( raise ValueError(
self.errmsg("If qop is not sent, " self.errmsg('If qop is not sent, '
"neither cnonce nor nc can be present")) 'neither cnonce nor nc can be present'))
def __str__(self): def __str__(self):
return 'authorization : %s' % self.auth_header return 'authorization : %s' % self.auth_header
@@ -239,7 +240,7 @@ class HttpDigestAuthorization (object):
except ValueError: # int() error except ValueError: # int() error
pass pass
if self.debug: if self.debug:
TRACE("nonce is stale") TRACE('nonce is stale')
return True return True
def HA2(self, entity_body=''): def HA2(self, entity_body=''):
@@ -251,14 +252,14 @@ class HttpDigestAuthorization (object):
# #
# If the "qop" value is "auth-int", then A2 is: # If the "qop" value is "auth-int", then A2 is:
# A2 = method ":" digest-uri-value ":" H(entity-body) # 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) a2 = '%s:%s' % (self.http_method, self.uri)
elif self.qop == "auth-int": elif self.qop == 'auth-int':
a2 = "%s:%s:%s" % (self.http_method, self.uri, H(entity_body)) a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body))
else: else:
# in theory, this should never happen, since I validate qop in # in theory, this should never happen, since I validate qop in
# __init__() # __init__()
raise ValueError(self.errmsg("Unrecognized value for qop!")) raise ValueError(self.errmsg('Unrecognized value for qop!'))
return H(a2) return H(a2)
def request_digest(self, ha1, entity_body=''): def request_digest(self, ha1, entity_body=''):
@@ -279,10 +280,10 @@ class HttpDigestAuthorization (object):
ha2 = self.HA2(entity_body) ha2 = self.HA2(entity_body)
# Request-Digest -- RFC 2617 3.2.2.1 # Request-Digest -- RFC 2617 3.2.2.1
if self.qop: if self.qop:
req = "%s:%s:%s:%s:%s" % ( req = '%s:%s:%s:%s:%s' % (
self.nonce, self.nc, self.cnonce, self.qop, ha2) self.nonce, self.nc, self.cnonce, self.qop, ha2)
else: else:
req = "%s:%s" % (self.nonce, ha2) req = '%s:%s' % (self.nonce, ha2)
# RFC 2617 3.2.2.2 # RFC 2617 3.2.2.2
# #
@@ -351,12 +352,10 @@ def digest_auth(realm, get_ha1, key, debug=False):
auth_header = request.headers.get('authorization') auth_header = request.headers.get('authorization')
nonce_is_stale = False nonce_is_stale = False
if auth_header is not None: if auth_header is not None:
try: with cherrypy.HTTPError.handle(ValueError, 400,
'The Authorization header could not be parsed.'):
auth = HttpDigestAuthorization( auth = HttpDigestAuthorization(
auth_header, request.method, debug=debug) auth_header, request.method, debug=debug)
except ValueError:
raise cherrypy.HTTPError(
400, "The Authorization header could not be parsed.")
if debug: if debug:
TRACE(str(auth)) TRACE(str(auth))
@@ -370,7 +369,7 @@ def digest_auth(realm, get_ha1, key, debug=False):
digest = auth.request_digest(ha1, entity_body=request.body) digest = auth.request_digest(ha1, entity_body=request.body)
if digest == auth.response: # authenticated if digest == auth.response: # authenticated
if debug: if debug:
TRACE("digest matches auth.response") TRACE('digest matches auth.response')
# Now check if nonce is stale. # Now check if nonce is stale.
# The choice of ten minutes' lifetime for nonce is somewhat # The choice of ten minutes' lifetime for nonce is somewhat
# arbitrary # arbitrary
@@ -378,7 +377,7 @@ def digest_auth(realm, get_ha1, key, debug=False):
if not nonce_is_stale: if not nonce_is_stale:
request.login = auth.username request.login = auth.username
if debug: if debug:
TRACE("authentication of %s successful" % TRACE('authentication of %s successful' %
auth.username) auth.username)
return return
@@ -388,4 +387,4 @@ def digest_auth(realm, get_ha1, key, debug=False):
TRACE(header) TRACE(header)
cherrypy.serving.response.headers['WWW-Authenticate'] = header cherrypy.serving.response.headers['WWW-Authenticate'] = header
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
401, "You are not authorized to access that resource") 401, 'You are not authorized to access that resource')
+13 -13
View File
@@ -39,7 +39,7 @@ import time
import cherrypy import cherrypy
from cherrypy.lib import cptools, httputil 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): class Cache(object):
@@ -170,7 +170,7 @@ class MemoryCache(Cache):
# Run self.expire_cache in a separate daemon thread. # Run self.expire_cache in a separate daemon thread.
t = threading.Thread(target=self.expire_cache, name='expire_cache') t = threading.Thread(target=self.expire_cache, name='expire_cache')
self.expiration_thread = t self.expiration_thread = t
set_daemon(t, True) t.daemon = True
t.start() t.start()
def clear(self): def clear(self):
@@ -265,7 +265,7 @@ class MemoryCache(Cache):
self.store.pop(uri, None) 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). """Try to obtain cached output. If fresh enough, raise HTTPError(304).
If POST, PUT, or DELETE: If POST, PUT, or DELETE:
@@ -291,9 +291,9 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
if not hasattr(cherrypy, "_cache"): if not hasattr(cherrypy, '_cache'):
# Make a process-wide Cache object. # 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. # Take all remaining kwargs and set them on the Cache object.
for k, v in kwargs.items(): 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 directive == 'max-age':
if len(atoms) != 1 or not atoms[0].isdigit(): if len(atoms) != 1 or not atoms[0].isdigit():
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
400, "Invalid Cache-Control header") 400, 'Invalid Cache-Control header')
max_age = int(atoms[0]) max_age = int(atoms[0])
break break
elif directive == 'no-cache': elif directive == 'no-cache':
@@ -359,7 +359,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
dict.__setitem__(rh, k, dict.__getitem__(h, k)) dict.__setitem__(rh, k, dict.__getitem__(h, k))
# Add the required Age header # Add the required Age header
response.headers["Age"] = str(age) response.headers['Age'] = str(age)
try: try:
# Note that validate_since depends on a Last-Modified header; # 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 secs = (86400 * secs.days) + secs.seconds
if secs == 0: if secs == 0:
if force or ("Pragma" not in headers): if force or ('Pragma' not in headers):
headers["Pragma"] = "no-cache" headers['Pragma'] = 'no-cache'
if cherrypy.serving.request.protocol >= (1, 1): if cherrypy.serving.request.protocol >= (1, 1):
if force or "Cache-Control" not in headers: if force or 'Cache-Control' not in headers:
headers["Cache-Control"] = "no-cache, must-revalidate" headers['Cache-Control'] = 'no-cache, must-revalidate'
# Set an explicit Expires date in the past. # Set an explicit Expires date in the past.
expiry = httputil.HTTPDate(1169942400.0) expiry = httputil.HTTPDate(1169942400.0)
else: else:
expiry = httputil.HTTPDate(response.time + secs) expiry = httputil.HTTPDate(response.time + secs)
if force or "Expires" not in headers: if force or 'Expires' not in headers:
headers["Expires"] = expiry headers['Expires'] = expiry
+24 -23
View File
@@ -25,11 +25,12 @@ import sys
import cgi import cgi
import os import os
import os.path import os.path
localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
from cherrypy._cpcompat import quote_plus
import cherrypy import cherrypy
from cherrypy._cpcompat import quote_plus
localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache')
the_coverage = None the_coverage = None
try: try:
@@ -45,8 +46,8 @@ except ImportError:
import warnings import warnings
warnings.warn( warnings.warn(
"No code coverage will be performed; " 'No code coverage will be performed; '
"coverage.py could not be imported.") 'coverage.py could not be imported.')
def start(): def start():
pass pass
@@ -196,7 +197,7 @@ def _percent(statements, missing):
return 0 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): coverage=the_coverage):
# Show the directory name and any of our children # Show the directory name and any of our children
@@ -207,7 +208,7 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
if newpath.lower().startswith(base): if newpath.lower().startswith(base):
relpath = newpath[len(base):] relpath = newpath[len(base):]
yield "| " * relpath.count(os.sep) yield '| ' * relpath.count(os.sep)
yield ( yield (
"<a class='directory' " "<a class='directory' "
"href='menu?base=%s&exclude=%s'>%s</a>\n" % "href='menu?base=%s&exclude=%s'>%s</a>\n" %
@@ -228,7 +229,7 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
for name in files: for name in files:
newpath = os.path.join(path, name) newpath = os.path.join(path, name)
pc_str = "" pc_str = ''
if showpct: if showpct:
try: try:
_, statements, _, missing, _ = coverage.analysis2(newpath) _, statements, _, missing, _ = coverage.analysis2(newpath)
@@ -237,13 +238,13 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
pass pass
else: else:
pc = _percent(statements, missing) pc = _percent(statements, missing)
pc_str = ("%3d%% " % pc).replace(' ', '&nbsp;') pc_str = ('%3d%% ' % pc).replace(' ', '&nbsp;')
if pc < float(pct) or pc == -1: if pc < float(pct) or pc == -1:
pc_str = "<span class='fail'>%s</span>" % pc_str pc_str = "<span class='fail'>%s</span>" % pc_str
else: else:
pc_str = "<span class='pass'>%s</span>" % pc_str 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) pc_str, newpath, name)
@@ -263,8 +264,8 @@ def _graft(path, tree):
break break
atoms.append(tail) atoms.append(tail)
atoms.append(p) atoms.append(p)
if p != "/": if p != '/':
atoms.append("/") atoms.append('/')
atoms.reverse() atoms.reverse()
for node in atoms: for node in atoms:
@@ -298,7 +299,7 @@ class CoverStats(object):
return TEMPLATE_FRAMESET % self.root.lower() return TEMPLATE_FRAMESET % self.root.lower()
@cherrypy.expose @cherrypy.expose
def menu(self, base="/", pct="50", showpct="", def menu(self, base='/', pct='50', showpct='',
exclude=r'python\d\.\d|test|tut\d|tutorial'): exclude=r'python\d\.\d|test|tut\d|tutorial'):
# The coverage module uses all-lower-case names. # The coverage module uses all-lower-case names.
@@ -309,36 +310,36 @@ class CoverStats(object):
# Start by showing links for parent paths # Start by showing links for parent paths
yield "<div id='crumbs'>" yield "<div id='crumbs'>"
path = "" path = ''
atoms = base.split(os.sep) atoms = base.split(os.sep)
atoms.pop() atoms.pop()
for atom in atoms: for atom in atoms:
path += atom + os.sep path += atom + os.sep
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s" yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
% (path, quote_plus(exclude), atom, os.sep)) % (path, quote_plus(exclude), atom, os.sep))
yield "</div>" yield '</div>'
yield "<div id='tree'>" yield "<div id='tree'>"
# Then display the tree # Then display the tree
tree = get_tree(base, exclude, self.coverage) tree = get_tree(base, exclude, self.coverage)
if not tree: if not tree:
yield "<p>No modules covered.</p>" yield '<p>No modules covered.</p>'
else: else:
for chunk in _show_branch(tree, base, "/", pct, for chunk in _show_branch(tree, base, '/', pct,
showpct == 'checked', exclude, showpct == 'checked', exclude,
coverage=self.coverage): coverage=self.coverage):
yield chunk yield chunk
yield "</div>" yield '</div>'
yield "</body></html>" yield '</body></html>'
def annotated_file(self, filename, statements, excluded, missing): def annotated_file(self, filename, statements, excluded, missing):
source = open(filename, 'r') source = open(filename, 'r')
buffer = [] buffer = []
for lineno, line in enumerate(source.readlines()): for lineno, line in enumerate(source.readlines()):
lineno += 1 lineno += 1
line = line.strip("\n\r") line = line.strip('\n\r')
empty_the_buffer = True empty_the_buffer = True
if lineno in excluded: if lineno in excluded:
template = TEMPLATE_LOC_EXCLUDED template = TEMPLATE_LOC_EXCLUDED
@@ -374,7 +375,7 @@ class CoverStats(object):
def serve(path=localFile, port=8080, root=None): def serve(path=localFile, port=8080, root=None):
if coverage is 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 from coverage import coverage
cov = coverage(data_file=path) cov = coverage(data_file=path)
cov.load() cov.load()
@@ -382,9 +383,9 @@ def serve(path=localFile, port=8080, root=None):
import cherrypy import cherrypy
cherrypy.config.update({'server.socket_port': int(port), cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10, 'server.thread_pool': 10,
'environment': "production", 'environment': 'production',
}) })
cherrypy.quickstart(CoverStats(cov, root)) cherrypy.quickstart(CoverStats(cov, root))
if __name__ == "__main__": if __name__ == '__main__':
serve(*tuple(sys.argv[1:])) serve(*tuple(sys.argv[1:]))
+9 -18
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 -------------------------------- # # ------------------------------- Statistics -------------------------------- #
import logging
if not hasattr(logging, 'statistics'): if not hasattr(logging, 'statistics'):
logging.statistics = {} logging.statistics = {}
@@ -210,12 +218,6 @@ def extrapolate_statistics(scope):
# -------------------- CherryPy Applications Statistics --------------------- # # -------------------- CherryPy Applications Statistics --------------------- #
import sys
import threading
import time
import cherrypy
appstats = logging.statistics.setdefault('CherryPy Applications', {}) appstats = logging.statistics.setdefault('CherryPy Applications', {})
appstats.update({ appstats.update({
'Enabled': True, 'Enabled': True,
@@ -390,24 +392,13 @@ class StatsTool(cherrypy.Tool):
sq.pop(0) sq.pop(0)
import cherrypy
cherrypy.tools.cpstats = StatsTool() cherrypy.tools.cpstats = StatsTool()
# ---------------------- CherryPy Statistics Reporting ---------------------- # # ---------------------- CherryPy Statistics Reporting ---------------------- #
import os
thisdir = os.path.abspath(os.path.dirname(__file__)) thisdir = os.path.abspath(os.path.dirname(__file__))
try:
import json
except ImportError:
try:
import simplejson as json
except ImportError:
json = None
missing = object() missing = object()
locale_date = lambda v: time.strftime('%c', time.gmtime(v)) locale_date = lambda v: time.strftime('%c', time.gmtime(v))
+54 -38
View File
@@ -7,7 +7,7 @@ from hashlib import md5
import six import six
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import httputil as _httputil from cherrypy.lib import httputil as _httputil
from cherrypy.lib import is_iterator from cherrypy.lib import is_iterator
@@ -33,7 +33,7 @@ def validate_etags(autotags=False, debug=False):
response = cherrypy.serving.response response = cherrypy.serving.response
# Guard against being run twice. # Guard against being run twice.
if hasattr(response, "ETag"): if hasattr(response, 'ETag'):
return return
status, reason, msg = _httputil.valid_status(response.status) status, reason, msg = _httputil.valid_status(response.status)
@@ -72,24 +72,24 @@ def validate_etags(autotags=False, debug=False):
if debug: if debug:
cherrypy.log('If-Match conditions: %s' % repr(conditions), cherrypy.log('If-Match conditions: %s' % repr(conditions),
'TOOLS.ETAGS') 'TOOLS.ETAGS')
if conditions and not (conditions == ["*"] or etag in conditions): if conditions and not (conditions == ['*'] or etag in conditions):
raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did " raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did '
"not match %r" % (etag, conditions)) 'not match %r' % (etag, conditions))
conditions = request.headers.elements('If-None-Match') or [] conditions = request.headers.elements('If-None-Match') or []
conditions = [str(x) for x in conditions] conditions = [str(x) for x in conditions]
if debug: if debug:
cherrypy.log('If-None-Match conditions: %s' % repr(conditions), cherrypy.log('If-None-Match conditions: %s' % repr(conditions),
'TOOLS.ETAGS') 'TOOLS.ETAGS')
if conditions == ["*"] or etag in conditions: if conditions == ['*'] or etag in conditions:
if debug: if debug:
cherrypy.log('request.method: %s' % cherrypy.log('request.method: %s' %
request.method, 'TOOLS.ETAGS') request.method, 'TOOLS.ETAGS')
if request.method in ("GET", "HEAD"): if request.method in ('GET', 'HEAD'):
raise cherrypy.HTTPRedirect([], 304) raise cherrypy.HTTPRedirect([], 304)
else: else:
raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r " raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r '
"matched %r" % (etag, conditions)) 'matched %r' % (etag, conditions))
def validate_since(): def validate_since():
@@ -113,7 +113,7 @@ def validate_since():
since = request.headers.get('If-Modified-Since') since = request.headers.get('If-Modified-Since')
if since and since == lastmod: if since and since == lastmod:
if (status >= 200 and status <= 299) or status == 304: 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) raise cherrypy.HTTPRedirect([], 304)
else: else:
raise cherrypy.HTTPError(412) raise cherrypy.HTTPError(412)
@@ -186,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' # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
scheme = s scheme = s
if not scheme: if not scheme:
scheme = request.base[:request.base.find("://")] scheme = request.base[:request.base.find('://')]
if local: if local:
lbase = request.headers.get(local, None) lbase = request.headers.get(local, None)
@@ -200,9 +200,9 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
if port != 80: if port != 80:
base += ':%s' % port base += ':%s' % port
if base.find("://") == -1: if base.find('://') == -1:
# add http:// or https:// if needed # add http:// or https:// if needed
base = scheme + "://" + base base = scheme + '://' + base
request.base = base request.base = base
@@ -212,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') cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
if xff: if xff:
if remote == 'X-Forwarded-For': if remote == 'X-Forwarded-For':
#Bug #1268 # Bug #1268
xff = xff.split(',')[0].strip() xff = xff.split(',')[0].strip()
request.remote.ip = xff request.remote.ip = xff
@@ -285,7 +285,7 @@ class SessionAuth(object):
"""Assert that the user is logged in.""" """Assert that the user is logged in."""
session_key = "username" session_key = 'username'
debug = False debug = False
def check_username_and_password(self, username, password): def check_username_and_password(self, username, password):
@@ -317,7 +317,7 @@ Message: %(error_msg)s
<br /> <br />
<input type="submit" /> <input type="submit" />
</form> </form>
</body></html>""") % vars()).encode("utf-8") </body></html>""") % vars()).encode('utf-8')
def do_login(self, username, password, from_page='..', **kwargs): def do_login(self, username, password, from_page='..', **kwargs):
"""Login. May raise redirect, or return True if request handled.""" """Login. May raise redirect, or return True if request handled."""
@@ -326,15 +326,15 @@ Message: %(error_msg)s
if error_msg: if error_msg:
body = self.login_screen(from_page, username, error_msg) body = self.login_screen(from_page, username, error_msg)
response.body = body response.body = body
if "Content-Length" in response.headers: if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers['Content-Length']
return True return True
else: else:
cherrypy.serving.request.login = username cherrypy.serving.request.login = username
cherrypy.session[self.session_key] = username cherrypy.session[self.session_key] = username
self.on_login(username) self.on_login(username)
raise cherrypy.HTTPRedirect(from_page or "/") raise cherrypy.HTTPRedirect(from_page or '/')
def do_logout(self, from_page='..', **kwargs): def do_logout(self, from_page='..', **kwargs):
"""Logout. May raise redirect, or return True if request handled.""" """Logout. May raise redirect, or return True if request handled."""
@@ -364,9 +364,9 @@ Message: %(error_msg)s
locals(), locals(),
) )
response.body = self.login_screen(url) 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. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers['Content-Length']
return True return True
self._debug_message('Setting request.login to %(username)r', locals()) self._debug_message('Setting request.login to %(username)r', locals())
request.login = username request.login = username
@@ -388,14 +388,14 @@ Message: %(error_msg)s
return True return True
elif path.endswith('do_login'): elif path.endswith('do_login'):
if request.method != 'POST': if request.method != 'POST':
response.headers['Allow'] = "POST" response.headers['Allow'] = 'POST'
self._debug_message('do_login requires POST') self._debug_message('do_login requires POST')
raise cherrypy.HTTPError(405) raise cherrypy.HTTPError(405)
self._debug_message('routing %(path)r to do_login', locals()) self._debug_message('routing %(path)r to do_login', locals())
return self.do_login(**request.params) return self.do_login(**request.params)
elif path.endswith('do_logout'): elif path.endswith('do_logout'):
if request.method != 'POST': if request.method != 'POST':
response.headers['Allow'] = "POST" response.headers['Allow'] = 'POST'
raise cherrypy.HTTPError(405) raise cherrypy.HTTPError(405)
self._debug_message('routing %(path)r to do_logout', locals()) self._debug_message('routing %(path)r to do_logout', locals())
return self.do_logout(**request.params) return self.do_logout(**request.params)
@@ -414,19 +414,19 @@ session_auth.__doc__ = """Session authentication hook.
Any attribute of the SessionAuth class may be overridden via a keyword arg Any attribute of the SessionAuth class may be overridden via a keyword arg
to this function: to this function:
""" + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__) """ + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__)
for k in dir(SessionAuth) if not k.startswith("__")]) for k in dir(SessionAuth) if not k.startswith('__')])
def log_traceback(severity=logging.ERROR, debug=False): def log_traceback(severity=logging.ERROR, debug=False):
"""Write the last error's traceback to the cherrypy error log.""" """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): def log_request_headers(debug=False):
"""Write request headers to the cherrypy error log.""" """Write request headers to the cherrypy error log."""
h = [" %s: %s" % (k, v) for k, v in cherrypy.serving.request.header_list] h = [' %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list]
cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP") cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP')
def log_hooks(debug=False): def log_hooks(debug=False):
@@ -442,13 +442,13 @@ def log_hooks(debug=False):
points.append(k) points.append(k)
for k in points: for k in points:
msg.append(" %s:" % k) msg.append(' %s:' % k)
v = request.hooks.get(k, []) v = request.hooks.get(k, [])
v.sort() v.sort()
for h in v: for h in v:
msg.append(" %r" % h) msg.append(' %r' % h)
cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
':\n' + '\n'.join(msg), "HTTP") ':\n' + '\n'.join(msg), 'HTTP')
def redirect(url='', internal=True, debug=False): def redirect(url='', internal=True, debug=False):
@@ -533,7 +533,7 @@ def accept(media=None, debug=False):
""" """
if not media: if not media:
return return
if isinstance(media, basestring): if isinstance(media, text_or_bytes):
media = [media] media = [media]
request = cherrypy.serving.request request = cherrypy.serving.request
@@ -549,12 +549,12 @@ def accept(media=None, debug=False):
# Note that 'ranges' is sorted in order of preference # Note that 'ranges' is sorted in order of preference
for element in ranges: for element in ranges:
if element.qvalue > 0: if element.qvalue > 0:
if element.value == "*/*": if element.value == '*/*':
# Matches any type or subtype # Matches any type or subtype
if debug: if debug:
cherrypy.log('Match due to */*', 'TOOLS.ACCEPT') cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
return media[0] return media[0]
elif element.value.endswith("/*"): elif element.value.endswith('/*'):
# Matches any subtype # Matches any subtype
mtype = element.value[:-1] # Keep the slash mtype = element.value[:-1] # Keep the slash
for m in media: for m in media:
@@ -574,11 +574,11 @@ def accept(media=None, debug=False):
# No suitable media-range found. # No suitable media-range found.
ah = request.headers.get('Accept') ah = request.headers.get('Accept')
if ah is None: if ah is None:
msg = "Your client did not send an Accept header." msg = 'Your client did not send an Accept header.'
else: else:
msg = "Your client sent this Accept header: %s." % ah msg = 'Your client sent this Accept header: %s.' % ah
msg += (" But this resource only emits these media types: %s." % msg += (' But this resource only emits these media types: %s.' %
", ".join(media)) ', '.join(media))
raise cherrypy.HTTPError(406, msg) raise cherrypy.HTTPError(406, msg)
@@ -630,3 +630,19 @@ def autovary(ignore=None, debug=False):
v.sort() v.sort()
resp_h['Vary'] = ', '.join(v) resp_h['Vary'] = ', '.join(v)
request.hooks.attach('before_finalize', set_response_header, 95) 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])
+24 -24
View File
@@ -5,7 +5,7 @@ import io
import six import six
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring, ntob from cherrypy._cpcompat import text_or_bytes, ntob
from cherrypy.lib import file_generator from cherrypy.lib import file_generator
from cherrypy.lib import is_closable_iterator from cherrypy.lib import is_closable_iterator
from cherrypy.lib import set_vary_header from cherrypy.lib import set_vary_header
@@ -66,7 +66,7 @@ class UTF8StreamEncoder:
class ResponseEncoder: class ResponseEncoder:
default_encoding = 'utf-8' default_encoding = 'utf-8'
failmsg = "Response body could not be encoded with %r." failmsg = 'Response body could not be encoded with %r.'
encoding = None encoding = None
errors = 'strict' errors = 'strict'
text_only = True text_only = True
@@ -131,7 +131,7 @@ class ResponseEncoder:
encoder = self.encode_stream encoder = self.encode_stream
else: else:
encoder = self.encode_string encoder = self.encode_string
if "Content-Length" in response.headers: if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it. # Delete Content-Length header so finalize() recalcs it.
# Encoded strings may be of different lengths from their # Encoded strings may be of different lengths from their
# unicode equivalents, and even from each other. For example: # unicode equivalents, and even from each other. For example:
@@ -142,7 +142,7 @@ class ResponseEncoder:
# 6 # 6
# >>> len(t.encode("utf7")) # >>> len(t.encode("utf7"))
# 8 # 8
del response.headers["Content-Length"] del response.headers['Content-Length']
# Parse the Accept-Charset request header, and try to provide one # Parse the Accept-Charset request header, and try to provide one
# of the requested charsets (in order of user preference). # of the requested charsets (in order of user preference).
@@ -157,7 +157,7 @@ class ResponseEncoder:
if self.debug: if self.debug:
cherrypy.log('Specified encoding %r' % cherrypy.log('Specified encoding %r' %
encoding, 'TOOLS.ENCODE') 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: if self.debug:
cherrypy.log('Attempting encoding %r' % cherrypy.log('Attempting encoding %r' %
encoding, 'TOOLS.ENCODE') encoding, 'TOOLS.ENCODE')
@@ -177,7 +177,7 @@ class ResponseEncoder:
else: else:
for element in encs: for element in encs:
if element.qvalue > 0: if element.qvalue > 0:
if element.value == "*": if element.value == '*':
# Matches any charset. Try our default. # Matches any charset. Try our default.
if self.debug: if self.debug:
cherrypy.log('Attempting default encoding due ' cherrypy.log('Attempting default encoding due '
@@ -192,7 +192,7 @@ class ResponseEncoder:
if encoder(encoding): if encoder(encoding):
return encoding return encoding
if "*" not in charsets: if '*' not in charsets:
# If no "*" is present in an Accept-Charset field, then all # If no "*" is present in an Accept-Charset field, then all
# character sets not explicitly mentioned get a quality # character sets not explicitly mentioned get a quality
# value of 0, except for ISO-8859-1, which gets a quality # value of 0, except for ISO-8859-1, which gets a quality
@@ -208,18 +208,18 @@ class ResponseEncoder:
# No suitable encoding found. # No suitable encoding found.
ac = request.headers.get('Accept-Charset') ac = request.headers.get('Accept-Charset')
if ac is None: 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: else:
msg = "Your client sent this Accept-Charset header: %s." % ac msg = 'Your client sent this Accept-Charset header: %s.' % ac
_charsets = ", ".join(sorted(self.attempted_charsets)) _charsets = ', '.join(sorted(self.attempted_charsets))
msg += " We tried these charsets: %s." % (_charsets,) msg += ' We tried these charsets: %s.' % (_charsets,)
raise cherrypy.HTTPError(406, msg) raise cherrypy.HTTPError(406, msg)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
response = cherrypy.serving.response response = cherrypy.serving.response
self.body = self.oldhandler(*args, **kwargs) 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 # strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character # item list is much faster than iterating over every character
# in a long string. # in a long string.
@@ -233,14 +233,14 @@ class ResponseEncoder:
elif self.body is None: elif self.body is None:
self.body = [] self.body = []
ct = response.headers.elements("Content-Type") ct = response.headers.elements('Content-Type')
if self.debug: if self.debug:
cherrypy.log('Content-Type: %r' % [str(h) cherrypy.log('Content-Type: %r' % [str(h)
for h in ct], 'TOOLS.ENCODE') for h in ct], 'TOOLS.ENCODE')
if ct and self.add_charset: if ct and self.add_charset:
ct = ct[0] ct = ct[0]
if self.text_only: if self.text_only:
if ct.value.lower().startswith("text/"): if ct.value.lower().startswith('text/'):
if self.debug: if self.debug:
cherrypy.log( cherrypy.log(
'Content-Type %s starts with "text/"' % ct, 'Content-Type %s starts with "text/"' % ct,
@@ -264,7 +264,7 @@ class ResponseEncoder:
if self.debug: if self.debug:
cherrypy.log('Setting Content-Type %s' % ct, cherrypy.log('Setting Content-Type %s' % ct,
'TOOLS.ENCODE') 'TOOLS.ENCODE')
response.headers["Content-Type"] = str(ct) response.headers['Content-Type'] = str(ct)
return self.body return self.body
@@ -280,11 +280,11 @@ def compress(body, compress_level):
yield ntob('\x08') # CM: compression method yield ntob('\x08') # CM: compression method
yield ntob('\x00') # FLG: none set yield ntob('\x00') # FLG: none set
# MTIME: 4 bytes # 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('\x02') # XFL: max compression, slowest algo
yield ntob('\xff') # OS: unknown yield ntob('\xff') # OS: unknown
crc = zlib.crc32(ntob("")) crc = zlib.crc32(ntob(''))
size = 0 size = 0
zobj = zlib.compressobj(compress_level, zobj = zlib.compressobj(compress_level,
zlib.DEFLATED, -zlib.MAX_WBITS, zlib.DEFLATED, -zlib.MAX_WBITS,
@@ -296,9 +296,9 @@ def compress(body, compress_level):
yield zobj.flush() yield zobj.flush()
# CRC32: 4 bytes # CRC32: 4 bytes
yield struct.pack("<L", crc & int('FFFFFFFF', 16)) yield struct.pack('<L', crc & int('FFFFFFFF', 16))
# ISIZE: 4 bytes # ISIZE: 4 bytes
yield struct.pack("<L", size & int('FFFFFFFF', 16)) yield struct.pack('<L', size & int('FFFFFFFF', 16))
def decompress(body): def decompress(body):
@@ -335,7 +335,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
set_vary_header(response, "Accept-Encoding") set_vary_header(response, 'Accept-Encoding')
if not response.body: if not response.body:
# Response body is empty (might be a 304 for instance) # Response body is empty (might be a 304 for instance)
@@ -345,7 +345,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
# If returning cached content (which should already have been gzipped), # If returning cached content (which should already have been gzipped),
# don't re-zip. # don't re-zip.
if getattr(request, "cached", False): if getattr(request, 'cached', False):
if debug: if debug:
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
return return
@@ -413,12 +413,12 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
# Return a generator that compresses the page # Return a generator that compresses the page
response.headers['Content-Encoding'] = 'gzip' response.headers['Content-Encoding'] = 'gzip'
response.body = compress(response.body, compress_level) 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. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers['Content-Length']
return return
if debug: if debug:
cherrypy.log('No acceptable encoding found.', context='GZIP') cherrypy.log('No acceptable encoding found.', context='GZIP')
cherrypy.HTTPError(406, "identity, gzip").set_response() cherrypy.HTTPError(406, 'identity, gzip').set_response()
+28 -29
View File
@@ -1,6 +1,5 @@
import gc import gc
import inspect import inspect
import os
import sys import sys
import time import time
@@ -36,7 +35,7 @@ class ReferrerTree(object):
refs = gc.get_referrers(obj) refs = gc.get_referrers(obj)
self.ignore.append(refs) self.ignore.append(refs)
if len(refs) > self.maxparents: if len(refs) > self.maxparents:
return [("[%s referrers]" % len(refs), [])] return [('[%s referrers]' % len(refs), [])]
try: try:
ascendcode = self.ascend.__code__ ascendcode = self.ascend.__code__
@@ -72,20 +71,20 @@ class ReferrerTree(object):
return self.peek(repr(obj)) return self.peek(repr(obj))
if isinstance(obj, dict): 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)) self._format(v, descend=False))
for k, v in obj.items()]) + "}" for k, v in obj.items()]) + '}'
elif isinstance(obj, list): elif isinstance(obj, list):
return "[" + ", ".join([self._format(item, descend=False) return '[' + ', '.join([self._format(item, descend=False)
for item in obj]) + "]" for item in obj]) + ']'
elif isinstance(obj, tuple): elif isinstance(obj, tuple):
return "(" + ", ".join([self._format(item, descend=False) return '(' + ', '.join([self._format(item, descend=False)
for item in obj]) + ")" for item in obj]) + ')'
r = self.peek(repr(obj)) r = self.peek(repr(obj))
if isinstance(obj, (str, int, float)): if isinstance(obj, (str, int, float)):
return r return r
return "%s: %s" % (type(obj), r) return '%s: %s' % (type(obj), r)
def format(self, tree): def format(self, tree):
"""Return a list of string reprs from a nested list of referrers.""" """Return a list of string reprs from a nested list of referrers."""
@@ -93,7 +92,7 @@ class ReferrerTree(object):
def ascend(branch, depth=1): def ascend(branch, depth=1):
for parent, grandparents in branch: for parent, grandparents in branch:
output.append((" " * depth) + self._format(parent)) output.append((' ' * depth) + self._format(parent))
if grandparents: if grandparents:
ascend(grandparents, depth + 1) ascend(grandparents, depth + 1)
ascend(tree) ascend(tree)
@@ -120,14 +119,14 @@ request_counter.subscribe()
def get_context(obj): def get_context(obj):
if isinstance(obj, _cprequest.Request): 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): elif isinstance(obj, _cprequest.Response):
return "status=%s" % obj.status return 'status=%s' % obj.status
elif isinstance(obj, _cpwsgi.AppResponse): elif isinstance(obj, _cpwsgi.AppResponse):
return "PATH_INFO=%s" % obj.environ.get('PATH_INFO', '') return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '')
elif hasattr(obj, "tb_lineno"): elif hasattr(obj, 'tb_lineno'):
return "tb_lineno=%s" % obj.tb_lineno return 'tb_lineno=%s' % obj.tb_lineno
return "" return ''
class GCRoot(object): class GCRoot(object):
@@ -136,27 +135,27 @@ class GCRoot(object):
classes = [ classes = [
(_cprequest.Request, 2, 2, (_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, (_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, (_cpwsgi.AppResponse, 1, 1,
"Should be 1 in this request thread only."), 'Should be 1 in this request thread only.'),
] ]
@cherrypy.expose @cherrypy.expose
def index(self): def index(self):
return "Hello, world!" return 'Hello, world!'
@cherrypy.expose @cherrypy.expose
def stats(self): def stats(self):
output = ["Statistics:"] output = ['Statistics:']
for trial in range(10): for trial in range(10):
if request_counter.count > 0: if request_counter.count > 0:
break break
time.sleep(0.5) time.sleep(0.5)
else: else:
output.append("\nNot all requests closed properly.") output.append('\nNot all requests closed properly.')
# gc_collect isn't perfectly synchronous, because it may # gc_collect isn't perfectly synchronous, because it may
# break reference cycles that then take time to fully # break reference cycles that then take time to fully
@@ -174,11 +173,11 @@ class GCRoot(object):
for x in gc.garbage: for x in gc.garbage:
trash[type(x)] = trash.get(type(x), 0) + 1 trash[type(x)] = trash.get(type(x), 0) + 1
if trash: 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 = [(v, k) for k, v in trash.items()]
trash.sort() trash.sort()
for pair in trash: for pair in trash:
output.append(" " + repr(pair)) output.append(' ' + repr(pair))
# Check declared classes to verify uncollected instances. # Check declared classes to verify uncollected instances.
# These don't have to be part of a cycle; they can be # These don't have to be part of a cycle; they can be
@@ -194,24 +193,24 @@ class GCRoot(object):
if lenobj < minobj or lenobj > maxobj: if lenobj < minobj or lenobj > maxobj:
if minobj == maxobj: if minobj == maxobj:
output.append( output.append(
"\nExpected %s %r references, got %s." % '\nExpected %s %r references, got %s.' %
(minobj, cls, lenobj)) (minobj, cls, lenobj))
else: else:
output.append( output.append(
"\nExpected %s to %s %r references, got %s." % '\nExpected %s to %s %r references, got %s.' %
(minobj, maxobj, cls, lenobj)) (minobj, maxobj, cls, lenobj))
for obj in objs: for obj in objs:
if objgraph is not None: if objgraph is not None:
ig = [id(objs), id(inspect.currentframe())] 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( objgraph.show_backrefs(
obj, extra_ignore=ig, max_depth=4, too_many=20, obj, extra_ignore=ig, max_depth=4, too_many=20,
filename=fname, extra_info=get_context) 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))) (repr(obj), sys.getrefcount(obj)))
t = ReferrerTree(ignore=[objs], maxdepth=3) t = ReferrerTree(ignore=[objs], maxdepth=3)
tree = t.ascend(obj) tree = t.ascend(obj)
output.extend(t.format(tree)) output.extend(t.format(tree))
return "\n".join(output) return '\n'.join(output)
-6
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 *
+67 -62
View File
@@ -20,8 +20,18 @@ Usage:
SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
SUPPORTED_QOP - list of supported 'Digest' 'qop'. 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 __version__ = 1, 0, 1
__author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>" __author__ = 'Tiago Cogumbreiro <cogumbreiro@users.sf.net>'
__credits__ = """ __credits__ = """
Peter van Kampen for its recipe which implement most of Digest Peter van Kampen for its recipe which implement most of Digest
authentication: authentication:
@@ -56,21 +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. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" """
__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse", __all__ = ('digestAuth', 'basicAuth', 'doAuth', 'checkResponse',
"parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey", 'parseAuthorization', 'SUPPORTED_ALGORITHM', 'md5SessionKey',
"calculateNonce", "SUPPORTED_QOP") 'calculateNonce', 'SUPPORTED_QOP')
########################################################################## ##########################################################################
import time
from hashlib import md5
from cherrypy._cpcompat import base64_decode, ntob MD5 = 'MD5'
from cherrypy._cpcompat import parse_http_list, parse_keqv_list MD5_SESS = 'MD5-sess'
AUTH = 'auth'
MD5 = "MD5" AUTH_INT = 'auth-int'
MD5_SESS = "MD5-sess"
AUTH = "auth"
AUTH_INT = "auth-int"
SUPPORTED_ALGORITHM = (MD5, MD5_SESS) SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
SUPPORTED_QOP = (AUTH, AUTH_INT) SUPPORTED_QOP = (AUTH, AUTH_INT)
@@ -95,10 +100,10 @@ def calculateNonce(realm, algorithm=MD5):
try: try:
encoder = DIGEST_AUTH_ENCODERS[algorithm] encoder = DIGEST_AUTH_ENCODERS[algorithm]
except KeyError: except KeyError:
raise NotImplementedError("The chosen algorithm (%s) does not have " raise NotImplementedError('The chosen algorithm (%s) does not have '
"an implementation yet" % algorithm) 'an implementation yet' % algorithm)
return encoder("%d:%s" % (time.time(), realm)) return encoder('%d:%s' % (time.time(), realm))
def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH): def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH):
@@ -129,7 +134,7 @@ def doAuth(realm):
This should be set in the HTTP header under the key 'WWW-Authenticate'.""" This should be set in the HTTP header under the key 'WWW-Authenticate'."""
return digestAuth(realm) + " " + basicAuth(realm) return digestAuth(realm) + ' ' + basicAuth(realm)
########################################################################## ##########################################################################
@@ -143,31 +148,31 @@ def _parseDigestAuthorization(auth_params):
# Now validate the params # Now validate the params
# Check for required parameters # Check for required parameters
required = ["username", "realm", "nonce", "uri", "response"] required = ['username', 'realm', 'nonce', 'uri', 'response']
for k in required: for k in required:
if k not in params: if k not in params:
return None return None
# If qop is sent then cnonce and nc MUST be present # If qop is sent then cnonce and nc MUST be present
if "qop" in params and not ("cnonce" in params if 'qop' in params and not ('cnonce' in params
and "nc" in params): and 'nc' in params):
return None return None
# If qop is not sent, neither cnonce nor nc can be present # If qop is not sent, neither cnonce nor nc can be present
if ("cnonce" in params or "nc" in params) and \ if ('cnonce' in params or 'nc' in params) and \
"qop" not in params: 'qop' not in params:
return None return None
return params return params
def _parseBasicAuthorization(auth_params): def _parseBasicAuthorization(auth_params):
username, password = base64_decode(auth_params).split(":", 1) username, password = base64_decode(auth_params).split(':', 1)
return {"username": username, "password": password} return {'username': username, 'password': password}
AUTH_SCHEMES = { AUTH_SCHEMES = {
"basic": _parseBasicAuthorization, 'basic': _parseBasicAuthorization,
"digest": _parseDigestAuthorization, 'digest': _parseDigestAuthorization,
} }
@@ -178,7 +183,7 @@ def parseAuthorization(credentials):
global AUTH_SCHEMES global AUTH_SCHEMES
auth_scheme, auth_params = credentials.split(" ", 1) auth_scheme, auth_params = credentials.split(' ', 1)
auth_scheme = auth_scheme.lower() auth_scheme = auth_scheme.lower()
parser = AUTH_SCHEMES[auth_scheme] parser = AUTH_SCHEMES[auth_scheme]
@@ -187,8 +192,8 @@ def parseAuthorization(credentials):
if params is None: if params is None:
return return
assert "auth_scheme" not in params assert 'auth_scheme' not in params
params["auth_scheme"] = auth_scheme params['auth_scheme'] = auth_scheme
return params return params
@@ -214,50 +219,50 @@ def md5SessionKey(params, password):
specification. specification.
""" """
keys = ("username", "realm", "nonce", "cnonce") keys = ('username', 'realm', 'nonce', 'cnonce')
params_copy = {} params_copy = {}
for key in keys: for key in keys:
params_copy[key] = params[key] params_copy[key] = params[key]
params_copy["algorithm"] = MD5_SESS params_copy['algorithm'] = MD5_SESS
return _A1(params_copy, password) return _A1(params_copy, password)
def _A1(params, password): def _A1(params, password):
algorithm = params.get("algorithm", MD5) algorithm = params.get('algorithm', MD5)
H = DIGEST_AUTH_ENCODERS[algorithm] H = DIGEST_AUTH_ENCODERS[algorithm]
if algorithm == MD5: if algorithm == MD5:
# If the "algorithm" directive's value is "MD5" or is # If the "algorithm" directive's value is "MD5" or is
# unspecified, then A1 is: # unspecified, then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd # 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: elif algorithm == MD5_SESS:
# This is A1 if qop is set # This is A1 if qop is set
# A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
# ":" unq(nonce-value) ":" unq(cnonce-value) # ":" unq(nonce-value) ":" unq(cnonce-value)
h_a1 = H("%s:%s:%s" % (params["username"], params["realm"], password)) h_a1 = H('%s:%s:%s' % (params['username'], params['realm'], password))
return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"]) return '%s:%s:%s' % (h_a1, params['nonce'], params['cnonce'])
def _A2(params, method, kwargs): def _A2(params, method, kwargs):
# If the "qop" directive's value is "auth" or is unspecified, then A2 is: # If the "qop" directive's value is "auth" or is unspecified, then A2 is:
# A2 = Method ":" digest-uri-value # A2 = Method ":" digest-uri-value
qop = params.get("qop", "auth") qop = params.get('qop', 'auth')
if qop == "auth": if qop == 'auth':
return method + ":" + params["uri"] return method + ':' + params['uri']
elif qop == "auth-int": elif qop == 'auth-int':
# If the "qop" value is "auth-int", then A2 is: # If the "qop" value is "auth-int", then A2 is:
# A2 = Method ":" digest-uri-value ":" H(entity-body) # A2 = Method ":" digest-uri-value ":" H(entity-body)
entity_body = kwargs.get("entity_body", "") entity_body = kwargs.get('entity_body', '')
H = kwargs["H"] H = kwargs['H']
return "%s:%s:%s" % ( return '%s:%s:%s' % (
method, method,
params["uri"], params['uri'],
H(entity_body) H(entity_body)
) )
@@ -265,19 +270,19 @@ def _A2(params, method, kwargs):
raise NotImplementedError("The 'qop' method is unknown: %s" % qop) raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
def _computeDigestResponse(auth_map, password, method="GET", A1=None, def _computeDigestResponse(auth_map, password, method='GET', A1=None,
**kwargs): **kwargs):
""" """
Generates a response respecting the algorithm defined in RFC 2617 Generates a response respecting the algorithm defined in RFC 2617
""" """
params = auth_map params = auth_map
algorithm = params.get("algorithm", MD5) algorithm = params.get('algorithm', MD5)
H = DIGEST_AUTH_ENCODERS[algorithm] 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)) H_A2 = H(_A2(params, method, kwargs))
@@ -286,7 +291,7 @@ def _computeDigestResponse(auth_map, password, method="GET", A1=None,
else: else:
H_A1 = H(_A1(params, password)) 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": # If the "qop" value is "auth" or "auth-int":
# request-digest = <"> < KD ( H(A1), unq(nonce-value) # request-digest = <"> < KD ( H(A1), unq(nonce-value)
# ":" nc-value # ":" nc-value
@@ -294,11 +299,11 @@ def _computeDigestResponse(auth_map, password, method="GET", A1=None,
# ":" unq(qop-value) # ":" unq(qop-value)
# ":" H(A2) # ":" H(A2)
# ) <"> # ) <">
request = "%s:%s:%s:%s:%s" % ( request = '%s:%s:%s:%s:%s' % (
params["nonce"], params['nonce'],
params["nc"], params['nc'],
params["cnonce"], params['cnonce'],
params["qop"], params['qop'],
H_A2, H_A2,
) )
elif qop is None: elif qop is None:
@@ -306,12 +311,12 @@ def _computeDigestResponse(auth_map, password, method="GET", A1=None,
# for compatibility with RFC 2069): # for compatibility with RFC 2069):
# request-digest = # request-digest =
# <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> # <"> < 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) 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 """This function is used to verify the response given by the client when
he tries to authenticate. he tries to authenticate.
Optional arguments: Optional arguments:
@@ -329,7 +334,7 @@ def _checkDigestResponse(auth_map, password, method="GET", A1=None, **kwargs):
response = _computeDigestResponse( response = _computeDigestResponse(
auth_map, password, method, A1, **kwargs) 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, def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
@@ -339,19 +344,19 @@ def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
pass_through = lambda password, username=None: password pass_through = lambda password, username=None: password
encrypt = encrypt or pass_through encrypt = encrypt or pass_through
try: try:
candidate = encrypt(auth_map["password"], auth_map["username"]) candidate = encrypt(auth_map['password'], auth_map['username'])
except TypeError: except TypeError:
# if encrypt only takes one parameter, it's the password # if encrypt only takes one parameter, it's the password
candidate = encrypt(auth_map["password"]) candidate = encrypt(auth_map['password'])
return candidate == password return candidate == password
AUTH_RESPONSES = { AUTH_RESPONSES = {
"basic": _checkBasicResponse, 'basic': _checkBasicResponse,
"digest": _checkDigestResponse, '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 """'checkResponse' compares the auth_map with the password and optionally
other arguments that each implementation might need. other arguments that each implementation might need.
@@ -368,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. The 'A1' argument is only used in MD5_SESS algorithm based responses.
Check md5SessionKey() for more info. 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, return checker(auth_map, password, method=method, encrypt=encrypt,
**kwargs) **kwargs)
+55 -49
View File
@@ -7,13 +7,23 @@ FuManChu will personally hang you up by your thumbs and submit you
to a public caning. to a public caning.
""" """
import functools
import email.utils
import re
from binascii import b2a_base64 from binascii import b2a_base64
from cgi import parse_header
try:
# Python 3
from email.header import decode_header
except ImportError:
from email.Header import decode_header
import six import six
from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou from cherrypy._cpcompat import BaseHTTPRequestHandler, ntob, ntou
from cherrypy._cpcompat import basestring, iteritems from cherrypy._cpcompat import text_or_bytes, iteritems
from cherrypy._cpcompat import reversed, sorted, unquote_qs from cherrypy._cpcompat import reversed, sorted, unquote_qs
response_codes = BaseHTTPRequestHandler.responses.copy() response_codes = BaseHTTPRequestHandler.responses.copy()
# From https://github.com/cherrypy/cherrypy/issues/361 # From https://github.com/cherrypy/cherrypy/issues/361
@@ -25,8 +35,8 @@ response_codes[503] = ('Service Unavailable',
'request due to a temporary overloading or ' 'request due to a temporary overloading or '
'maintenance of the server.') 'maintenance of the server.')
import re
from cgi import parse_header HTTPDate = functools.partial(email.utils.formatdate, usegmt=True)
def urljoin(*atoms): def urljoin(*atoms):
@@ -35,11 +45,11 @@ def urljoin(*atoms):
This will correctly join a SCRIPT_NAME and PATH_INFO into the This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank. original URL, even if either atom is blank.
""" """
url = "/".join([x for x in atoms if x]) url = '/'.join([x for x in atoms if x])
while "//" in url: while '//' in url:
url = url.replace("//", "/") url = url.replace('//', '/')
# Special-case the final url of "", and return "/" instead. # Special-case the final url of "", and return "/" instead.
return url or "/" return url or '/'
def urljoin_bytes(*atoms): def urljoin_bytes(*atoms):
@@ -48,11 +58,11 @@ def urljoin_bytes(*atoms):
This will correctly join a SCRIPT_NAME and PATH_INFO into the This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank. original URL, even if either atom is blank.
""" """
url = ntob("/").join([x for x in atoms if x]) url = ntob('/').join([x for x in atoms if x])
while ntob("//") in url: while ntob('//') in url:
url = url.replace(ntob("//"), ntob("/")) url = url.replace(ntob('//'), ntob('/'))
# Special-case the final url of "", and return "/" instead. # Special-case the final url of "", and return "/" instead.
return url or ntob("/") return url or ntob('/')
def protocol_from_http(protocol_str): def protocol_from_http(protocol_str):
@@ -75,9 +85,9 @@ def get_ranges(headervalue, content_length):
return None return None
result = [] result = []
bytesunit, byteranges = headervalue.split("=", 1) bytesunit, byteranges = headervalue.split('=', 1)
for brange in byteranges.split(","): for brange in byteranges.split(','):
start, stop = [x.strip() for x in brange.split("-", 1)] start, stop = [x.strip() for x in brange.split('-', 1)]
if start: if start:
if not stop: if not stop:
stop = content_length - 1 stop = content_length - 1
@@ -135,8 +145,8 @@ class HeaderElement(object):
return self.value < other.value return self.value < other.value
def __str__(self): def __str__(self):
p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)] p = [';%s=%s' % (k, v) for k, v in iteritems(self.params)]
return str("%s%s" % (self.value, "".join(p))) return str('%s%s' % (self.value, ''.join(p)))
def __bytes__(self): def __bytes__(self):
return ntob(self.__str__()) return ntob(self.__str__())
@@ -144,17 +154,17 @@ class HeaderElement(object):
def __unicode__(self): def __unicode__(self):
return ntou(self.__str__()) return ntou(self.__str__())
@staticmethod
def parse(elementstr): def parse(elementstr):
"""Transform 'token;key=val' to ('token', {'key': 'val'}).""" """Transform 'token;key=val' to ('token', {'key': 'val'})."""
initial_value, params = parse_header(elementstr) initial_value, params = parse_header(elementstr)
return initial_value, params return initial_value, params
parse = staticmethod(parse)
@classmethod
def from_str(cls, elementstr): def from_str(cls, elementstr):
"""Construct an instance from a string of the form 'token;key=val'.""" """Construct an instance from a string of the form 'token;key=val'."""
ival, params = cls.parse(elementstr) ival, params = cls.parse(elementstr)
return cls(ival, params) return cls(ival, params)
from_str = classmethod(from_str)
q_separator = re.compile(r'; *q *=') q_separator = re.compile(r'; *q *=')
@@ -171,6 +181,7 @@ class AcceptElement(HeaderElement):
have been the other way around, but it's too late to fix now. have been the other way around, but it's too late to fix now.
""" """
@classmethod
def from_str(cls, elementstr): def from_str(cls, elementstr):
qvalue = None qvalue = None
# The first "q" parameter (if any) separates the initial # The first "q" parameter (if any) separates the initial
@@ -184,16 +195,16 @@ class AcceptElement(HeaderElement):
media_type, params = cls.parse(media_range) media_type, params = cls.parse(media_range)
if qvalue is not None: if qvalue is not None:
params["q"] = qvalue params['q'] = qvalue
return cls(media_type, params) return cls(media_type, params)
from_str = classmethod(from_str)
@property
def qvalue(self): 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): if isinstance(val, HeaderElement):
val = val.value val = val.value
return float(val) return float(val)
qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
def __cmp__(self, other): def __cmp__(self, other):
diff = cmp(self.qvalue, other.qvalue) diff = cmp(self.qvalue, other.qvalue)
@@ -216,7 +227,7 @@ def header_elements(fieldname, fieldvalue):
result = [] result = []
for element in RE_HEADER_SPLIT.split(fieldvalue): 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) hv = AcceptElement.from_str(element)
else: else:
hv = HeaderElement.from_str(element) hv = HeaderElement.from_str(element)
@@ -227,13 +238,8 @@ def header_elements(fieldname, fieldvalue):
def decode_TEXT(value): def decode_TEXT(value):
r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr").""" 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) atoms = decode_header(value)
decodedvalue = "" decodedvalue = ''
for atom, charset in atoms: for atom, charset in atoms:
if charset is not None: if charset is not None:
atom = atom.decode(charset) atom = atom.decode(charset)
@@ -254,7 +260,7 @@ def valid_status(status):
status = 200 status = 200
status = str(status) status = str(status)
parts = status.split(" ", 1) parts = status.split(' ', 1)
if len(parts) == 1: if len(parts) == 1:
# No reason supplied. # No reason supplied.
code, = parts code, = parts
@@ -266,16 +272,16 @@ def valid_status(status):
try: try:
code = int(code) code = int(code)
except ValueError: except ValueError:
raise ValueError("Illegal response status from server " raise ValueError('Illegal response status from server '
"(%s is non-numeric)." % repr(code)) '(%s is non-numeric).' % repr(code))
if code < 100 or code > 599: if code < 100 or code > 599:
raise ValueError("Illegal response status from server " raise ValueError('Illegal response status from server '
"(%s is out of range)." % repr(code)) '(%s is out of range).' % repr(code))
if code not in response_codes: if code not in response_codes:
# code is unknown but not illegal # code is unknown but not illegal
default_reason, message = "", "" default_reason, message = '', ''
else: else:
default_reason, message = response_codes[code] default_reason, message = response_codes[code]
@@ -316,7 +322,7 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
nv = name_value.split('=', 1) nv = name_value.split('=', 1)
if len(nv) != 2: if len(nv) != 2:
if strict_parsing: 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 # Handle case of a control-name with no equal sign
if keep_blank_values: if keep_blank_values:
nv.append('') nv.append('')
@@ -334,7 +340,7 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
return d 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'): def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
@@ -347,7 +353,7 @@ def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
if image_map_pattern.match(query_string): if image_map_pattern.match(query_string):
# Server-side image map. Map the coords to 'x' and 'y' # Server-side image map. Map the coords to 'x' and 'y'
# (like CGI::Request does). # (like CGI::Request does).
pm = query_string.split(",") pm = query_string.split(',')
pm = {'x': int(pm[0]), 'y': int(pm[1])} pm = {'x': int(pm[0]), 'y': int(pm[1])}
else: else:
pm = _parse_qs(query_string, keep_blank_values, encoding=encoding) pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
@@ -384,12 +390,12 @@ class CaseInsensitiveDict(dict):
for k in E.keys(): for k in E.keys():
self[str(k).title()] = E[k] self[str(k).title()] = E[k]
@classmethod
def fromkeys(cls, seq, value=None): def fromkeys(cls, seq, value=None):
newdict = cls() newdict = cls()
for k in seq: for k in seq:
newdict[str(k).title()] = value newdict[str(k).title()] = value
return newdict return newdict
fromkeys = classmethod(fromkeys)
def setdefault(self, key, x=None): def setdefault(self, key, x=None):
key = str(key).title() key = str(key).title()
@@ -428,7 +434,7 @@ class HeaderMap(CaseInsensitiveDict):
""" """
protocol = (1, 1) protocol = (1, 1)
encodings = ["ISO-8859-1"] encodings = ['ISO-8859-1']
# Someday, when http-bis is done, this will probably get dropped # Someday, when http-bis is done, this will probably get dropped
# since few servers, clients, or intermediaries do it. But until then, # since few servers, clients, or intermediaries do it. But until then,
@@ -451,6 +457,7 @@ class HeaderMap(CaseInsensitiveDict):
"""Transform self into a list of (name, value) tuples.""" """Transform self into a list of (name, value) tuples."""
return list(self.encode_header_items(self.items())) return list(self.encode_header_items(self.items()))
@classmethod
def encode_header_items(cls, header_items): def encode_header_items(cls, header_items):
""" """
Prepare the sequence of name, value tuples into a form suitable for Prepare the sequence of name, value tuples into a form suitable for
@@ -460,7 +467,7 @@ class HeaderMap(CaseInsensitiveDict):
if isinstance(k, six.text_type): if isinstance(k, six.text_type):
k = cls.encode(k) k = cls.encode(k)
if not isinstance(v, basestring): if not isinstance(v, text_or_bytes):
v = str(v) v = str(v)
if isinstance(v, six.text_type): if isinstance(v, six.text_type):
@@ -474,8 +481,8 @@ class HeaderMap(CaseInsensitiveDict):
header_translate_deletechars) header_translate_deletechars)
yield (k, v) yield (k, v)
encode_header_items = classmethod(encode_header_items)
@classmethod
def encode(cls, v): def encode(cls, v):
"""Return the given header name or value, encoded for HTTP output.""" """Return the given header name or value, encoded for HTTP output."""
for enc in cls.encodings: for enc in cls.encodings:
@@ -493,10 +500,9 @@ class HeaderMap(CaseInsensitiveDict):
v = b2a_base64(v.encode('utf-8')) v = b2a_base64(v.encode('utf-8'))
return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?=')) return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
raise ValueError("Could not encode header part %r using " raise ValueError('Could not encode header part %r using '
"any of the encodings %r." % 'any of the encodings %r.' %
(v, cls.encodings)) (v, cls.encodings))
encode = classmethod(encode)
class Host(object): class Host(object):
@@ -509,9 +515,9 @@ class Host(object):
""" """
ip = "0.0.0.0" ip = '0.0.0.0'
port = 80 port = 80
name = "unknown.tld" name = 'unknown.tld'
def __init__(self, ip, port, name=None): def __init__(self, ip, port, name=None):
self.ip = ip self.ip = ip
@@ -521,4 +527,4 @@ class Host(object):
self.name = name self.name = name
def __repr__(self): 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)
+4 -6
View File
@@ -1,17 +1,15 @@
import cherrypy 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): def json_processor(entity):
"""Read application/json data into request.json.""" """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) raise cherrypy.HTTPError(411)
body = entity.fp.read() body = entity.fp.read()
try: with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'):
cherrypy.serving.request.json = json_decode(body.decode('utf-8')) 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')], 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. package importable; otherwise, ValueError is raised during processing.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
if isinstance(content_type, basestring): if isinstance(content_type, text_or_bytes):
content_type = [content_type] content_type = [content_type]
if force: if force:
+142 -147
View File
@@ -1,147 +1,142 @@
""" """
Platform-independent file locking. Inspired by and modeled after zc.lockfile. Platform-independent file locking. Inspired by and modeled after zc.lockfile.
""" """
import os import os
try: try:
import msvcrt import msvcrt
except ImportError: except ImportError:
pass pass
try: try:
import fcntl import fcntl
except ImportError: except ImportError:
pass pass
class LockError(Exception): class LockError(Exception):
"Could not obtain a lock" 'Could not obtain a lock'
msg = "Unable to lock %r" msg = 'Unable to lock %r'
def __init__(self, path): def __init__(self, path):
super(LockError, self).__init__(self.msg % path) super(LockError, self).__init__(self.msg % path)
class UnlockError(LockError): class UnlockError(LockError):
"Could not release a lock" 'Could not release a lock'
msg = "Unable to unlock %r" msg = 'Unable to unlock %r'
# first, a default, naive locking implementation # first, a default, naive locking implementation
class LockFile(object): class LockFile(object):
""" """
A default, naive locking implementation. Always fails if the file A default, naive locking implementation. Always fails if the file
already exists. already exists.
""" """
def __init__(self, path): def __init__(self, path):
self.path = path self.path = path
try: try:
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL) fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
except OSError: except OSError:
raise LockError(self.path) raise LockError(self.path)
os.close(fd) os.close(fd)
def release(self): def release(self):
os.remove(self.path) os.remove(self.path)
def remove(self): def remove(self):
pass pass
class SystemLockFile(object): class SystemLockFile(object):
""" """
An abstract base class for platform-specific locking. An abstract base class for platform-specific locking.
""" """
def __init__(self, path): def __init__(self, path):
self.path = path self.path = path
try: try:
# Open lockfile for writing without truncation: # Open lockfile for writing without truncation:
self.fp = open(path, 'r+') self.fp = open(path, 'r+')
except IOError: except IOError:
# If the file doesn't exist, IOError is raised; Use a+ instead. # If the file doesn't exist, IOError is raised; Use a+ instead.
# Note that there may be a race here. Multiple processes # Note that there may be a race here. Multiple processes
# could fail on the r+ open and open the file a+, but only # could fail on the r+ open and open the file a+, but only
# one will get the the lock and write a pid. # one will get the the lock and write a pid.
self.fp = open(path, 'a+') self.fp = open(path, 'a+')
try: try:
self._lock_file() self._lock_file()
except: except:
self.fp.seek(1) self.fp.seek(1)
self.fp.close() self.fp.close()
del self.fp del self.fp
raise raise
self.fp.write(" %s\n" % os.getpid()) self.fp.write(' %s\n' % os.getpid())
self.fp.truncate() self.fp.truncate()
self.fp.flush() self.fp.flush()
def release(self): def release(self):
if not hasattr(self, 'fp'): if not hasattr(self, 'fp'):
return return
self._unlock_file() self._unlock_file()
self.fp.close() self.fp.close()
del self.fp del self.fp
def remove(self): def remove(self):
""" """
Attempt to remove the file Attempt to remove the file
""" """
try: try:
os.remove(self.path) os.remove(self.path)
except: except:
pass pass
#@abc.abstract_method def _unlock_file(self):
# def _lock_file(self): """Attempt to obtain the lock on self.fp. Raise UnlockError if not
# """Attempt to obtain the lock on self.fp. Raise LockError if not released."""
# acquired."""
def _unlock_file(self): class WindowsLockFile(SystemLockFile):
"""Attempt to obtain the lock on self.fp. Raise UnlockError if not
released.""" def _lock_file(self):
# Lock just the first byte
try:
class WindowsLockFile(SystemLockFile): msvcrt.locking(self.fp.fileno(), msvcrt.LK_NBLCK, 1)
except IOError:
def _lock_file(self): raise LockError(self.fp.name)
# Lock just the first byte
try: def _unlock_file(self):
msvcrt.locking(self.fp.fileno(), msvcrt.LK_NBLCK, 1) try:
except IOError: self.fp.seek(0)
raise LockError(self.fp.name) msvcrt.locking(self.fp.fileno(), msvcrt.LK_UNLCK, 1)
except IOError:
def _unlock_file(self): raise UnlockError(self.fp.name)
try:
self.fp.seek(0) if 'msvcrt' in globals():
msvcrt.locking(self.fp.fileno(), msvcrt.LK_UNLCK, 1) LockFile = WindowsLockFile
except IOError:
raise UnlockError(self.fp.name)
class UnixLockFile(SystemLockFile):
if 'msvcrt' in globals():
LockFile = WindowsLockFile def _lock_file(self):
flags = fcntl.LOCK_EX | fcntl.LOCK_NB
try:
class UnixLockFile(SystemLockFile): fcntl.flock(self.fp.fileno(), flags)
except IOError:
def _lock_file(self): raise LockError(self.fp.name)
flags = fcntl.LOCK_EX | fcntl.LOCK_NB
try: # no need to implement _unlock_file, it will be unlocked on close()
fcntl.flock(self.fp.fileno(), flags)
except IOError: if 'fcntl' in globals():
raise LockError(self.fp.name) LockFile = UnixLockFile
# no need to implement _unlock_file, it will be unlocked on close()
if 'fcntl' in globals():
LockFile = UnixLockFile
+3 -3
View File
@@ -11,7 +11,7 @@ class Timer(object):
A simple timer that will indicate when an expiration time has passed. A simple timer that will indicate when an expiration time has passed.
""" """
def __init__(self, expiration): 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 self.expiration = expiration
@classmethod @classmethod
@@ -26,7 +26,7 @@ class Timer(object):
class LockTimeout(Exception): 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): class LockChecker(object):
@@ -43,5 +43,5 @@ class LockChecker(object):
def expired(self): def expired(self):
if self.timer.expired(): if self.timer.expired():
raise LockTimeout( raise LockTimeout(
"Timeout acquiring lock for %(session_id)s" % vars(self)) 'Timeout acquiring lock for %(session_id)s' % vars(self))
return False return False
+29 -30
View File
@@ -34,30 +34,31 @@ module from the command line, it will call ``serve()`` for you.
""" """
import io import io
import os
import os.path
import sys
import warnings
import cherrypy import cherrypy
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: try:
import profile import profile
import pstats 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 pstats.func_strip_path = new_func_strip_path
except ImportError: except ImportError:
profile = None profile = None
pstats = None pstats = None
import os
import os.path
import sys
import warnings
_count = 0 _count = 0
@@ -66,7 +67,7 @@ class Profiler(object):
def __init__(self, path=None): def __init__(self, path=None):
if not path: if not path:
path = os.path.join(os.path.dirname(__file__), "profile") path = os.path.join(os.path.dirname(__file__), 'profile')
self.path = path self.path = path
if not os.path.exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
@@ -75,7 +76,7 @@ class Profiler(object):
"""Dump profile data into self.path.""" """Dump profile data into self.path."""
global _count global _count
c = _count = _count + 1 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() prof = profile.Profile()
result = prof.runcall(func, *args, **params) result = prof.runcall(func, *args, **params)
prof.dump_stats(path) prof.dump_stats(path)
@@ -85,7 +86,7 @@ class Profiler(object):
""":rtype: list of available profiles. """:rtype: list of available profiles.
""" """
return [f for f in os.listdir(self.path) 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'): def stats(self, filename, sortby='cumulative'):
""":rtype stats(index): output of print_stats() for the given profile. """:rtype stats(index): output of print_stats() for the given profile.
@@ -125,8 +126,8 @@ class Profiler(object):
@cherrypy.expose @cherrypy.expose
def menu(self): def menu(self):
yield "<h2>Profiling runs</h2>" yield '<h2>Profiling runs</h2>'
yield "<p>Click on one of the runs below to see profiling data.</p>" yield '<p>Click on one of the runs below to see profiling data.</p>'
runs = self.statfiles() runs = self.statfiles()
runs.sort() runs.sort()
for i in runs: for i in runs:
@@ -135,7 +136,6 @@ class Profiler(object):
@cherrypy.expose @cherrypy.expose
def report(self, filename): def report(self, filename):
import cherrypy
cherrypy.response.headers['Content-Type'] = 'text/plain' cherrypy.response.headers['Content-Type'] = 'text/plain'
return self.stats(filename) return self.stats(filename)
@@ -149,7 +149,7 @@ class ProfileAggregator(Profiler):
self.profiler = profile.Profile() self.profiler = profile.Profile()
def run(self, func, *args, **params): 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) result = self.profiler.runcall(func, *args, **params)
self.profiler.dump_stats(path) self.profiler.dump_stats(path)
return result return result
@@ -174,11 +174,11 @@ class make_app:
""" """
if profile is None or pstats is None: 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 " "module. If you're on Debian, try "
"`sudo apt-get install python-profiler`. " '`sudo apt-get install python-profiler`. '
"See http://www.cherrypy.org/wiki/ProfilingOnDebian " 'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
"for details.") 'for details.')
warnings.warn(msg) warnings.warn(msg)
self.nextapp = nextapp self.nextapp = nextapp
@@ -199,20 +199,19 @@ class make_app:
def serve(path=None, port=8080): def serve(path=None, port=8080):
if profile is None or pstats is None: 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 " "If you're on Debian, try "
"`sudo apt-get install python-profiler`. " '`sudo apt-get install python-profiler`. '
"See http://www.cherrypy.org/wiki/ProfilingOnDebian " 'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
"for details.") 'for details.')
warnings.warn(msg) warnings.warn(msg)
import cherrypy
cherrypy.config.update({'server.socket_port': int(port), cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10, 'server.thread_pool': 10,
'environment': "production", 'environment': 'production',
}) })
cherrypy.quickstart(Profiler(path)) cherrypy.quickstart(Profiler(path))
if __name__ == "__main__": if __name__ == '__main__':
serve(*tuple(sys.argv[1:])) serve(*tuple(sys.argv[1:]))
+20 -25
View File
@@ -25,14 +25,9 @@ except ImportError:
from ConfigParser import ConfigParser from ConfigParser import ConfigParser
try: try:
set text_or_bytes
except NameError: except NameError:
from sets import Set as set text_or_bytes = str
try:
basestring
except NameError:
basestring = str
try: try:
# Python 3 # Python 3
@@ -47,7 +42,7 @@ import sys
def as_dict(config): def as_dict(config):
"""Return a dict from 'config' whether it is a dict, file, or filename.""" """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) config = Parser().dict_from_file(config)
elif hasattr(config, 'read'): elif hasattr(config, 'read'):
config = Parser().dict_from_file(config) config = Parser().dict_from_file(config)
@@ -83,8 +78,8 @@ class NamespaceSet(dict):
# Separate the given config into namespaces # Separate the given config into namespaces
ns_confs = {} ns_confs = {}
for k in config: for k in config:
if "." in k: if '.' in k:
ns, name = k.split(".", 1) ns, name = k.split('.', 1)
bucket = ns_confs.setdefault(ns, {}) bucket = ns_confs.setdefault(ns, {})
bucket[name] = config[k] bucket[name] = config[k]
@@ -95,7 +90,7 @@ class NamespaceSet(dict):
# for k, v in ns_confs.get(ns, {}).iteritems(): # for k, v in ns_confs.get(ns, {}).iteritems():
# callable(k, v) # callable(k, v)
for ns, handler in self.items(): for ns, handler in self.items():
exit = getattr(handler, "__exit__", None) exit = getattr(handler, '__exit__', None)
if exit: if exit:
callable = handler.__enter__() callable = handler.__enter__()
no_exc = True no_exc = True
@@ -120,7 +115,7 @@ class NamespaceSet(dict):
handler(k, v) handler(k, v)
def __repr__(self): def __repr__(self):
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__, return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
dict.__repr__(self)) dict.__repr__(self))
def __copy__(self): def __copy__(self):
@@ -155,7 +150,7 @@ class Config(dict):
def update(self, config): def update(self, config):
"""Update self from a dict, file or filename.""" """Update self from a dict, file or filename."""
if isinstance(config, basestring): if isinstance(config, text_or_bytes):
# Filename # Filename
config = Parser().dict_from_file(config) config = Parser().dict_from_file(config)
elif hasattr(config, 'read'): elif hasattr(config, 'read'):
@@ -192,7 +187,7 @@ class Parser(ConfigParser):
return optionstr return optionstr
def read(self, filenames): def read(self, filenames):
if isinstance(filenames, basestring): if isinstance(filenames, text_or_bytes):
filenames = [filenames] filenames = [filenames]
for filename in filenames: for filename in filenames:
# try: # try:
@@ -218,8 +213,8 @@ class Parser(ConfigParser):
value = unrepr(value) value = unrepr(value)
except Exception: except Exception:
x = sys.exc_info()[1] x = sys.exc_info()[1]
msg = ("Config error in section: %r, option: %r, " msg = ('Config error in section: %r, option: %r, '
"value: %r. Config values must be valid Python." % 'value: %r. Config values must be valid Python.' %
(section, option, value)) (section, option, value))
raise ValueError(msg, x.__class__.__name__, x.args) raise ValueError(msg, x.__class__.__name__, x.args)
result[section][option] = value result[section][option] = value
@@ -241,7 +236,7 @@ class _Builder2:
def build(self, o): def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None) m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None: if m is None:
raise TypeError("unrepr does not recognize %s" % raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__)) repr(o.__class__.__name__))
return m(o) return m(o)
@@ -254,7 +249,7 @@ class _Builder2:
# e.g. IronPython 1.0. # e.g. IronPython 1.0.
return eval(s) return eval(s)
p = compiler.parse("__tempvalue__ = " + s) p = compiler.parse('__tempvalue__ = ' + s)
return p.getChildren()[1].getChildren()[0].getChildren()[1] return p.getChildren()[1].getChildren()[0].getChildren()[1]
def build_Subscript(self, o): def build_Subscript(self, o):
@@ -327,7 +322,7 @@ class _Builder2:
except AttributeError: except AttributeError:
pass 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): def build_Add(self, o):
left, right = map(self.build, o.getChildren()) left, right = map(self.build, o.getChildren())
@@ -356,7 +351,7 @@ class _Builder3:
def build(self, o): def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None) m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None: if m is None:
raise TypeError("unrepr does not recognize %s" % raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__)) repr(o.__class__.__name__))
return m(o) return m(o)
@@ -369,7 +364,7 @@ class _Builder3:
# e.g. IronPython 1.0. # e.g. IronPython 1.0.
return eval(s) return eval(s)
p = ast.parse("__tempvalue__ = " + s) p = ast.parse('__tempvalue__ = ' + s)
return p.body[0].value return p.body[0].value
def build_Subscript(self, o): def build_Subscript(self, o):
@@ -397,8 +392,8 @@ class _Builder3:
if kw.arg is None: # double asterix `**` if kw.arg is None: # double asterix `**`
rst = self.build(kw.value) rst = self.build(kw.value)
if not isinstance(rst, dict): if not isinstance(rst, dict):
raise TypeError("Invalid argument for call." raise TypeError('Invalid argument for call.'
"Must be a mapping object.") 'Must be a mapping object.')
# give preference to the keys set directly from arg=value # give preference to the keys set directly from arg=value
for k, v in rst.items(): for k, v in rst.items():
if k not in kwargs: if k not in kwargs:
@@ -471,7 +466,7 @@ class _Builder3:
except AttributeError: except AttributeError:
pass 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): def build_NameConstant(self, o):
return o.value return o.value
@@ -523,7 +518,7 @@ def attributes(full_attribute_name):
"""Load a module and retrieve an attribute of that module.""" """Load a module and retrieve an attribute of that module."""
# Parse out the path, module, and attribute # 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:] attr_name = full_attribute_name[last_dot + 1:]
mod_path = full_attribute_name[:last_dot] mod_path = full_attribute_name[:last_dot]
+52 -51
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.on = True
tools.sessions.storage_type = "file" tools.sessions.storage_class = cherrypy.lib.sessions.FileSession
tools.sessions.storage_path = "/home/site/sessions" tools.sessions.storage_path = "/home/site/sessions"
tools.sessions.timeout = 60 tools.sessions.timeout = 60
This sets the session to be stored in files in the directory 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 /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, ``tools.sessions.on`` is the only required line for working sessions,
the rest are optional. the rest are optional.
@@ -95,8 +95,6 @@ import os
import time import time
import threading import threading
import six
import cherrypy import cherrypy
from cherrypy._cpcompat import copyitems, pickle, random20 from cherrypy._cpcompat import copyitems, pickle, random20
from cherrypy.lib import httputil from cherrypy.lib import httputil
@@ -123,10 +121,10 @@ class Session(object):
self._id = value self._id = value
for o in self.id_observers: for o in self.id_observers:
o(value) 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 timeout = 60
"Number of minutes after which to delete session data." 'Number of minutes after which to delete session data.'
locked = False locked = False
""" """
@@ -139,16 +137,16 @@ class Session(object):
automatically on the first attempt to access session data.""" automatically on the first attempt to access session data."""
clean_thread = None clean_thread = None
"Class-level Monitor which calls self.clean_up." 'Class-level Monitor which calls self.clean_up.'
clean_freq = 5 clean_freq = 5
"The poll rate for expired session cleanup in minutes." 'The poll rate for expired session cleanup in minutes.'
originalid = None 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 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 regenerated = False
""" """
@@ -156,7 +154,7 @@ class Session(object):
internal calls to regenerate the session id.""" internal calls to regenerate the session id."""
debug = False debug = False
"If True, log debug information." 'If True, log debug information.'
# --------------------- Session management methods --------------------- # # --------------------- Session management methods --------------------- #
@@ -472,9 +470,10 @@ class FileSession(Session):
if isinstance(self.lock_timeout, (int, float)): if isinstance(self.lock_timeout, (int, float)):
self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout) self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout)
if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))): if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))):
raise ValueError("Lock timeout must be numeric seconds or " raise ValueError('Lock timeout must be numeric seconds or '
"a timedelta instance.") 'a timedelta instance.')
@classmethod
def setup(cls, **kwargs): def setup(cls, **kwargs):
"""Set up the storage system for file-based sessions. """Set up the storage system for file-based sessions.
@@ -486,12 +485,11 @@ class FileSession(Session):
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(cls, k, v) setattr(cls, k, v)
setup = classmethod(setup)
def _get_file_path(self): def _get_file_path(self):
f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
if not os.path.abspath(f).startswith(self.storage_path): 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 return f
def _exists(self): def _exists(self):
@@ -499,12 +497,12 @@ class FileSession(Session):
return os.path.exists(path) return os.path.exists(path)
def _load(self, path=None): 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.") "Check your tools' priority levels.")
if path is None: if path is None:
path = self._get_file_path() path = self._get_file_path()
try: try:
f = open(path, "rb") f = open(path, 'rb')
try: try:
return pickle.load(f) return pickle.load(f)
finally: finally:
@@ -512,21 +510,21 @@ class FileSession(Session):
except (IOError, EOFError): except (IOError, EOFError):
e = sys.exc_info()[1] e = sys.exc_info()[1]
if self.debug: if self.debug:
cherrypy.log("Error loading the session pickle: %s" % cherrypy.log('Error loading the session pickle: %s' %
e, 'TOOLS.SESSIONS') e, 'TOOLS.SESSIONS')
return None return None
def _save(self, expiration_time): 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.") "Check your tools' priority levels.")
f = open(self._get_file_path(), "wb") f = open(self._get_file_path(), 'wb')
try: try:
pickle.dump((self._data, expiration_time), f, self.pickle_protocol) pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
finally: finally:
f.close() f.close()
def _delete(self): 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.") "Check your tools' priority levels.")
try: try:
os.unlink(self._get_file_path()) os.unlink(self._get_file_path())
@@ -603,6 +601,7 @@ class MemcachedSession(Session):
servers = ['127.0.0.1:11211'] servers = ['127.0.0.1:11211']
@classmethod
def setup(cls, **kwargs): def setup(cls, **kwargs):
"""Set up the storage system for memcached-based sessions. """Set up the storage system for memcached-based sessions.
@@ -614,21 +613,6 @@ class MemcachedSession(Session):
import memcache import memcache
cls.cache = memcache.Client(cls.servers) 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, six.text_type):
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): def _exists(self):
self.mc_lock.acquire() self.mc_lock.acquire()
@@ -651,7 +635,7 @@ class MemcachedSession(Session):
try: try:
if not self.cache.set(self.id, (self._data, expiration_time), td): if not self.cache.set(self.id, (self._data, expiration_time), td):
raise AssertionError( raise AssertionError(
"Session data for id %r not set." % self.id) 'Session data for id %r not set.' % self.id)
finally: finally:
self.mc_lock.release() self.mc_lock.release()
@@ -680,13 +664,13 @@ class MemcachedSession(Session):
def save(): def save():
"""Save any changed session data.""" """Save any changed session data."""
if not hasattr(cherrypy.serving, "session"): if not hasattr(cherrypy.serving, 'session'):
return return
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
# Guard against running twice # Guard against running twice
if hasattr(request, "_sessionsaved"): if hasattr(request, '_sessionsaved'):
return return
request._sessionsaved = True request._sessionsaved = True
@@ -705,8 +689,8 @@ save.failsafe = True
def close(): def close():
"""Close the session object for this request.""" """Close the session object for this request."""
sess = getattr(cherrypy.serving, "session", None) sess = getattr(cherrypy.serving, 'session', None)
if getattr(sess, "locked", False): if getattr(sess, 'locked', False):
# If the session is still locked we release the lock # If the session is still locked we release the lock
sess.release_lock() sess.release_lock()
if sess.debug: if sess.debug:
@@ -715,12 +699,19 @@ close.failsafe = True
close.priority = 90 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, 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). """Initialize session object (using cookies).
storage_class
The Session subclass to use. Defaults to RamSession.
storage_type storage_type
(deprecated)
One of 'ram', 'file', memcached'. This will be One of 'ram', 'file', memcached'. This will be
used to look up the corresponding class in cherrypy.lib.sessions used to look up the corresponding class in cherrypy.lib.sessions
globals. For example, 'file' will use the FileSession class. globals. For example, 'file' will use the FileSession class.
@@ -765,10 +756,13 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
you're using for more information. you're using for more information.
""" """
# Py27 compat
storage_class = kwargs.pop('storage_class', RamSession)
request = cherrypy.serving.request request = cherrypy.serving.request
# Guard against running twice # Guard against running twice
if hasattr(request, "_session_init_flag"): if hasattr(request, '_session_init_flag'):
return return
request._session_init_flag = True request._session_init_flag = True
@@ -780,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, cherrypy.log('ID obtained from request.cookie: %r' % id,
'TOOLS.SESSIONS') 'TOOLS.SESSIONS')
# Find the storage class and call setup (first time only). first_time = not hasattr(cherrypy, 'session')
storage_class = storage_type.title() + 'Session'
storage_class = globals()[storage_class] if storage_type:
if not hasattr(cherrypy, "session"): if first_time:
if hasattr(storage_class, "setup"): 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) storage_class.setup(**kwargs)
# Create and attach a new Session instance to cherrypy.serving. # Create and attach a new Session instance to cherrypy.serving.
@@ -801,7 +802,7 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
sess.id_observers.append(update_cookie) sess.id_observers.append(update_cookie)
# Create cherrypy.session which will proxy to cherrypy.serving.session # 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') cherrypy.session = cherrypy._ThreadLocalProxy('session')
if persistent: if persistent:
@@ -869,7 +870,7 @@ def set_response_cookie(path=None, path_header=None, name='session_id',
cookie[name]['secure'] = 1 cookie[name]['secure'] = 1
if httponly: if httponly:
if not cookie[name].isReservedKey('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 cookie[name]['httponly'] = 1
+28 -28
View File
@@ -71,7 +71,7 @@ def serve_file(path, content_type=None, disposition=None, name=None,
if content_type is None: if content_type is None:
# Set content-type based on filename extension # Set content-type based on filename extension
ext = "" ext = ''
i = path.rfind('.') i = path.rfind('.')
if i != -1: if i != -1:
ext = path[i:].lower() ext = path[i:].lower()
@@ -86,7 +86,7 @@ def serve_file(path, content_type=None, disposition=None, name=None,
if name is None: if name is None:
name = os.path.basename(path) name = os.path.basename(path)
cd = '%s; filename="%s"' % (disposition, name) cd = '%s; filename="%s"' % (disposition, name)
response.headers["Content-Disposition"] = cd response.headers['Content-Disposition'] = cd
if debug: if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
@@ -144,7 +144,7 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
cd = disposition cd = disposition
else: else:
cd = '%s; filename="%s"' % (disposition, name) cd = '%s; filename="%s"' % (disposition, name)
response.headers["Content-Disposition"] = cd response.headers['Content-Disposition'] = cd
if debug: if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
@@ -158,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 # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
request = cherrypy.serving.request request = cherrypy.serving.request
if request.protocol >= (1, 1): 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) r = httputil.get_ranges(request.headers.get('Range'), content_length)
if r == []: if r == []:
response.headers['Content-Range'] = "bytes */%s" % content_length response.headers['Content-Range'] = 'bytes */%s' % content_length
message = ("Invalid Range (first-byte-pos greater than " message = ('Invalid Range (first-byte-pos greater than '
"Content-Length)") 'Content-Length)')
if debug: if debug:
cherrypy.log(message, 'TOOLS.STATIC') cherrypy.log(message, 'TOOLS.STATIC')
raise cherrypy.HTTPError(416, message) raise cherrypy.HTTPError(416, message)
@@ -179,15 +179,15 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
cherrypy.log( cherrypy.log(
'Single part; start: %r, stop: %r' % (start, stop), 'Single part; start: %r, stop: %r' % (start, stop),
'TOOLS.STATIC') 'TOOLS.STATIC')
response.status = "206 Partial Content" response.status = '206 Partial Content'
response.headers['Content-Range'] = ( 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 response.headers['Content-Length'] = r_len
fileobj.seek(start) fileobj.seek(start)
response.body = file_generator_limited(fileobj, r_len) response.body = file_generator_limited(fileobj, r_len)
else: else:
# Return a multipart/byteranges response. # Return a multipart/byteranges response.
response.status = "206 Partial Content" response.status = '206 Partial Content'
try: try:
# Python 3 # Python 3
from email.generator import _make_boundary as make_boundary from email.generator import _make_boundary as make_boundary
@@ -195,15 +195,15 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
# Python 2 # Python 2
from mimetools import choose_boundary as make_boundary from mimetools import choose_boundary as make_boundary
boundary = make_boundary() boundary = make_boundary()
ct = "multipart/byteranges; boundary=%s" % boundary ct = 'multipart/byteranges; boundary=%s' % boundary
response.headers['Content-Type'] = ct 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. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers['Content-Length']
def file_ranges(): def file_ranges():
# Apache compatibility: # Apache compatibility:
yield ntob("\r\n") yield ntob('\r\n')
for start, stop in r: for start, stop in r:
if debug: if debug:
@@ -211,23 +211,23 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
'Multipart; start: %r, stop: %r' % ( 'Multipart; start: %r, stop: %r' % (
start, stop), start, stop),
'TOOLS.STATIC') 'TOOLS.STATIC')
yield ntob("--" + boundary, 'ascii') yield ntob('--' + boundary, 'ascii')
yield ntob("\r\nContent-type: %s" % content_type, yield ntob('\r\nContent-type: %s' % content_type,
'ascii') 'ascii')
yield ntob( 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), start, stop - 1, content_length),
'ascii') 'ascii')
fileobj.seek(start) fileobj.seek(start)
gen = file_generator_limited(fileobj, stop - start) gen = file_generator_limited(fileobj, stop - start)
for chunk in gen: for chunk in gen:
yield chunk yield chunk
yield ntob("\r\n") yield ntob('\r\n')
# Final boundary # Final boundary
yield ntob("--" + boundary + "--", 'ascii') yield ntob('--' + boundary + '--', 'ascii')
# Apache compatibility: # Apache compatibility:
yield ntob("\r\n") yield ntob('\r\n')
response.body = file_ranges() response.body = file_ranges()
return response.body return response.body
else: else:
@@ -244,7 +244,7 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
def serve_download(path, name=None): def serve_download(path, name=None):
"""Serve 'path' as an application/x-download attachment.""" """Serve 'path' as an application/x-download attachment."""
# This is such a common idiom I felt it deserved its own wrapper. # 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): def _attempt(filename, content_types, debug=False):
@@ -268,7 +268,7 @@ def _attempt(filename, content_types, debug=False):
return 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): debug=False):
"""Serve a static resource from the given (root +) dir. """Serve a static resource from the given (root +) dir.
@@ -306,7 +306,7 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
# If dir is relative, make absolute using "root". # If dir is relative, make absolute using "root".
if not os.path.isabs(dir): if not os.path.isabs(dir):
if not root: if not root:
msg = "Static dir requires an absolute dir (or root)." msg = 'Static dir requires an absolute dir (or root).'
if debug: if debug:
cherrypy.log(msg, 'TOOLS.STATICDIR') cherrypy.log(msg, 'TOOLS.STATICDIR')
raise ValueError(msg) raise ValueError(msg)
@@ -315,10 +315,10 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
# Determine where we are in the object tree relative to 'section' # Determine where we are in the object tree relative to 'section'
# (where the static tool was defined). # (where the static tool was defined).
if section == 'global': if section == 'global':
section = "/" section = '/'
section = section.rstrip(r"\/") section = section.rstrip(r'\/')
branch = request.path_info[len(section) + 1:] 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 # If branch is "", filename will end in a slash
filename = os.path.join(dir, branch) filename = os.path.join(dir, branch)
@@ -338,11 +338,11 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
if index: if index:
handled = _attempt(os.path.join(filename, index), content_types) handled = _attempt(os.path.join(filename, index), content_types)
if handled: if handled:
request.is_index = filename[-1] in (r"\/") request.is_index = filename[-1] in (r'\/')
return handled 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. """Serve a static resource from the given (root +) filename.
match match
+2 -2
View File
@@ -10,5 +10,5 @@ use with the bus. Some use tool-specific channels; see the documentation
for each class. for each class.
""" """
from cherrypy.process.wspbus import bus from cherrypy.process.wspbus import bus # noqa
from cherrypy.process import plugins, servers from cherrypy.process import plugins, servers # noqa
+42 -42
View File
@@ -7,8 +7,8 @@ import sys
import time import time
import threading import threading
from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident from cherrypy._cpcompat import text_or_bytes, get_thread_ident
from cherrypy._cpcompat import ntob, Timer, SetDaemonProperty from cherrypy._cpcompat import ntob, Timer
# _module__file__base is used by Autoreload to make # _module__file__base is used by Autoreload to make
# absolute any filenames retrieved from sys.modules which are not # absolute any filenames retrieved from sys.modules which are not
@@ -104,8 +104,8 @@ class SignalHandler(object):
if sys.platform[:4] == 'java': if sys.platform[:4] == 'java':
del self.handlers['SIGUSR1'] del self.handlers['SIGUSR1']
self.handlers['SIGUSR2'] = self.bus.graceful self.handlers['SIGUSR2'] = self.bus.graceful
self.bus.log("SIGUSR1 cannot be set on the JVM platform. " self.bus.log('SIGUSR1 cannot be set on the JVM platform. '
"Using SIGUSR2 instead.") 'Using SIGUSR2 instead.')
self.handlers['SIGINT'] = self._jython_SIGINT_handler self.handlers['SIGINT'] = self._jython_SIGINT_handler
self._previous_handlers = {} self._previous_handlers = {}
@@ -152,19 +152,19 @@ class SignalHandler(object):
signame = self.signals[signum] signame = self.signals[signum]
if handler is None: 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 handler = _signal.SIG_DFL
else: else:
self.bus.log("Restoring %s handler %r." % (signame, handler)) self.bus.log('Restoring %s handler %r.' % (signame, handler))
try: try:
our_handler = _signal.signal(signum, handler) our_handler = _signal.signal(signum, handler)
if our_handler is None: if our_handler is None:
self.bus.log("Restored old %s handler %r, but our " self.bus.log('Restored old %s handler %r, but our '
"handler was not registered." % 'handler was not registered.' %
(signame, handler), level=30) (signame, handler), level=30)
except ValueError: 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) (signame, handler), level=40, traceback=True)
def set_handler(self, signal, listener=None): def set_handler(self, signal, listener=None):
@@ -176,39 +176,39 @@ class SignalHandler(object):
If the given signal name or number is not available on the current If the given signal name or number is not available on the current
platform, ValueError is raised. platform, ValueError is raised.
""" """
if isinstance(signal, basestring): if isinstance(signal, text_or_bytes):
signum = getattr(_signal, signal, None) signum = getattr(_signal, signal, None)
if signum is None: if signum is None:
raise ValueError("No such signal: %r" % signal) raise ValueError('No such signal: %r' % signal)
signame = signal signame = signal
else: else:
try: try:
signame = self.signals[signal] signame = self.signals[signal]
except KeyError: except KeyError:
raise ValueError("No such signal: %r" % signal) raise ValueError('No such signal: %r' % signal)
signum = signal signum = signal
prev = _signal.signal(signum, self._handle_signal) prev = _signal.signal(signum, self._handle_signal)
self._previous_handlers[signum] = prev self._previous_handlers[signum] = prev
if listener is not None: if listener is not None:
self.bus.log("Listening for %s." % signame) self.bus.log('Listening for %s.' % signame)
self.bus.subscribe(signame, listener) self.bus.subscribe(signame, listener)
def _handle_signal(self, signum=None, frame=None): def _handle_signal(self, signum=None, frame=None):
"""Python signal handler (self.set_handler subscribes it for you).""" """Python signal handler (self.set_handler subscribes it for you)."""
signame = self.signals[signum] signame = self.signals[signum]
self.bus.log("Caught signal %s." % signame) self.bus.log('Caught signal %s.' % signame)
self.bus.publish(signame) self.bus.publish(signame)
def handle_SIGHUP(self): def handle_SIGHUP(self):
"""Restart if daemonized, else exit.""" """Restart if daemonized, else exit."""
if self._is_daemonized(): if self._is_daemonized():
self.bus.log("SIGHUP caught while daemonized. Restarting.") self.bus.log('SIGHUP caught while daemonized. Restarting.')
self.bus.restart() self.bus.restart()
else: else:
# not daemonized (may be foreground or background) # not daemonized (may be foreground or background)
self.bus.log("SIGHUP caught but not daemonized. Exiting.") self.bus.log('SIGHUP caught but not daemonized. Exiting.')
self.bus.exit() self.bus.exit()
@@ -239,14 +239,14 @@ class DropPrivileges(SimplePlugin):
def _set_uid(self, val): def _set_uid(self, val):
if val is not None: if val is not None:
if pwd is 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) level=30)
val = None val = None
elif isinstance(val, basestring): elif isinstance(val, text_or_bytes):
val = pwd.getpwnam(val)[2] val = pwd.getpwnam(val)[2]
self._uid = val self._uid = val
uid = property(_get_uid, _set_uid, 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): def _get_gid(self):
return self._gid return self._gid
@@ -254,14 +254,14 @@ class DropPrivileges(SimplePlugin):
def _set_gid(self, val): def _set_gid(self, val):
if val is not None: if val is not None:
if grp is 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) level=30)
val = None val = None
elif isinstance(val, basestring): elif isinstance(val, text_or_bytes):
val = grp.getgrnam(val)[2] val = grp.getgrnam(val)[2]
self._gid = val self._gid = val
gid = property(_get_gid, _set_gid, 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): def _get_umask(self):
return self._umask return self._umask
@@ -271,7 +271,7 @@ class DropPrivileges(SimplePlugin):
try: try:
os.umask os.umask
except AttributeError: except AttributeError:
self.bus.log("umask function not available; ignoring umask.", self.bus.log('umask function not available; ignoring umask.',
level=30) level=30)
val = None val = None
self._umask = val self._umask = val
@@ -393,7 +393,7 @@ class Daemonizer(SimplePlugin):
except OSError: except OSError:
# Python raises OSError rather than returning negative numbers. # Python raises OSError rather than returning negative numbers.
exc = sys.exc_info()[1] 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)) % (sys.argv[0], exc.errno, exc.strerror))
os.setsid() os.setsid()
@@ -406,15 +406,15 @@ class Daemonizer(SimplePlugin):
os._exit(0) # Exit second parent os._exit(0) # Exit second parent
except OSError: except OSError:
exc = sys.exc_info()[1] 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)) % (sys.argv[0], exc.errno, exc.strerror))
os.chdir("/") os.chdir('/')
os.umask(0) os.umask(0)
si = open(self.stdin, "r") si = open(self.stdin, 'r')
so = open(self.stdout, "a+") so = open(self.stdout, 'a+')
se = open(self.stderr, "a+") se = open(self.stderr, 'a+')
# os.dup2(fd, fd2) will close fd2 if necessary, # os.dup2(fd, fd2) will close fd2 if necessary,
# so we don't explicitly close stdin/out/err. # so we don't explicitly close stdin/out/err.
@@ -442,7 +442,7 @@ class PIDFile(SimplePlugin):
if self.finalized: if self.finalized:
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
else: 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.bus.log('PID %r written to %r.' % (pid, self.pidfile))
self.finalized = True self.finalized = True
start.priority = 70 start.priority = 70
@@ -481,13 +481,13 @@ class PerpetualTimer(Timer):
except Exception: except Exception:
if self.bus: if self.bus:
self.bus.log( self.bus.log(
"Error in perpetual timer thread function %r." % 'Error in perpetual timer thread function %r.' %
self.function, level=40, traceback=True) self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs. # Quit on first error to avoid massive logs.
raise raise
class BackgroundTask(SetDaemonProperty, threading.Thread): class BackgroundTask(threading.Thread):
"""A subclass of threading.Thread whose run() method repeats. """A subclass of threading.Thread whose run() method repeats.
@@ -499,7 +499,7 @@ class BackgroundTask(SetDaemonProperty, threading.Thread):
""" """
def __init__(self, interval, function, args=[], kwargs={}, bus=None): def __init__(self, interval, function, args=[], kwargs={}, bus=None):
threading.Thread.__init__(self) super(BackgroundTask, self).__init__()
self.interval = interval self.interval = interval
self.function = function self.function = function
self.args = args self.args = args
@@ -523,7 +523,7 @@ class BackgroundTask(SetDaemonProperty, threading.Thread):
self.function(*self.args, **self.kwargs) self.function(*self.args, **self.kwargs)
except Exception: except Exception:
if self.bus: 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) % self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs. # Quit on first error to avoid massive logs.
raise raise
@@ -560,24 +560,24 @@ class Monitor(SimplePlugin):
bus=self.bus) bus=self.bus)
self.thread.setName(threadname) self.thread.setName(threadname)
self.thread.start() self.thread.start()
self.bus.log("Started monitor thread %r." % threadname) self.bus.log('Started monitor thread %r.' % threadname)
else: else:
self.bus.log("Monitor thread %r already started." % threadname) self.bus.log('Monitor thread %r already started.' % threadname)
start.priority = 70 start.priority = 70
def stop(self): def stop(self):
"""Stop our callback's background task thread.""" """Stop our callback's background task thread."""
if self.thread is None: 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__) self.name or self.__class__.__name__)
else: else:
if self.thread is not threading.currentThread(): if self.thread is not threading.currentThread():
name = self.thread.getName() name = self.thread.getName()
self.thread.cancel() self.thread.cancel()
if not get_daemon(self.thread): if not self.thread.daemon:
self.bus.log("Joining %r" % name) self.bus.log('Joining %r' % name)
self.thread.join() self.thread.join()
self.bus.log("Stopped thread %r." % name) self.bus.log('Stopped thread %r.' % name)
self.thread = None self.thread = None
def graceful(self): def graceful(self):
@@ -674,10 +674,10 @@ class Autoreloader(Monitor):
else: else:
if mtime is None or mtime > oldtime: if mtime is None or mtime > oldtime:
# The file has been deleted or modified. # The file has been deleted or modified.
self.bus.log("Restarting because %s changed." % self.bus.log('Restarting because %s changed.' %
filename) filename)
self.thread.cancel() self.thread.cancel()
self.bus.log("Stopped thread %r." % self.bus.log('Stopped thread %r.' %
self.thread.getName()) self.thread.getName())
self.bus.restart() self.bus.restart()
return return
+35 -30
View File
@@ -113,6 +113,7 @@ Please see `Lighttpd FastCGI Docs
an explanation of the possible configuration options. an explanation of the possible configuration options.
""" """
import os
import sys import sys
import time import time
import warnings import warnings
@@ -151,32 +152,33 @@ class ServerAdapter(object):
def start(self): def start(self):
"""Start the HTTP server.""" """Start the HTTP server."""
if self.bind_addr is None: if self.bind_addr is None:
on_what = "unknown interface (dynamic?)" on_what = 'unknown interface (dynamic?)'
elif isinstance(self.bind_addr, tuple): elif isinstance(self.bind_addr, tuple):
on_what = self._get_base() on_what = self._get_base()
else: else:
on_what = "socket file: %s" % self.bind_addr on_what = 'socket file: %s' % self.bind_addr
if self.running: if self.running:
self.bus.log("Already serving on %s" % on_what) self.bus.log('Already serving on %s' % on_what)
return return
self.interrupt = None self.interrupt = None
if not self.httpserver: 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 not os.environ.get('LISTEN_PID', None):
if isinstance(self.bind_addr, tuple): # Start the httpserver in a new thread.
wait_for_free_port(*self.bind_addr) if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
import threading import threading
t = threading.Thread(target=self._start_http_thread) t = threading.Thread(target=self._start_http_thread)
t.setName("HTTPServer " + t.getName()) t.setName('HTTPServer ' + t.getName())
t.start() t.start()
self.wait() self.wait()
self.running = True self.running = True
self.bus.log("Serving on %s" % on_what) self.bus.log('Serving on %s' % on_what)
start.priority = 75 start.priority = 75
def _get_base(self): def _get_base(self):
@@ -184,15 +186,15 @@ class ServerAdapter(object):
return '' return ''
host, port = self.bind_addr host, port = self.bind_addr
if getattr(self.httpserver, 'ssl_adapter', None): if getattr(self.httpserver, 'ssl_adapter', None):
scheme = "https" scheme = 'https'
if port != 443: if port != 443:
host += ":%s" % port host += ':%s' % port
else: else:
scheme = "http" scheme = 'http'
if port != 80: if port != 80:
host += ":%s" % port host += ':%s' % port
return "%s://%s" % (scheme, host) return '%s://%s' % (scheme, host)
def _start_http_thread(self): def _start_http_thread(self):
"""HTTP servers MUST be running in new threads, so that the """HTTP servers MUST be running in new threads, so that the
@@ -204,32 +206,35 @@ class ServerAdapter(object):
try: try:
self.httpserver.start() self.httpserver.start()
except KeyboardInterrupt: 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.interrupt = sys.exc_info()[1]
self.bus.exit() self.bus.exit()
except SystemExit: 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.interrupt = sys.exc_info()[1]
self.bus.exit() self.bus.exit()
raise raise
except: except:
self.interrupt = sys.exc_info()[1] 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) traceback=True, level=40)
self.bus.exit() self.bus.exit()
raise raise
def wait(self): def wait(self):
"""Wait until the HTTP server is ready to receive requests.""" """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: if self.interrupt:
raise self.interrupt raise self.interrupt
time.sleep(.1) time.sleep(.1)
# Wait for port to be occupied # Wait for port to be occupied
if isinstance(self.bind_addr, tuple): if not os.environ.get('LISTEN_PID', None):
host, port = self.bind_addr # Wait for port to be occupied if not running via socket-activation
wait_for_occupied_port(host, port) # (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): def stop(self):
"""Stop the HTTP server.""" """Stop the HTTP server."""
@@ -240,9 +245,9 @@ class ServerAdapter(object):
if isinstance(self.bind_addr, tuple): if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr) wait_for_free_port(*self.bind_addr)
self.running = False self.running = False
self.bus.log("HTTP Server %s shut down" % self.httpserver) self.bus.log('HTTP Server %s shut down' % self.httpserver)
else: 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 stop.priority = 25
def restart(self): def restart(self):
@@ -389,10 +394,10 @@ def check_port(host, port, timeout=1.0):
except socket.gaierror: except socket.gaierror:
if ':' in host: if ':' in host:
info = [( info = [(
socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0) socket.AF_INET6, socket.SOCK_STREAM, 0, '', (host, port, 0, 0)
)] )]
else: 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: for res in info:
af, socktype, proto, canonname, sa = res af, socktype, proto, canonname, sa = res
@@ -408,8 +413,8 @@ def check_port(host, port, timeout=1.0):
if s: if s:
s.close() s.close()
else: else:
raise IOError("Port %s is in use on %s; perhaps the previous " raise IOError('Port %s is in use on %s; perhaps the previous '
"httpserver did not shut down properly." % 'httpserver did not shut down properly.' %
(repr(port), repr(host))) (repr(port), repr(host)))
@@ -435,7 +440,7 @@ def wait_for_free_port(host, port, timeout=None):
else: else:
return 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): def wait_for_occupied_port(host, port, timeout=None):
@@ -455,11 +460,11 @@ def wait_for_occupied_port(host, port, timeout=None):
time.sleep(timeout) time.sleep(timeout)
if host == client_host(host): 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 # On systems where a loopback interface is not available and the
# server is bound to all interfaces, it's difficult to determine # server is bound to all interfaces, it's difficult to determine
# whether the server is in fact occupying the port. In this case, # whether the server is in fact occupying the port. In this case,
# just issue a warning and move on. See issue #1100. # 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) warnings.warn(msg)
+6 -6
View File
@@ -85,7 +85,7 @@ class Win32Bus(wspbus.Bus):
return self.events[state] return self.events[state]
except KeyError: except KeyError:
event = win32event.CreateEvent(None, 0, 0, event = win32event.CreateEvent(None, 0, 0,
"WSPBus %s Event (pid=%r)" % 'WSPBus %s Event (pid=%r)' %
(state.name, os.getpid())) (state.name, os.getpid()))
self.events[state] = event self.events[state] = event
return event return event
@@ -135,7 +135,7 @@ class _ControlCodes(dict):
for key, val in self.items(): for key, val in self.items():
if val is obj: if val is obj:
return key 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}) control_codes = _ControlCodes({'graceful': 138})
@@ -153,14 +153,14 @@ class PyWebService(win32serviceutil.ServiceFramework):
"""Python Web Service.""" """Python Web Service."""
_svc_name_ = "Python Web Service" _svc_name_ = 'Python Web Service'
_svc_display_name_ = "Python Web Service" _svc_display_name_ = 'Python Web Service'
_svc_deps_ = None # sequence of service names on which this depends _svc_deps_ = None # sequence of service names on which this depends
_exe_name_ = "pywebsvc" _exe_name_ = 'pywebsvc'
_exe_args_ = None # Default to no arguments _exe_args_ = None # Default to no arguments
# Only exists on Windows 2000 or later, ignored on windows NT # Only exists on Windows 2000 or later, ignored on windows NT
_svc_description_ = "Python Web Service" _svc_description_ = 'Python Web Service'
def SvcDoRun(self): def SvcDoRun(self):
from cherrypy import process from cherrypy import process
+87 -20
View File
@@ -61,13 +61,20 @@ the new state.::
""" """
import atexit import atexit
import ctypes
import operator
import os import os
import subprocess
import sys import sys
import threading import threading
import time import time
import traceback as _traceback import traceback as _traceback
import warnings import warnings
import operator
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, # 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 # will be the directory from which the startup script was run. This is needed
@@ -85,9 +92,7 @@ class ChannelFailures(Exception):
delimiter = '\n' delimiter = '\n'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't use 'super' here; Exceptions are old-style in Py2.4 super(Exception, self).__init__(*args, **kwargs)
# See https://github.com/cherrypy/cherrypy/issues/959
Exception.__init__(self, *args, **kwargs)
self._exceptions = list() self._exceptions = list()
def handle_exception(self): def handle_exception(self):
@@ -117,7 +122,7 @@ class _StateEnum(object):
name = None name = None
def __repr__(self): def __repr__(self):
return "states.%s" % self.name return 'states.%s' % self.name
def __setattr__(self, key, value): def __setattr__(self, key, value):
if isinstance(value, self.State): if isinstance(value, self.State):
@@ -170,9 +175,8 @@ class Bus(object):
def subscribe(self, channel, callback, priority=None): def subscribe(self, channel, callback, priority=None):
"""Add the given callback at the given channel (if not present).""" """Add the given callback at the given channel (if not present)."""
if channel not in self.listeners: ch_listeners = self.listeners.setdefault(channel, set())
self.listeners[channel] = set() ch_listeners.add(callback)
self.listeners[channel].add(callback)
if priority is None: if priority is None:
priority = getattr(callback, 'priority', 50) priority = getattr(callback, 'priority', 50)
@@ -215,7 +219,7 @@ class Bus(object):
# Assume any further messages to 'log' will fail. # Assume any further messages to 'log' will fail.
pass pass
else: else:
self.log("Error in %r listener %r" % (channel, listener), self.log('Error in %r listener %r' % (channel, listener),
level=40, traceback=True) level=40, traceback=True)
if exc: if exc:
raise exc raise exc
@@ -225,10 +229,10 @@ class Bus(object):
"""An atexit handler which asserts the Bus is not running.""" """An atexit handler which asserts the Bus is not running."""
if self.state != states.EXITING: if self.state != states.EXITING:
warnings.warn( warnings.warn(
"The main thread is exiting, but the Bus is in the %r state; " 'The main thread is exiting, but the Bus is in the %r state; '
"shutting it down automatically now. You must either call " 'shutting it down automatically now. You must either call '
"bus.block() after start(), or call bus.exit() before the " 'bus.block() after start(), or call bus.exit() before the '
"main thread exits." % self.state, RuntimeWarning) 'main thread exits.' % self.state, RuntimeWarning)
self.exit() self.exit()
def start(self): def start(self):
@@ -244,7 +248,7 @@ class Bus(object):
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except: 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) level=40, traceback=True)
e_info = sys.exc_info()[1] e_info = sys.exc_info()[1]
try: try:
@@ -321,7 +325,7 @@ class Bus(object):
# It's also good to let them all shut down before allowing # It's also good to let them all shut down before allowing
# the main thread to call atexit handlers. # the main thread to call atexit handlers.
# See https://github.com/cherrypy/cherrypy/issues/751. # See https://github.com/cherrypy/cherrypy/issues/751.
self.log("Waiting for child threads to terminate...") self.log('Waiting for child threads to terminate...')
for t in threading.enumerate(): for t in threading.enumerate():
# Validate the we're not trying to join the MainThread # Validate the we're not trying to join the MainThread
# that will cause a deadlock and the case exist when # that will cause a deadlock and the case exist when
@@ -333,13 +337,13 @@ class Bus(object):
not isinstance(t, threading._MainThread) not isinstance(t, threading._MainThread)
): ):
# Note that any dummy (external) threads are always daemonic. # Note that any dummy (external) threads are always daemonic.
if hasattr(threading.Thread, "daemon"): if hasattr(threading.Thread, 'daemon'):
# Python 2.6+ # Python 2.6+
d = t.daemon d = t.daemon
else: else:
d = t.isDaemon() d = t.isDaemon()
if not d: if not d:
self.log("Waiting for thread %s." % t.getName()) self.log('Waiting for thread %s.' % t.getName())
t.join() t.join()
if self.execv: if self.execv:
@@ -376,14 +380,25 @@ class Bus(object):
This must be called from the main thread, because certain platforms 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. (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.log('Re-spawning %s' % ' '.join(args))
self._extend_pythonpath(os.environ)
if sys.platform[:4] == 'java': if sys.platform[:4] == 'java':
from _systemrestart import SystemRestart from _systemrestart import SystemRestart
raise SystemRestart raise SystemRestart
else: else:
args.insert(0, sys.executable)
if sys.platform == 'win32': if sys.platform == 'win32':
args = ['"%s"' % arg for arg in args] args = ['"%s"' % arg for arg in args]
@@ -392,6 +407,58 @@ class Bus(object):
self._set_cloexec() self._set_cloexec()
os.execv(sys.executable, args) 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): def _set_cloexec(self):
"""Set the CLOEXEC flag on all open files (except stdin/out/err). """Set the CLOEXEC flag on all open files (except stdin/out/err).
@@ -437,7 +504,7 @@ class Bus(object):
return t 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.""" """Log the given message. Append the last traceback if requested."""
if traceback: if traceback:
# Work-around for bug in Python's traceback implementation # Work-around for bug in Python's traceback implementation
File diff suppressed because it is too large. Load diff
+29 -16
View File
@@ -33,10 +33,10 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
private_key = None private_key = None
"""The filename of the server's private key file.""" """The filename of the server's private key file."""
certificate_chain = None certificate_chain = None
"""The filename of the certificate chain file.""" """The filename of the certificate chain file."""
"""The ssl.SSLContext that will be used to wrap sockets where available """The ssl.SSLContext that will be used to wrap sockets where available
(on Python > 2.7.9 / 3.3) (on Python > 2.7.9 / 3.3)
""" """
@@ -44,7 +44,7 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
def __init__(self, certificate, private_key, certificate_chain=None): def __init__(self, certificate, private_key, certificate_chain=None):
if ssl is 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.certificate = certificate
self.private_key = private_key self.private_key = private_key
self.certificate_chain = certificate_chain self.certificate_chain = certificate_chain
@@ -79,13 +79,30 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
# the 'ping' isn't SSL. # the 'ping' isn't SSL.
return None, {} return None, {}
elif e.errno == ssl.SSL_ERROR_SSL: 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. # The client is speaking HTTP to an HTTPS server.
raise wsgiserver.NoSSLError raise wsgiserver.NoSSLError
elif e.args[1].endswith('unknown protocol'):
# The client is speaking some non-HTTP protocol. # Check if it's one of the known errors
# Drop the conn. # Errors that are caught by PyOpenSSL, but thrown by built-in ssl
return None, {} _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
except:
# Temporary fix for https://github.com/cherrypy/cherrypy/issues/1618
e = sys.exc_info()[1]
if e.args == (0, 'Error'):
return None, {}
raise raise
return s, self.get_environ(s) return s, self.get_environ(s)
@@ -94,8 +111,8 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
"""Create WSGI environ entries to be merged into each request.""" """Create WSGI environ entries to be merged into each request."""
cipher = sock.cipher() cipher = sock.cipher()
ssl_environ = { ssl_environ = {
"wsgi.url_scheme": "https", 'wsgi.url_scheme': 'https',
"HTTPS": "on", 'HTTPS': 'on',
'SSL_PROTOCOL': cipher[1], 'SSL_PROTOCOL': cipher[1],
'SSL_CIPHER': cipher[0] 'SSL_CIPHER': cipher[0]
# SSL_VERSION_INTERFACE string The mod_ssl program version # SSL_VERSION_INTERFACE string The mod_ssl program version
@@ -103,9 +120,5 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
} }
return ssl_environ return ssl_environ
if sys.version_info >= (3, 0): def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): return wsgiserver.CP_makefile(sock, mode, bufsize)
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)
+12 -16
View File
@@ -43,7 +43,7 @@ except ImportError:
SSL = None SSL = None
class SSL_fileobject(wsgiserver.CP_fileobject): class SSL_fileobject(wsgiserver.CP_makefile):
"""SSL file object attached to a socket object.""" """SSL file object attached to a socket object."""
@@ -70,15 +70,15 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
time.sleep(self.ssl_retry) time.sleep(self.ssl_retry)
except SSL.SysCallError as e: except SSL.SysCallError as e:
if is_reader and e.args == (-1, 'Unexpected EOF'): if is_reader and e.args == (-1, 'Unexpected EOF'):
return "" return ''
errnum = e.args[0] errnum = e.args[0]
if is_reader and errnum in wsgiserver.socket_errors_to_ignore: if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
return "" return ''
raise socket.error(errnum) raise socket.error(errnum)
except SSL.Error as e: except SSL.Error as e:
if is_reader and e.args == (-1, 'Unexpected EOF'): if is_reader and e.args == (-1, 'Unexpected EOF'):
return "" return ''
thirdarg = None thirdarg = None
try: try:
@@ -95,7 +95,7 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
raise raise
if time.time() - start > self.ssl_timeout: if time.time() - start > self.ssl_timeout:
raise socket.timeout("timed out") raise socket.timeout('timed out')
def recv(self, size): def recv(self, size):
return self._safe_call(True, super(SSL_fileobject, self).recv, 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): def __init__(self, certificate, private_key, certificate_chain=None):
if SSL is 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.context = None
self.certificate = certificate self.certificate = certificate
@@ -192,18 +192,14 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
c = SSL.Context(SSL.SSLv23_METHOD) c = SSL.Context(SSL.SSLv23_METHOD)
c.use_privatekey_file(self.private_key) c.use_privatekey_file(self.private_key)
if self.certificate_chain: if self.certificate_chain:
if isinstance(self.certificate_chain, unicode) and self.certificate_chain.encode('cp1252', 'ignore') == self.certificate_chain.encode('cp1252', 'replace'): c.load_verify_locations(self.certificate_chain)
# 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.use_certificate_file(self.certificate) c.use_certificate_file(self.certificate)
return c return c
def get_environ(self): def get_environ(self):
"""Return WSGI environ entries to be merged into each request.""" """Return WSGI environ entries to be merged into each request."""
ssl_environ = { ssl_environ = {
"HTTPS": "on", 'HTTPS': 'on',
# pyOpenSSL doesn't provide access to any of these AFAICT # pyOpenSSL doesn't provide access to any of these AFAICT
# 'SSL_PROTOCOL': 'SSLv2', # 'SSL_PROTOCOL': 'SSLv2',
# SSL_CIPHER string The cipher specification name # SSL_CIPHER string The cipher specification name
@@ -224,8 +220,8 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
# Validity of server's certificate (end time), # Validity of server's certificate (end time),
}) })
for prefix, dn in [("I", cert.get_issuer()), for prefix, dn in [('I', cert.get_issuer()),
("S", cert.get_subject())]: ('S', cert.get_subject())]:
# X509Name objects don't seem to have a way to get the # X509Name objects don't seem to have a way to get the
# complete DN string. Use str() and slice it instead, # complete DN string. Use str() and slice it instead,
# because str(dn) == "<X509Name object '/C=US/ST=...'>" # 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 # 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). # for any value to contain slashes itself (in a URL).
while dnstr: while dnstr:
pos = dnstr.rfind("=") pos = dnstr.rfind('=')
dnstr, value = dnstr[:pos], dnstr[pos + 1:] dnstr, value = dnstr[:pos], dnstr[pos + 1:]
pos = dnstr.rfind("/") pos = dnstr.rfind('/')
dnstr, key = dnstr[:pos], dnstr[pos + 1:] dnstr, key = dnstr[:pos], dnstr[pos + 1:]
if key and value: if key and value:
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
+16
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)
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-509
View File
@@ -1,509 +0,0 @@
import re
import hashlib
import time
import StringIO
__version__ = '0.8'
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
GNTP_INFO_LINE = re.compile(
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
re.IGNORECASE
)
GNTP_INFO_LINE_SHORT = re.compile(
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
re.IGNORECASE
)
GNTP_HEADER = re.compile('([\w-]+):(.+)')
GNTP_EOL = '\r\n'
class BaseError(Exception):
def gntp_error(self):
error = GNTPError(self.errorcode, self.errordesc)
return error.encode()
class ParseError(BaseError):
errorcode = 500
errordesc = 'Error parsing the message'
class AuthError(BaseError):
errorcode = 400
errordesc = 'Error with authorization'
class UnsupportedError(BaseError):
errorcode = 500
errordesc = 'Currently unsupported by gntp.py'
class _GNTPBuffer(StringIO.StringIO):
"""GNTP Buffer class"""
def writefmt(self, message="", *args):
"""Shortcut function for writing GNTP Headers"""
self.write((message % args).encode('utf8', 'replace'))
self.write(GNTP_EOL)
class _GNTPBase(object):
"""Base initilization
:param string messagetype: GNTP Message type
:param string version: GNTP Protocol version
:param string encription: Encryption protocol
"""
def __init__(self, messagetype=None, version='1.0', encryption=None):
self.info = {
'version': version,
'messagetype': messagetype,
'encryptionAlgorithmID': encryption
}
self.headers = {}
self.resources = {}
def __str__(self):
return self.encode()
def _parse_info(self, data):
"""Parse the first line of a GNTP message to get security and other info values
:param string data: GNTP Message
:return dict: Parsed GNTP Info line
"""
match = GNTP_INFO_LINE.match(data)
if not match:
raise ParseError('ERROR_PARSING_INFO_LINE')
info = match.groupdict()
if info['encryptionAlgorithmID'] == 'NONE':
info['encryptionAlgorithmID'] = None
return info
def set_password(self, password, encryptAlgo='MD5'):
"""Set a password for a GNTP Message
:param string password: Null to clear password
:param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
"""
hash = {
'MD5': hashlib.md5,
'SHA1': hashlib.sha1,
'SHA256': hashlib.sha256,
'SHA512': hashlib.sha512,
}
self.password = password
self.encryptAlgo = encryptAlgo.upper()
if not password:
self.info['encryptionAlgorithmID'] = None
self.info['keyHashAlgorithm'] = None
return
if not self.encryptAlgo in hash.keys():
raise UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
hashfunction = hash.get(self.encryptAlgo)
password = password.encode('utf8')
seed = time.ctime()
salt = hashfunction(seed).hexdigest()
saltHash = hashfunction(seed).digest()
keyBasis = password + saltHash
key = hashfunction(keyBasis).digest()
keyHash = hashfunction(key).hexdigest()
self.info['keyHashAlgorithmID'] = self.encryptAlgo
self.info['keyHash'] = keyHash.upper()
self.info['salt'] = salt.upper()
def _decode_hex(self, value):
"""Helper function to decode hex string to `proper` hex string
:param string value: Human readable hex string
:return string: Hex string
"""
result = ''
for i in range(0, len(value), 2):
tmp = int(value[i:i + 2], 16)
result += chr(tmp)
return result
def _decode_binary(self, rawIdentifier, identifier):
rawIdentifier += '\r\n\r\n'
dataLength = int(identifier['Length'])
pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
pointerEnd = pointerStart + dataLength
data = self.raw[pointerStart:pointerEnd]
if not len(data) == dataLength:
raise ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
return data
def _validate_password(self, password):
"""Validate GNTP Message against stored password"""
self.password = password
if password == None:
raise AuthError('Missing password')
keyHash = self.info.get('keyHash', None)
if keyHash is None and self.password is None:
return True
if keyHash is None:
raise AuthError('Invalid keyHash')
if self.password is None:
raise AuthError('Missing password')
password = self.password.encode('utf8')
saltHash = self._decode_hex(self.info['salt'])
keyBasis = password + saltHash
key = hashlib.md5(keyBasis).digest()
keyHash = hashlib.md5(key).hexdigest()
if not keyHash.upper() == self.info['keyHash'].upper():
raise AuthError('Invalid Hash')
return True
def validate(self):
"""Verify required headers"""
for header in self._requiredHeaders:
if not self.headers.get(header, False):
raise ParseError('Missing Notification Header: ' + header)
def _format_info(self):
"""Generate info line for GNTP Message
:return string:
"""
info = u'GNTP/%s %s' % (
self.info.get('version'),
self.info.get('messagetype'),
)
if self.info.get('encryptionAlgorithmID', None):
info += ' %s:%s' % (
self.info.get('encryptionAlgorithmID'),
self.info.get('ivValue'),
)
else:
info += ' NONE'
if self.info.get('keyHashAlgorithmID', None):
info += ' %s:%s.%s' % (
self.info.get('keyHashAlgorithmID'),
self.info.get('keyHash'),
self.info.get('salt')
)
return info
def _parse_dict(self, data):
"""Helper function to parse blocks of GNTP headers into a dictionary
:param string data:
:return dict:
"""
dict = {}
for line in data.split('\r\n'):
match = GNTP_HEADER.match(line)
if not match:
continue
key = unicode(match.group(1).strip(), 'utf8', 'replace')
val = unicode(match.group(2).strip(), 'utf8', 'replace')
dict[key] = val
return dict
def add_header(self, key, value):
if isinstance(value, unicode):
self.headers[key] = value
else:
self.headers[key] = unicode('%s' % value, 'utf8', 'replace')
def add_resource(self, data):
"""Add binary resource
:param string data: Binary Data
"""
identifier = hashlib.md5(data).hexdigest()
self.resources[identifier] = data
return 'x-growl-resource://%s' % identifier
def decode(self, data, password=None):
"""Decode GNTP Message
:param string data:
"""
self.password = password
self.raw = data
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(data)
self.headers = self._parse_dict(parts[0])
def encode(self):
"""Encode a generic GNTP Message
:return string: GNTP Message ready to be sent
"""
buffer = _GNTPBuffer()
buffer.writefmt(self._format_info())
#Headers
for k, v in self.headers.iteritems():
buffer.writefmt('%s: %s', k, v)
buffer.writefmt()
#Resources
for resource, data in self.resources.iteritems():
buffer.writefmt('Identifier: %s', resource)
buffer.writefmt('Length: %d', len(data))
buffer.writefmt()
buffer.write(data)
buffer.writefmt()
buffer.writefmt()
return buffer.getvalue()
class GNTPRegister(_GNTPBase):
"""Represents a GNTP Registration Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notifications-Count'
]
_requiredNotificationHeaders = ['Notification-Name']
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'REGISTER')
self.notifications = []
if data:
self.decode(data, password)
else:
self.set_password(password)
self.add_header('Application-Name', 'pygntp')
self.add_header('Notifications-Count', 0)
def validate(self):
'''Validate required headers and validate notification headers'''
for header in self._requiredHeaders:
if not self.headers.get(header, False):
raise ParseError('Missing Registration Header: ' + header)
for notice in self.notifications:
for header in self._requiredNotificationHeaders:
if not notice.get(header, False):
raise ParseError('Missing Notification Header: ' + header)
def decode(self, data, password):
"""Decode existing GNTP Registration message
:param string data: Message to decode
"""
self.raw = data
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(data)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Notification-Name', False):
self.notifications.append(notice)
elif notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('register.png','wblol').write(notice['Data'])
self.resources[notice.get('Identifier')] = notice
def add_notification(self, name, enabled=True):
"""Add new Notification to Registration message
:param string name: Notification Name
:param boolean enabled: Enable this notification by default
"""
notice = {}
notice['Notification-Name'] = u'%s' % name
notice['Notification-Enabled'] = u'%s' % enabled
self.notifications.append(notice)
self.add_header('Notifications-Count', len(self.notifications))
def encode(self):
"""Encode a GNTP Registration Message
:return string: Encoded GNTP Registration message
"""
buffer = _GNTPBuffer()
buffer.writefmt(self._format_info())
#Headers
for k, v in self.headers.iteritems():
buffer.writefmt('%s: %s', k, v)
buffer.writefmt()
#Notifications
if len(self.notifications) > 0:
for notice in self.notifications:
for k, v in notice.iteritems():
buffer.writefmt('%s: %s', k, v)
buffer.writefmt()
#Resources
for resource, data in self.resources.iteritems():
buffer.writefmt('Identifier: %s', resource)
buffer.writefmt('Length: %d', len(data))
buffer.writefmt()
buffer.write(data)
buffer.writefmt()
buffer.writefmt()
return buffer.getvalue()
class GNTPNotice(_GNTPBase):
"""Represents a GNTP Notification Command
:param string data: (Optional) See decode()
:param string app: (Optional) Set Application-Name
:param string name: (Optional) Set Notification-Name
:param string title: (Optional) Set Notification Title
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notification-Name',
'Notification-Title'
]
def __init__(self, data=None, app=None, name=None, title=None, password=None):
_GNTPBase.__init__(self, 'NOTIFY')
if data:
self.decode(data, password)
else:
self.set_password(password)
if app:
self.add_header('Application-Name', app)
if name:
self.add_header('Notification-Name', name)
if title:
self.add_header('Notification-Title', title)
def decode(self, data, password):
"""Decode existing GNTP Notification message
:param string data: Message to decode.
"""
self.raw = data
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(data)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('notice.png','wblol').write(notice['Data'])
self.resources[notice.get('Identifier')] = notice
class GNTPSubscribe(_GNTPBase):
"""Represents a GNTP Subscribe Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Subscriber-ID',
'Subscriber-Name',
]
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'SUBSCRIBE')
if data:
self.decode(data, password)
else:
self.set_password(password)
class GNTPOK(_GNTPBase):
"""Represents a GNTP OK Response
:param string data: (Optional) See _GNTPResponse.decode()
:param string action: (Optional) Set type of action the OK Response is for
"""
_requiredHeaders = ['Response-Action']
def __init__(self, data=None, action=None):
_GNTPBase.__init__(self, '-OK')
if data:
self.decode(data)
if action:
self.add_header('Response-Action', action)
class GNTPError(_GNTPBase):
"""Represents a GNTP Error response
:param string data: (Optional) See _GNTPResponse.decode()
:param string errorcode: (Optional) Error code
:param string errordesc: (Optional) Error Description
"""
_requiredHeaders = ['Error-Code', 'Error-Description']
def __init__(self, data=None, errorcode=None, errordesc=None):
_GNTPBase.__init__(self, '-ERROR')
if data:
self.decode(data)
if errorcode:
self.add_header('Error-Code', errorcode)
self.add_header('Error-Description', errordesc)
def error(self):
return (self.headers.get('Error-Code', None),
self.headers.get('Error-Description', None))
def parse_gntp(data, password=None):
"""Attempt to parse a message as a GNTP message
:param string data: Message to be parsed
:param string password: Optional password to be used to verify the message
"""
match = GNTP_INFO_LINE_SHORT.match(data)
if not match:
raise ParseError('INVALID_GNTP_INFO')
info = match.groupdict()
if info['messagetype'] == 'REGISTER':
return GNTPRegister(data, password=password)
elif info['messagetype'] == 'NOTIFY':
return GNTPNotice(data, password=password)
elif info['messagetype'] == 'SUBSCRIBE':
return GNTPSubscribe(data, password=password)
elif info['messagetype'] == '-OK':
return GNTPOK(data)
elif info['messagetype'] == '-ERROR':
return GNTPError(data)
raise ParseError('INVALID_GNTP_MESSAGE')
+141
View File
@@ -0,0 +1,141 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
import logging
import os
import sys
from optparse import OptionParser, OptionGroup
from gntp.notifier import GrowlNotifier
from gntp.shim import RawConfigParser
from gntp.version import __version__
DEFAULT_CONFIG = os.path.expanduser('~/.gntp')
config = RawConfigParser({
'hostname': 'localhost',
'password': None,
'port': 23053,
})
config.read([DEFAULT_CONFIG])
if not config.has_section('gntp'):
config.add_section('gntp')
class ClientParser(OptionParser):
def __init__(self):
OptionParser.__init__(self, version="%%prog %s" % __version__)
group = OptionGroup(self, "Network Options")
group.add_option("-H", "--host",
dest="host", default=config.get('gntp', 'hostname'),
help="Specify a hostname to which to send a remote notification. [%default]")
group.add_option("--port",
dest="port", default=config.getint('gntp', 'port'), type="int",
help="port to listen on [%default]")
group.add_option("-P", "--password",
dest='password', default=config.get('gntp', 'password'),
help="Network password")
self.add_option_group(group)
group = OptionGroup(self, "Notification Options")
group.add_option("-n", "--name",
dest="app", default='Python GNTP Test Client',
help="Set the name of the application [%default]")
group.add_option("-s", "--sticky",
dest='sticky', default=False, action="store_true",
help="Make the notification sticky [%default]")
group.add_option("--image",
dest="icon", default=None,
help="Icon for notification (URL or /path/to/file)")
group.add_option("-m", "--message",
dest="message", default=None,
help="Sets the message instead of using stdin")
group.add_option("-p", "--priority",
dest="priority", default=0, type="int",
help="-2 to 2 [%default]")
group.add_option("-d", "--identifier",
dest="identifier",
help="Identifier for coalescing")
group.add_option("-t", "--title",
dest="title", default=None,
help="Set the title of the notification [%default]")
group.add_option("-N", "--notification",
dest="name", default='Notification',
help="Set the notification name [%default]")
group.add_option("--callback",
dest="callback",
help="URL callback")
self.add_option_group(group)
# Extra Options
self.add_option('-v', '--verbose',
dest='verbose', default=0, action='count',
help="Verbosity levels")
def parse_args(self, args=None, values=None):
values, args = OptionParser.parse_args(self, args, values)
if values.message is None:
print('Enter a message followed by Ctrl-D')
try:
message = sys.stdin.read()
except KeyboardInterrupt:
exit()
else:
message = values.message
if values.title is None:
values.title = ' '.join(args)
# If we still have an empty title, use the
# first bit of the message as the title
if values.title == '':
values.title = message[:20]
values.verbose = logging.WARNING - values.verbose * 10
return values, message
def main():
(options, message) = ClientParser().parse_args()
logging.basicConfig(level=options.verbose)
if not os.path.exists(DEFAULT_CONFIG):
logging.info('No config read found at %s', DEFAULT_CONFIG)
growl = GrowlNotifier(
applicationName=options.app,
notifications=[options.name],
defaultNotifications=[options.name],
hostname=options.host,
password=options.password,
port=options.port,
)
result = growl.register()
if result is not True:
exit(result)
# This would likely be better placed within the growl notifier
# class but until I make _checkIcon smarter this is "easier"
if options.icon and growl._checkIcon(options.icon) is False:
logging.info('Loading image %s', options.icon)
f = open(options.icon, 'rb')
options.icon = f.read()
f.close()
result = growl.notify(
noteType=options.name,
title=options.title,
description=message,
icon=options.icon,
sticky=options.sticky,
priority=options.priority,
callback=options.callback,
identifier=options.identifier,
)
if result is not True:
exit(result)
if __name__ == "__main__":
main()
+77
View File
@@ -0,0 +1,77 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
"""
The gntp.config module is provided as an extended GrowlNotifier object that takes
advantage of the ConfigParser module to allow us to setup some default values
(such as hostname, password, and port) in a more global way to be shared among
programs using gntp
"""
import logging
import os
import gntp.notifier
import gntp.shim
__all__ = [
'mini',
'GrowlNotifier'
]
logger = logging.getLogger(__name__)
class GrowlNotifier(gntp.notifier.GrowlNotifier):
"""
ConfigParser enhanced GrowlNotifier object
For right now, we are only interested in letting users overide certain
values from ~/.gntp
::
[gntp]
hostname = ?
password = ?
port = ?
"""
def __init__(self, *args, **kwargs):
config = gntp.shim.RawConfigParser({
'hostname': kwargs.get('hostname', 'localhost'),
'password': kwargs.get('password'),
'port': kwargs.get('port', 23053),
})
config.read([os.path.expanduser('~/.gntp')])
# If the file does not exist, then there will be no gntp section defined
# and the config.get() lines below will get confused. Since we are not
# saving the config, it should be safe to just add it here so the
# code below doesn't complain
if not config.has_section('gntp'):
logger.info('Error reading ~/.gntp config file')
config.add_section('gntp')
kwargs['password'] = config.get('gntp', 'password')
kwargs['hostname'] = config.get('gntp', 'hostname')
kwargs['port'] = config.getint('gntp', 'port')
super(GrowlNotifier, self).__init__(*args, **kwargs)
def mini(description, **kwargs):
"""Single notification function
Simple notification function in one line. Has only one required parameter
and attempts to use reasonable defaults for everything else
:param string description: Notification message
"""
kwargs['notifierFactory'] = GrowlNotifier
gntp.notifier.mini(description, **kwargs)
if __name__ == '__main__':
# If we're running this module directly we're likely running it as a test
# so extra debugging is useful
logging.basicConfig(level=logging.INFO)
mini('Testing mini notification')
+518
View File
@@ -0,0 +1,518 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
import hashlib
import re
import time
import gntp.shim
import gntp.errors as errors
__all__ = [
'GNTPRegister',
'GNTPNotice',
'GNTPSubscribe',
'GNTPOK',
'GNTPError',
'parse_gntp',
]
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
GNTP_INFO_LINE = re.compile(
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
re.IGNORECASE
)
GNTP_INFO_LINE_SHORT = re.compile(
'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
re.IGNORECASE
)
GNTP_HEADER = re.compile('([\w-]+):(.+)')
GNTP_EOL = gntp.shim.b('\r\n')
GNTP_SEP = gntp.shim.b(': ')
class _GNTPBuffer(gntp.shim.StringIO):
"""GNTP Buffer class"""
def writeln(self, value=None):
if value:
self.write(gntp.shim.b(value))
self.write(GNTP_EOL)
def writeheader(self, key, value):
if not isinstance(value, str):
value = str(value)
self.write(gntp.shim.b(key))
self.write(GNTP_SEP)
self.write(gntp.shim.b(value))
self.write(GNTP_EOL)
class _GNTPBase(object):
"""Base initilization
:param string messagetype: GNTP Message type
:param string version: GNTP Protocol version
:param string encription: Encryption protocol
"""
def __init__(self, messagetype=None, version='1.0', encryption=None):
self.info = {
'version': version,
'messagetype': messagetype,
'encryptionAlgorithmID': encryption
}
self.hash_algo = {
'MD5': hashlib.md5,
'SHA1': hashlib.sha1,
'SHA256': hashlib.sha256,
'SHA512': hashlib.sha512,
}
self.headers = {}
self.resources = {}
# For Python2 we can just return the bytes as is without worry
# but on Python3 we want to make sure we return the packet as
# a unicode string so that things like logging won't get confused
if gntp.shim.PY2:
def __str__(self):
return self.encode()
else:
def __str__(self):
return gntp.shim.u(self.encode())
def _parse_info(self, data):
"""Parse the first line of a GNTP message to get security and other info values
:param string data: GNTP Message
:return dict: Parsed GNTP Info line
"""
match = GNTP_INFO_LINE.match(data)
if not match:
raise errors.ParseError('ERROR_PARSING_INFO_LINE')
info = match.groupdict()
if info['encryptionAlgorithmID'] == 'NONE':
info['encryptionAlgorithmID'] = None
return info
def set_password(self, password, encryptAlgo='MD5'):
"""Set a password for a GNTP Message
:param string password: Null to clear password
:param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
"""
if not password:
self.info['encryptionAlgorithmID'] = None
self.info['keyHashAlgorithm'] = None
return
self.password = gntp.shim.b(password)
self.encryptAlgo = encryptAlgo.upper()
if not self.encryptAlgo in self.hash_algo:
raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
hashfunction = self.hash_algo.get(self.encryptAlgo)
password = password.encode('utf8')
seed = time.ctime().encode('utf8')
salt = hashfunction(seed).hexdigest()
saltHash = hashfunction(seed).digest()
keyBasis = password + saltHash
key = hashfunction(keyBasis).digest()
keyHash = hashfunction(key).hexdigest()
self.info['keyHashAlgorithmID'] = self.encryptAlgo
self.info['keyHash'] = keyHash.upper()
self.info['salt'] = salt.upper()
def _decode_hex(self, value):
"""Helper function to decode hex string to `proper` hex string
:param string value: Human readable hex string
:return string: Hex string
"""
result = ''
for i in range(0, len(value), 2):
tmp = int(value[i:i + 2], 16)
result += chr(tmp)
return result
def _decode_binary(self, rawIdentifier, identifier):
rawIdentifier += '\r\n\r\n'
dataLength = int(identifier['Length'])
pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
pointerEnd = pointerStart + dataLength
data = self.raw[pointerStart:pointerEnd]
if not len(data) == dataLength:
raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
return data
def _validate_password(self, password):
"""Validate GNTP Message against stored password"""
self.password = password
if password is None:
raise errors.AuthError('Missing password')
keyHash = self.info.get('keyHash', None)
if keyHash is None and self.password is None:
return True
if keyHash is None:
raise errors.AuthError('Invalid keyHash')
if self.password is None:
raise errors.AuthError('Missing password')
keyHashAlgorithmID = self.info.get('keyHashAlgorithmID','MD5')
password = self.password.encode('utf8')
saltHash = self._decode_hex(self.info['salt'])
keyBasis = password + saltHash
self.key = self.hash_algo[keyHashAlgorithmID](keyBasis).digest()
keyHash = self.hash_algo[keyHashAlgorithmID](self.key).hexdigest()
if not keyHash.upper() == self.info['keyHash'].upper():
raise errors.AuthError('Invalid Hash')
return True
def validate(self):
"""Verify required headers"""
for header in self._requiredHeaders:
if not self.headers.get(header, False):
raise errors.ParseError('Missing Notification Header: ' + header)
def _format_info(self):
"""Generate info line for GNTP Message
:return string:
"""
info = 'GNTP/%s %s' % (
self.info.get('version'),
self.info.get('messagetype'),
)
if self.info.get('encryptionAlgorithmID', None):
info += ' %s:%s' % (
self.info.get('encryptionAlgorithmID'),
self.info.get('ivValue'),
)
else:
info += ' NONE'
if self.info.get('keyHashAlgorithmID', None):
info += ' %s:%s.%s' % (
self.info.get('keyHashAlgorithmID'),
self.info.get('keyHash'),
self.info.get('salt')
)
return info
def _parse_dict(self, data):
"""Helper function to parse blocks of GNTP headers into a dictionary
:param string data:
:return dict: Dictionary of parsed GNTP Headers
"""
d = {}
for line in data.split('\r\n'):
match = GNTP_HEADER.match(line)
if not match:
continue
key = match.group(1).strip()
val = match.group(2).strip()
d[key] = val
return d
def add_header(self, key, value):
self.headers[key] = value
def add_resource(self, data):
"""Add binary resource
:param string data: Binary Data
"""
data = gntp.shim.b(data)
identifier = hashlib.md5(data).hexdigest()
self.resources[identifier] = data
return 'x-growl-resource://%s' % identifier
def decode(self, data, password=None):
"""Decode GNTP Message
:param string data:
"""
self.password = password
self.raw = gntp.shim.u(data)
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(self.raw)
self.headers = self._parse_dict(parts[0])
def encode(self):
"""Encode a generic GNTP Message
:return string: GNTP Message ready to be sent. Returned as a byte string
"""
buff = _GNTPBuffer()
buff.writeln(self._format_info())
#Headers
for k, v in self.headers.items():
buff.writeheader(k, v)
buff.writeln()
#Resources
for resource, data in self.resources.items():
buff.writeheader('Identifier', resource)
buff.writeheader('Length', len(data))
buff.writeln()
buff.write(data)
buff.writeln()
buff.writeln()
return buff.getvalue()
class GNTPRegister(_GNTPBase):
"""Represents a GNTP Registration Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notifications-Count'
]
_requiredNotificationHeaders = ['Notification-Name']
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'REGISTER')
self.notifications = []
if data:
self.decode(data, password)
else:
self.set_password(password)
self.add_header('Application-Name', 'pygntp')
self.add_header('Notifications-Count', 0)
def validate(self):
'''Validate required headers and validate notification headers'''
for header in self._requiredHeaders:
if not self.headers.get(header, False):
raise errors.ParseError('Missing Registration Header: ' + header)
for notice in self.notifications:
for header in self._requiredNotificationHeaders:
if not notice.get(header, False):
raise errors.ParseError('Missing Notification Header: ' + header)
def decode(self, data, password):
"""Decode existing GNTP Registration message
:param string data: Message to decode
"""
self.raw = gntp.shim.u(data)
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(self.raw)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Notification-Name', False):
self.notifications.append(notice)
elif notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('register.png','wblol').write(notice['Data'])
self.resources[notice.get('Identifier')] = notice
def add_notification(self, name, enabled=True):
"""Add new Notification to Registration message
:param string name: Notification Name
:param boolean enabled: Enable this notification by default
"""
notice = {}
notice['Notification-Name'] = name
notice['Notification-Enabled'] = enabled
self.notifications.append(notice)
self.add_header('Notifications-Count', len(self.notifications))
def encode(self):
"""Encode a GNTP Registration Message
:return string: Encoded GNTP Registration message. Returned as a byte string
"""
buff = _GNTPBuffer()
buff.writeln(self._format_info())
#Headers
for k, v in self.headers.items():
buff.writeheader(k, v)
buff.writeln()
#Notifications
if len(self.notifications) > 0:
for notice in self.notifications:
for k, v in notice.items():
buff.writeheader(k, v)
buff.writeln()
#Resources
for resource, data in self.resources.items():
buff.writeheader('Identifier', resource)
buff.writeheader('Length', len(data))
buff.writeln()
buff.write(data)
buff.writeln()
buff.writeln()
return buff.getvalue()
class GNTPNotice(_GNTPBase):
"""Represents a GNTP Notification Command
:param string data: (Optional) See decode()
:param string app: (Optional) Set Application-Name
:param string name: (Optional) Set Notification-Name
:param string title: (Optional) Set Notification Title
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Application-Name',
'Notification-Name',
'Notification-Title'
]
def __init__(self, data=None, app=None, name=None, title=None, password=None):
_GNTPBase.__init__(self, 'NOTIFY')
if data:
self.decode(data, password)
else:
self.set_password(password)
if app:
self.add_header('Application-Name', app)
if name:
self.add_header('Notification-Name', name)
if title:
self.add_header('Notification-Title', title)
def decode(self, data, password):
"""Decode existing GNTP Notification message
:param string data: Message to decode.
"""
self.raw = gntp.shim.u(data)
parts = self.raw.split('\r\n\r\n')
self.info = self._parse_info(self.raw)
self._validate_password(password)
self.headers = self._parse_dict(parts[0])
for i, part in enumerate(parts):
if i == 0:
continue # Skip Header
if part.strip() == '':
continue
notice = self._parse_dict(part)
if notice.get('Identifier', False):
notice['Data'] = self._decode_binary(part, notice)
#open('notice.png','wblol').write(notice['Data'])
self.resources[notice.get('Identifier')] = notice
class GNTPSubscribe(_GNTPBase):
"""Represents a GNTP Subscribe Command
:param string data: (Optional) See decode()
:param string password: (Optional) Password to use while encoding/decoding messages
"""
_requiredHeaders = [
'Subscriber-ID',
'Subscriber-Name',
]
def __init__(self, data=None, password=None):
_GNTPBase.__init__(self, 'SUBSCRIBE')
if data:
self.decode(data, password)
else:
self.set_password(password)
class GNTPOK(_GNTPBase):
"""Represents a GNTP OK Response
:param string data: (Optional) See _GNTPResponse.decode()
:param string action: (Optional) Set type of action the OK Response is for
"""
_requiredHeaders = ['Response-Action']
def __init__(self, data=None, action=None):
_GNTPBase.__init__(self, '-OK')
if data:
self.decode(data)
if action:
self.add_header('Response-Action', action)
class GNTPError(_GNTPBase):
"""Represents a GNTP Error response
:param string data: (Optional) See _GNTPResponse.decode()
:param string errorcode: (Optional) Error code
:param string errordesc: (Optional) Error Description
"""
_requiredHeaders = ['Error-Code', 'Error-Description']
def __init__(self, data=None, errorcode=None, errordesc=None):
_GNTPBase.__init__(self, '-ERROR')
if data:
self.decode(data)
if errorcode:
self.add_header('Error-Code', errorcode)
self.add_header('Error-Description', errordesc)
def error(self):
return (self.headers.get('Error-Code', None),
self.headers.get('Error-Description', None))
def parse_gntp(data, password=None):
"""Attempt to parse a message as a GNTP message
:param string data: Message to be parsed
:param string password: Optional password to be used to verify the message
"""
data = gntp.shim.u(data)
match = GNTP_INFO_LINE_SHORT.match(data)
if not match:
raise errors.ParseError('INVALID_GNTP_INFO')
info = match.groupdict()
if info['messagetype'] == 'REGISTER':
return GNTPRegister(data, password=password)
elif info['messagetype'] == 'NOTIFY':
return GNTPNotice(data, password=password)
elif info['messagetype'] == 'SUBSCRIBE':
return GNTPSubscribe(data, password=password)
elif info['messagetype'] == '-OK':
return GNTPOK(data)
elif info['messagetype'] == '-ERROR':
return GNTPError(data)
raise errors.ParseError('INVALID_GNTP_MESSAGE')
+25
View File
@@ -0,0 +1,25 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
class BaseError(Exception):
pass
class ParseError(BaseError):
errorcode = 500
errordesc = 'Error parsing the message'
class AuthError(BaseError):
errorcode = 400
errordesc = 'Error with authorization'
class UnsupportedError(BaseError):
errorcode = 500
errordesc = 'Currently unsupported by gntp.py'
class NetworkError(BaseError):
errorcode = 500
errordesc = "Error connecting to growl server"
+90 -71
View File
@@ -1,3 +1,6 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
""" """
The gntp.notifier module is provided as a simple way to send notifications The gntp.notifier module is provided as a simple way to send notifications
using GNTP using GNTP
@@ -9,10 +12,15 @@ using GNTP
`Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_ `Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_
""" """
import gntp
import socket
import logging import logging
import platform import platform
import socket
import sys
from gntp.version import __version__
import gntp.core
import gntp.errors as errors
import gntp.shim
__all__ = [ __all__ = [
'mini', 'mini',
@@ -22,45 +30,6 @@ __all__ = [
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def mini(description, applicationName='PythonMini', noteType="Message",
title="Mini Message", applicationIcon=None, hostname='localhost',
password=None, port=23053, sticky=False, priority=None,
callback=None, notificationIcon=None, identifier=None):
"""Single notification function
Simple notification function in one line. Has only one required parameter
and attempts to use reasonable defaults for everything else
:param string description: Notification message
.. warning::
For now, only URL callbacks are supported. In the future, the
callback argument will also support a function
"""
growl = GrowlNotifier(
applicationName=applicationName,
notifications=[noteType],
defaultNotifications=[noteType],
applicationIcon=applicationIcon,
hostname=hostname,
password=password,
port=port,
)
result = growl.register()
if result is not True:
return result
return growl.notify(
noteType=noteType,
title=title,
description=description,
icon=notificationIcon,
sticky=sticky,
priority=priority,
callback=callback,
identifier=identifier,
)
class GrowlNotifier(object): class GrowlNotifier(object):
"""Helper class to simplfy sending Growl messages """Helper class to simplfy sending Growl messages
@@ -99,8 +68,9 @@ class GrowlNotifier(object):
If it's a simple URL icon, then we return True. If it's a data icon If it's a simple URL icon, then we return True. If it's a data icon
then we return False then we return False
''' '''
logger.debug('Checking icon') logger.info('Checking icon')
return data.startswith('http')
return gntp.shim.u(data)[:4] in ['http', 'file']
def register(self): def register(self):
"""Send GNTP Registration """Send GNTP Registration
@@ -109,8 +79,8 @@ class GrowlNotifier(object):
Before sending notifications to Growl, you need to have Before sending notifications to Growl, you need to have
sent a registration message at least once sent a registration message at least once
""" """
logger.debug('Sending registration to %s:%s', self.hostname, self.port) logger.info('Sending registration to %s:%s', self.hostname, self.port)
register = gntp.GNTPRegister() register = gntp.core.GNTPRegister()
register.add_header('Application-Name', self.applicationName) register.add_header('Application-Name', self.applicationName)
for notification in self.notifications: for notification in self.notifications:
enabled = notification in self.defaultNotifications enabled = notification in self.defaultNotifications
@@ -119,8 +89,8 @@ class GrowlNotifier(object):
if self._checkIcon(self.applicationIcon): if self._checkIcon(self.applicationIcon):
register.add_header('Application-Icon', self.applicationIcon) register.add_header('Application-Icon', self.applicationIcon)
else: else:
id = register.add_resource(self.applicationIcon) resource = register.add_resource(self.applicationIcon)
register.add_header('Application-Icon', id) register.add_header('Application-Icon', resource)
if self.password: if self.password:
register.set_password(self.password, self.passwordHash) register.set_password(self.password, self.passwordHash)
self.add_origin_info(register) self.add_origin_info(register)
@@ -128,7 +98,7 @@ class GrowlNotifier(object):
return self._send('register', register) return self._send('register', register)
def notify(self, noteType, title, description, icon=None, sticky=False, def notify(self, noteType, title, description, icon=None, sticky=False,
priority=None, callback=None, identifier=None): priority=None, callback=None, identifier=None, custom={}):
"""Send a GNTP notifications """Send a GNTP notifications
.. warning:: .. warning::
@@ -141,14 +111,16 @@ class GrowlNotifier(object):
:param boolean sticky: Sticky notification :param boolean sticky: Sticky notification
:param integer priority: Message priority level from -2 to 2 :param integer priority: Message priority level from -2 to 2
:param string callback: URL callback :param string callback: URL callback
:param dict custom: Custom attributes. Key names should be prefixed with X-
according to the spec but this is not enforced by this class
.. warning:: .. warning::
For now, only URL callbacks are supported. In the future, the For now, only URL callbacks are supported. In the future, the
callback argument will also support a function callback argument will also support a function
""" """
logger.debug('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port) logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
assert noteType in self.notifications assert noteType in self.notifications
notice = gntp.GNTPNotice() notice = gntp.core.GNTPNotice()
notice.add_header('Application-Name', self.applicationName) notice.add_header('Application-Name', self.applicationName)
notice.add_header('Notification-Name', noteType) notice.add_header('Notification-Name', noteType)
notice.add_header('Notification-Title', title) notice.add_header('Notification-Title', title)
@@ -162,8 +134,8 @@ class GrowlNotifier(object):
if self._checkIcon(icon): if self._checkIcon(icon):
notice.add_header('Notification-Icon', icon) notice.add_header('Notification-Icon', icon)
else: else:
id = notice.add_resource(icon) resource = notice.add_resource(icon)
notice.add_header('Notification-Icon', id) notice.add_header('Notification-Icon', resource)
if description: if description:
notice.add_header('Notification-Text', description) notice.add_header('Notification-Text', description)
@@ -172,6 +144,9 @@ class GrowlNotifier(object):
if identifier: if identifier:
notice.add_header('Notification-Coalescing-ID', identifier) notice.add_header('Notification-Coalescing-ID', identifier)
for key in custom:
notice.add_header(key, custom[key])
self.add_origin_info(notice) self.add_origin_info(notice)
self.notify_hook(notice) self.notify_hook(notice)
@@ -179,7 +154,7 @@ class GrowlNotifier(object):
def subscribe(self, id, name, port): def subscribe(self, id, name, port):
"""Send a Subscribe request to a remote machine""" """Send a Subscribe request to a remote machine"""
sub = gntp.GNTPSubscribe() sub = gntp.core.GNTPSubscribe()
sub.add_header('Subscriber-ID', id) sub.add_header('Subscriber-ID', id)
sub.add_header('Subscriber-Name', name) sub.add_header('Subscriber-Name', name)
sub.add_header('Subscriber-Port', port) sub.add_header('Subscriber-Port', port)
@@ -195,7 +170,7 @@ class GrowlNotifier(object):
"""Add optional Origin headers to message""" """Add optional Origin headers to message"""
packet.add_header('Origin-Machine-Name', platform.node()) packet.add_header('Origin-Machine-Name', platform.node())
packet.add_header('Origin-Software-Name', 'gntp.py') packet.add_header('Origin-Software-Name', 'gntp.py')
packet.add_header('Origin-Software-Version', gntp.__version__) packet.add_header('Origin-Software-Version', __version__)
packet.add_header('Origin-Platform-Name', platform.system()) packet.add_header('Origin-Platform-Name', platform.system())
packet.add_header('Origin-Platform-Version', platform.platform()) packet.add_header('Origin-Platform-Version', platform.platform())
@@ -214,34 +189,78 @@ class GrowlNotifier(object):
packet.validate() packet.validate()
data = packet.encode() data = packet.encode()
#logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data) logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
#Less verbose
logger.debug('To : %s:%s <%s>', self.hostname, self.port, packet.__class__)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(self.socketTimeout) s.settimeout(self.socketTimeout)
s.connect((self.hostname, self.port)) try:
s.send(data) s.connect((self.hostname, self.port))
recv_data = s.recv(1024) s.send(data)
while not recv_data.endswith("\r\n\r\n"): recv_data = s.recv(1024)
recv_data += s.recv(1024) while not recv_data.endswith(gntp.shim.b("\r\n\r\n")):
response = gntp.parse_gntp(recv_data) recv_data += s.recv(1024)
except socket.error:
# Python2.5 and Python3 compatibile exception
exc = sys.exc_info()[1]
raise errors.NetworkError(exc)
response = gntp.core.parse_gntp(recv_data)
s.close() s.close()
#logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response) logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
#Less verbose
logger.debug('From : %s:%s <%s>', self.hostname, self.port, response.__class__)
if type(response) == gntp.GNTPOK: if type(response) == gntp.core.GNTPOK:
return True
if response.error()[0] == '404' and 'disabled' in response.error()[1]:
# Ignore message saying that user has disabled this class
return True return True
logger.error('Invalid response: %s', response.error()) logger.error('Invalid response: %s', response.error())
return response.error() return response.error()
def mini(description, applicationName='PythonMini', noteType="Message",
title="Mini Message", applicationIcon=None, hostname='localhost',
password=None, port=23053, sticky=False, priority=None,
callback=None, notificationIcon=None, identifier=None,
notifierFactory=GrowlNotifier):
"""Single notification function
Simple notification function in one line. Has only one required parameter
and attempts to use reasonable defaults for everything else
:param string description: Notification message
.. warning::
For now, only URL callbacks are supported. In the future, the
callback argument will also support a function
"""
try:
growl = notifierFactory(
applicationName=applicationName,
notifications=[noteType],
defaultNotifications=[noteType],
applicationIcon=applicationIcon,
hostname=hostname,
password=password,
port=port,
)
result = growl.register()
if result is not True:
return result
return growl.notify(
noteType=noteType,
title=title,
description=description,
icon=notificationIcon,
sticky=sticky,
priority=priority,
callback=callback,
identifier=identifier,
)
except Exception:
# We want the "mini" function to be simple and swallow Exceptions
# in order to be less invasive
logger.exception("Growl error")
if __name__ == '__main__': if __name__ == '__main__':
# If we're running this module directly we're likely running it as a test # If we're running this module directly we're likely running it as a test
# so extra debugging is useful # so extra debugging is useful
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.INFO)
mini('Testing mini notification') mini('Testing mini notification')
+46
View File
@@ -0,0 +1,46 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
"""
Python2.5 and Python3.3 compatibility shim
Heavily inspirted by the "six" library.
https://pypi.python.org/pypi/six
"""
import sys
PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
if PY3:
def b(s):
if isinstance(s, bytes):
return s
return s.encode('utf8', 'replace')
def u(s):
if isinstance(s, bytes):
return s.decode('utf8', 'replace')
return s
from io import BytesIO as StringIO
from configparser import RawConfigParser
else:
def b(s):
if isinstance(s, unicode):
return s.encode('utf8', 'replace')
return s
def u(s):
if isinstance(s, unicode):
return s
if isinstance(s, int):
s = str(s)
return unicode(s, "utf8", "replace")
from StringIO import StringIO
from ConfigParser import RawConfigParser
b.__doc__ = "Ensure we have a byte string"
u.__doc__ = "Ensure we have a unicode string"
+4
View File
@@ -0,0 +1,4 @@
# Copyright: 2013 Paul Traylor
# These sources are released under the terms of the MIT license: see LICENSE
__version__ = '1.0.3'
+1 -1
View File
@@ -20,7 +20,7 @@ jQuery Form Plugin
* http://www.opensource.org/licenses/mit-license.php * http://www.opensource.org/licenses/mit-license.php
Bootstrap v3.3.6 (http://getbootstrap.com) Bootstrap v3.3.6 (http://getbootstrap.com)
* Licensed under the MIT license * Licensed under the MIT license
Changed by Safihre, Nov 2015 Changed by Safihre, Nov 2015
We include the icon-file directly into the CSS, We include the icon-file directly into the CSS,
this way we avoid errors when HTTPS is enabled this way we avoid errors when HTTPS is enabled
@@ -1,6 +1,6 @@
<!-- Content end --> <!-- Content end -->
<div class="clearfix"></div> <div class="clearfix"></div>
<!-- Filebrowser modal --> <!-- Filebrowser modal -->
<div class="modal fade" id="filebrowser_modal"> <div class="modal fade" id="filebrowser_modal">
<div class="modal-dialog"> <div class="modal-dialog">
@@ -28,7 +28,7 @@
</div> </div>
<div class="main-restarting modal-backdrop fade in" style="display: none;"> <div class="main-restarting modal-backdrop fade in" style="display: none;">
<div> <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> <small><span class="restarting-url"></span><span class="dotOne">.</span><span class="dotTwo">.</span><span class="dotThree">.</span></small>
</div> </div>
</div> </div>
+39 -13
View File
@@ -20,31 +20,35 @@
#if $pane == "Special" then $T('cmenu-special') else ""# #if $pane == "Special" then $T('cmenu-special') else ""#
#if $pane == "RSS" then $T('cmenu-rss') else ""# #if $pane == "RSS" then $T('cmenu-rss') else ""#
</title> </title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1" />
<meta name="apple-mobile-web-app-title" content="SABnzbd" /> <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="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="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="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="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="stylesheet" type="text/css" href="${root}staticcfg/css/chartist.min.css" />
<link rel="shortcut icon" href="${root}staticcfg/ico/favicon.ico?v=1.1.0" /> <link rel="stylesheet" type="text/css" href="${root}staticcfg/css/style.css?v=$version" />
<link rel="shortcut icon" href="${root}staticcfg/ico/favicon.ico?v=$version" />
<script type="text/javascript"> <script type="text/javascript">
// Keeping track of the form state // Keeping track of the form state
var formHasChanged = false; var formHasChanged = false;
var formWasSubmitted = false; var formWasSubmitted = false;
// Information we need // Information we need
var sabSession = '$session'; var sabSession = '$session';
var folderBrowseUrl = '${root}tapi?mode=browse&output=json&apikey=$session'; var rootURL = '${root}'
var urlBase = '${url_base}'
var folderBrowseUrl = '${root}tapi?mode=browse&output=json&apikey=$session';
var folderSeperator = '#if $os.sep == '\\' then '\\\\' else '/'#' var folderSeperator = '#if $os.sep == '\\' then '\\\\' else '/'#'
// Translations // Translations
var configTranslate = new Object(); var configTranslate = new Object();
configTranslate.browseText = "$T('browse-folder')"; configTranslate.browseText = "$T('browse-folder')";
@@ -52,14 +56,15 @@
configTranslate.saving = "$T('smpl-saving')"; configTranslate.saving = "$T('smpl-saving')";
configTranslate.failed = "$T('smpl-failed')"; configTranslate.failed = "$T('smpl-failed')";
configTranslate.explainRestart = "$T('explain-Restart')"; configTranslate.explainRestart = "$T('explain-Restart')";
configTranslate.wizzardRestart = "$T('wizard-restarting')"; configTranslate.wizzardRestart = "$T('restarting-sab')";
configTranslate.needRestart = "$T('restartRequired') $T('explain-needNewLogin')".replace(/\<br(\s*\/|)\>/g, '\n'); configTranslate.needRestart = "$T('restartRequired') $T('explain-needNewLogin')".replace(/\<br(\s*\/|)\>/g, '\n');
configTranslate.buttonRestart = "$T('button-restart')"; configTranslate.buttonRestart = "$T('button-restart')";
configTranslate.confirmLeave = "$T('confirmWithoutSavingPrompt')"; 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>
<script type="text/javascript" src="${root}staticcfg/js/jquery-1.11.2.min.js"></script> <script type="text/javascript" src="${root}staticcfg/js/jquery-3.2.1.min.js?v=$version"></script>
<script type="text/javascript" src="${root}staticcfg/bootstrap/js/bootstrap.min.js"></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?p=$pid"></script> <script type="text/javascript" src="${root}staticcfg/js/script.js?v=$version"></script>
<script type="text/javascript"> <script type="text/javascript">
// Set default functions for the autocomplete everywhere // Set default functions for the autocomplete everywhere
\$.extend(\$.fn.typeahead.defaults, { \$.extend(\$.fn.typeahead.defaults, {
@@ -83,6 +88,13 @@
return item return item
} }
}) })
// to top right away
if(window.location.hash) {
scroll(0,0);
// void some browsers issue
setTimeout(function() { scroll(0,0); }, 1);
}
</script> </script>
</head> </head>
@@ -174,9 +186,23 @@
<strong>$T('menu-help')</strong> <strong>$T('menu-help')</strong>
</a> </a>
</li> </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> </ul>
</div> </div>
</div> </div>
</nav> </nav>
<div id="content" class="container"> <div id="content" class="container">
<!-- Content start --> <!-- Content start -->
+46 -29
View File
@@ -1,11 +1,8 @@
<!--#set global $pane="Config"#--> <!--#set global $pane="Config"#-->
<!--#set global $help_uri="configuration/1.1/configure"#--> <!--#set global $help_uri="configuration/2.2/configure"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<!--#from sabnzbd.newswrapper import HAVE_SSL#-->
<!--#from locale import getpreferredencoding#--> <!--#from locale import getpreferredencoding#-->
<!--#import sabnzbd.utils.sslinfo#-->
<!--#from sabnzbd.decoder import HAVE_YENC#-->
<div class="colmask"> <div class="colmask">
<div class="section padTable"> <div class="section padTable">
<table class="table table-striped"> <table class="table table-striped">
@@ -22,10 +19,6 @@
<th scope="row">$T('confgFile'): </th> <th scope="row">$T('confgFile'): </th>
<td>$configfn</td> <td>$configfn</td>
</tr> </tr>
<tr>
<th scope="row">$T('cache').capitalize(): </th>
<td><!--#set $msg=$T('ft-buffer@2')%($cache_art, $cache_size)#-->$msg</td>
</tr>
<tr> <tr>
<th scope="row">$T('parameters'): </th> <th scope="row">$T('parameters'): </th>
<td>$cmdline</td> <td>$cmdline</td>
@@ -37,37 +30,57 @@
<tr> <tr>
<th scope="row">OpenSSL:</th> <th scope="row">OpenSSL:</th>
<td> <td>
<!--#if HAVE_SSL#--> $ssl_version
<!--#set $sslversions = ', '.join(sabnzbd.utils.sslinfo.ssl_protocols_labels())#-->
$sabnzbd.utils.sslinfo.ssl_version() &nbsp; [$sslversions]
<!--#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> </td>
</tr> </tr>
<!--#if not $have_ssl_context#-->
<tr> <tr>
<th scope="row">pyOpenSSL:</th> <th scope="row"></th>
<td> <td>
<!--#if HAVE_SSL#--> <span class="label label-danger">$T('warning')</span> $T('explain-nosslcontext')
$sabnzbd.utils.sslinfo.pyopenssl_version() </td>
</tr>
<!--#end if#-->
<!--#if not $nt and not $darwin#-->
<tr>
<th scope="row">$T('opt-multicore-par2')</th>
<td>
<!--#if $have_mt_par2#-->
<span class="glyphicon glyphicon-ok"></span>
<!--#else#--> <!--#else#-->
<span class="label label-warning">$T('notAvailable')</span> <span class="label label-warning">$T('notAvailable')</span> $T('explain-getpar2mt')
<a href="$helpuri$help_uri#no_ssl" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a> <a href="${helpuri}installation/multicore-par2" target="_blank">${helpuri}installation/multicore-par2</a>
<!--#end if#--> <!--#end if#-->
</td> </td>
</tr> </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> <tr>
<th scope="row">yEnc:</th> <th scope="row">yEnc:</th>
<td> <td>
<!--#if HAVE_YENC#--> <span class="label label-danger">$T('notAvailable')</span>
<span class="glyphicon glyphicon-ok"></span> <a href="$helpuri$help_uri#no_yenc" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
<!--#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> </td>
</tr> </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 #--> <!--#if not $have_unzip #-->
<tr> <tr>
<th scope="row">$T('opt-enable_unzip'):</th> <th scope="row">$T('opt-enable_unzip'):</th>
@@ -96,7 +109,7 @@
<tbody> <tbody>
<tr> <tr>
<th scope="row">$T('homePage') </th> <th scope="row">$T('homePage') </th>
<td><a href="http://sabnzbd.org/" target="_blank">http://sabnzbd.org/</a></td> <td><a href="https://sabnzbd.org/" target="_blank">https://sabnzbd.org/</a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">$T('menu-wiki') </th> <th scope="row">$T('menu-wiki') </th>
@@ -104,7 +117,7 @@
</tr> </tr>
<tr> <tr>
<th scope="row">$T('menu-forums') </th> <th scope="row">$T('menu-forums') </th>
<td><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td> <td><a href="https://forums.sabnzbd.org/" target="_blank">https://forums.sabnzbd.org/</a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">$T('source') </th> <th scope="row">$T('source') </th>
@@ -118,6 +131,10 @@
<th scope="row">$T('menu-issues') </th> <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> <td><a href="https://sabnzbd.org/wiki/introduction/known-issues" target="_blank">https://sabnzbd.org/wiki/introduction/known-issues</a></td>
</tr> </tr>
<tr>
<th scope="row">$T('menu-donate') </th>
<td><a href="https://sabnzbd.org/donate" target="_blank">https://sabnzbd.org/donate</a></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -125,7 +142,7 @@
<div class="colmask"> <div class="colmask">
<div class="padding alt"> <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> <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> <p class="copyright"><small>$T('yourRights')</small></p>
</div> </div>
+60 -27
View File
@@ -1,35 +1,34 @@
<!--#set global $pane="Categories"#--> <!--#set global $pane="Categories"#-->
<!--#set global $help_uri="configuration/1.1/categories"#--> <!--#set global $help_uri="configuration/2.2/categories"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask"> <div class="colmask">
<div class="section"> <div class="section">
<div class="padTable"> <a class="main-helplink" href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a> <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> <h5 class="darkred"><strong>$T('explain-relFolder'):</strong> <span class="path">$defdir</span></h5>
<!--#for $cur, $slot in enumerate($slotinfo)#-->
<!--#set $odd = False#--> <!--#set $cansort = $slot.name != '*' and $slot.name != ''#-->
<!--#set $cur = 0#--> <form action="save" method="get" <!--#if $cansort#-->class="sorting-row"<!--#end if#-->>
<!--#for $slot in $slotinfo#-->
<!--#set $odd = not $odd#-->
<!--#set $cur = $cur+1#-->
<form action="save" method="get">
<table class="catTable"> <table class="catTable">
<!--#if $cur == 1#--> <!--#if $cur == 0#-->
<tr> <tr>
<th></th>
<th>$T('category')</th> <th>$T('category')</th>
<th>$T('priority')</th> <th>$T('priority')</th>
<th>$T('mode')</th> <th>$T('mode')</th>
<!--#if $script_list#-->
<th>$T('script')</th> <th>$T('script')</th>
<!--#end if#-->
<th>$T('catFolderPath')</th> <th>$T('catFolderPath')</th>
<th>$T('catTags')</th> <th colspan="2">$T('catTags')</th>
<th>&nbsp;</th>
</tr> </tr>
<!--#end if#--> <!--#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> <td>
<input type="hidden" name="session" value="$session" /> <input type="hidden" name="session" value="$session" />
<input type="hidden" name="order" value="$slot.order" />
<input type="hidden" value="$slot.name" name="name" /> <input type="hidden" value="$slot.name" name="name" />
<!--#if $slot.name != '*'#--> <!--#if $slot.name != '*'#-->
<input type="text" name="newname" value="$slot.name" size="10" /> <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> <option value="3" <!--#if $slot.pp=="3" then 'selected="selected"' else ""#-->>$T('pp-delete')</option>
</select> </select>
</td> </td>
<!--#if $script_list#-->
<td> <td>
<!--#if $scripts#-->
<select name="script"> <select name="script">
<!--#for $sc in $script_list#--> <!--#for $sc in $scripts#-->
<!--#if not ($sc == 'Default' and $slot.name == '*')#--> <!--#if not ($sc == 'Default' and $slot.name == '*')#-->
<option value="$sc" <!--#if $slot.script.lower()==$sc.lower() then 'selected="selected"' else ""#-->>$Tspec($sc)</option> <option value="$sc" <!--#if $slot.script.lower()==$sc.lower() then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end if#--> <!--#end if#-->
<!--#end for#--> <!--#end for#-->
</select> </select>
<!--#else#-->
<select name="script" disabled>
</select>
<!--#end if#-->
</td> </td>
<!--#end if#-->
<td class="nowrap"> <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>
<td> <td>
<input type="text" name="newzbin" value="$slot.newzbin" size="20" /> <input type="text" name="newzbin" value="$slot.newzbin" size="20" />
</td> </td>
<td class="nowrap"> <td class="nowrap">
<button class="btn btn-default"> <button class="btn btn-default">
<!--#if $cur == 2#--><span class="glyphicon glyphicon-plus"></span> $T('button-add') <!--#if $cur == 1#-->
<!--#else#--><span class="glyphicon glyphicon-ok"></span> $T('button-save') <span class="glyphicon glyphicon-plus"></span> $T('button-add')
<!--#else#-->
<span class="glyphicon glyphicon-ok"></span> $T('button-save')
<!--#end if#--> <!--#end if#-->
</button> </button>
<!--#if $slot.name and $slot.name != '*'#--> <!--#if $cansort#-->
<button class="btn btn-default delCat" type="button"><span class="glyphicon glyphicon-trash"></span></button> <button class="btn btn-default delCat" type="button"><span class="glyphicon glyphicon-trash"></span></button>
<!--#end if#--> <!--#end if#-->
</td> </td>
</tr> </tr>
</table> </table>
</form> </form>
<!--#if not $cansort#-->
<hr>
<!--#end if#-->
<!--#end for#--> <!--#end for#-->
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript" src="${root}staticcfg/js/jquery-ui.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
\$(document).ready(function() { \$(document).ready(function() {
\$('.delCat').click(function() { \$('.delCat').click(function() {
var theForm = \$(this).closest("form"); var theForm = \$(this).closest("form");
theForm.attr("action", "delete").submit(); theForm.attr("action", "delete").submit();
}); });
// Add autocomplete and file-browser // Add autocomplete and file-browser
\$('.fileBrowserSmall').typeahead().fileBrowser(); \$('.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> </script>
@@ -1,5 +1,5 @@
<!--#set global $pane="Folders"#--> <!--#set global $pane="Folders"#-->
<!--#set global $help_uri="configuration/1.1/folders"#--> <!--#set global $help_uri="configuration/2.2/folders"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask"> <div class="colmask">
@@ -65,7 +65,7 @@
</div> </div>
<div class="field-pair"> <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 saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span id="config_err_msg" class="darkred nomargin">&nbsp;</span> <span id="config_err_msg" class="darkred nomargin">&nbsp;</span>
</div> </div>
+124 -66
View File
@@ -1,5 +1,5 @@
<!--#set global $pane="General"#--> <!--#set global $pane="General"#-->
<!--#set global $help_uri="configuration/1.1/general"#--> <!--#set global $help_uri="configuration/2.2/general"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask"> <div class="colmask">
@@ -23,6 +23,11 @@
<input type="number" name="port" id="port" value="$port" size="8" data-original="$port" /> <input type="number" name="port" id="port" value="$port" size="8" data-original="$port" />
<span class="desc">$T('explain-port')</span> <span class="desc">$T('explain-port')</span>
</div> </div>
<div class="field-pair">
<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" data-original="1"' else ""#-->/>
<span class="desc">$T('explain-enable_https')</span>
</div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="web_dir">$T('opt-web_dir')</label> <label class="config" for="web_dir">$T('opt-web_dir')</label>
<select name="web_dir" id="web_dir"> <select name="web_dir" id="web_dir">
@@ -34,21 +39,7 @@
<!--#end if#--> <!--#end if#-->
<!--#end for#--> <!--#end for#-->
</select> </select>
<span class="desc">$T('explain-web_dir')&nbsp;&nbsp;<a href="$caller_url1">$caller_url1</a></span> <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="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>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="language">$T('opt-language')</label> <label class="config" for="language">$T('opt-language')</label>
@@ -66,6 +57,50 @@
$T('explain-ask-language') <a href="https://sabnzbd.org/wiki/translate" target="_blank" class="alert-link">https://sabnzbd.org/wiki/translate</a> $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> </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" data-original="$https_port" />
<span class="desc">$T('explain-https_port')</span>
</div>
<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 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 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>
</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>
<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 --> <!-- Tricks to avoid browser auto-fill, fixed on-submit with javascript -->
<div class="field-pair"> <div class="field-pair">
<label class="config" for="${pid}_wu">$T('opt-web_username')</label> <label class="config" for="${pid}_wu">$T('opt-web_username')</label>
@@ -76,29 +111,28 @@
<label class="config" for="${pid}_wp">$T('opt-web_password')</label> <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" /> <input type="text" name="${pid}_wp" id="${pid}_wp" value="$password" data-hide="password" />
<span class="desc">$T('explain-web_password')</span> <span class="desc">$T('explain-web_password')</span>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="inet_exposure">$T('opt-inet_exposure')</label> <label class="config" for="inet_exposure">$T('opt-inet_exposure')</label>
<select name="inet_exposure" id="inet_exposure" class="select"> <select name="inet_exposure" id="inet_exposure" class="select">
<option value="0" <!--#if $inet_exposure == 0 then 'selected="selected"' else ""#-->>$T('inet-local')</option> <optgroup label="API">
<option value="1" <!--#if $inet_exposure == 1 then 'selected="selected"' else ""#-->>$T('inet-nzb')</option> <option value="0" <!--#if $inet_exposure == 0 then 'selected="selected"' else ""#-->>$T('inet-local')</option>
<option value="2" <!--#if $inet_exposure == 2 then 'selected="selected"' else ""#-->>$T('inet-api')</option> <option value="1" <!--#if $inet_exposure == 1 then 'selected="selected"' else ""#-->>$T('inet-nzb')</option>
<option value="3" <!--#if $inet_exposure == 3 then 'selected="selected"' else ""#-->>$T('inet-fullapi')</option> <option value="2" <!--#if $inet_exposure == 2 then 'selected="selected"' else ""#-->>$T('inet-api')</option>
<option value="4" <!--#if $inet_exposure == 4 then 'selected="selected"' else ""#-->>$T('inet-ui')</option> <option value="3" <!--#if $inet_exposure == 3 then 'selected="selected"' else ""#-->>$T('inet-fullapi')</option>
<option value="5" <!--#if $inet_exposure == 5 then 'selected="selected"' else ""#-->>$T('inet-ui') - $T('inet-external_login')</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> </select>
<span class="desc">$T('explain-inet_exposure')</span> <span class="desc">$T('explain-inet_exposure').replace('. ','.<br><span class="label label-warning">'+$T('warning').upper()+'</span> ')</span>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="local_ranges">$T('opt-local_ranges')</label> <label class="config" for="local_ranges">$T('opt-local_ranges')</label>
<input type="text" name="local_ranges" id="local_ranges" value="$local_ranges" /> <input type="text" name="local_ranges" id="local_ranges" value="$local_ranges" />
<span class="desc">$T('explain-local_ranges')</span> <span class="desc">$T('explain-local_ranges')</span>
</div> </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"> <div class="field-pair">
<label class="config" for="apikey">$T('opt-apikey')</label> <label class="config" for="apikey">$T('opt-apikey')</label>
<input type="text" id="apikey" class="fileBrowserField" value="$session" readonly /> <input type="text" id="apikey" class="fileBrowserField" value="$session" readonly />
@@ -115,51 +149,37 @@
</div> </div>
<div class="field-pair"> <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 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> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div><!-- /col1 -->
</div><!-- /section --> </div><!-- /section -->
<div class="section"> <div class="section">
<div class="col2"> <div class="col2">
<h3>$T('httpsSupport') <a href="$helpuri$help_uri#toc1" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3> <h3>$T('cmenu-switches') <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><!-- /col2 -->
<div class="col1"> <div class="col1">
<fieldset> <fieldset>
<div class="field-pair"> <div class="field-pair">
<h5 class="darkred nomargin">$T('base-folder'): <span class="path">$my_lcldata</span></h5> <label class="config" for="auto_browser">$T('opt-auto_browser')</label>
</div> <input type="checkbox" name="auto_browser" id="auto_browser" value="1" <!--#if int($auto_browser) > 0 then 'checked="checked"' else ""#--> />
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->"> <span class="desc">$T('explain-auto_browser')</span>
<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 ""#--> />
<span class="desc">$T('explain-enable_https')</span>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="https_port">$T('opt-https_port')</label> <label class="config" for="check_new_rel">$T('opt-check_new_rel')</label>
<input type="number" name="https_port" id="https_port" value="$https_port" size="8" data-original="$https_port" /> <select name="check_new_rel" id="check_new_rel">
<span class="desc">$T('explain-https_port')</span> <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>
<div class="field-pair"> <div class="field-pair <!--#if int($have_ssl_context) == 0 then "disabled" else ""#-->">
<label class="config" for="https_cert">$T('opt-https_cert')</label> <label class="config" for="enable_https_verification">$T('opt-enable_https_verification')</label>
<input type="text" name="https_cert" id="https_cert" value="$https_cert" /> <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 ""#--> />
<button class="btn btn-default generate_cert" title="$T('explain-new-cert')"><span class="glyphicon glyphicon-repeat"></span></button> <span class="desc">$T('explain-enable_https_verification')</span>
<span class="desc">$T('explain-https_cert')</span>
</div>
<div class="field-pair">
<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')"><span class="glyphicon glyphicon-repeat"></span></button>
<span class="desc">$T('explain-https_key')</span>
</div>
<div class="field-pair">
<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>
</div> </div>
<div class="field-pair"> <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 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> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div><!-- /col1 -->
@@ -170,10 +190,15 @@
</div><!-- /col2 --> </div><!-- /col2 -->
<div class="col1"> <div class="col1">
<fieldset> <fieldset>
<div class="field-pair"> <div class="field-pair value-and-select">
<label class="config" for="bandwidth_max">$T('opt-bandwidth_max')</label> <label class="config" for="bandwidth_max_value">$T('opt-bandwidth_max')</label>
<input type="text" name="bandwidth_max" id="bandwidth_max" value="$bandwidth_max" class="smaller_input" /> <input type="number" name="bandwidth_max_value" id="bandwidth_max_value" class="smaller_input" />
<span class="desc">$T('explain-bandwidth_max')<br />$T('wizard-bandwidth-explain')</span> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="bandwidth_perc">$T('opt-bandwidth_perc')</label> <label class="config" for="bandwidth_perc">$T('opt-bandwidth_perc')</label>
@@ -191,6 +216,7 @@
</fieldset> </fieldset>
</div><!-- /col1 --> </div><!-- /col1 -->
</div><!-- /section --> </div><!-- /section -->
</form> </form>
</div><!-- /colmask --> </div><!-- /colmask -->
@@ -214,11 +240,21 @@
\$('.alert-translate').show() \$('.alert-translate').show()
} }
} }
\$('#language').on('change', hideOrShowTranslate) \$('#language').on('change', function() {
// Show message
hideOrShowTranslate()
// Re-load page on submit
\$('.fullform').submit(function() {
// Skip the fancy stuff, just submit
this.submit()
})
// No JSON reponse
\$('#ajax').val('')
})
hideOrShowTranslate() hideOrShowTranslate()
\$('#apikey, #nzbkey').click(function () { \$(this).select() }); \$('#apikey, #nzbkey').click(function () { \$(this).select() });
\$('#generate_new_apikey').click(function () { \$('#generate_new_apikey').click(function () {
if (confirm("$T('Plush-confirm')")) { if (confirm("$T('Plush-confirm')")) {
$.ajax({ $.ajax({
@@ -255,9 +291,9 @@
"text": \$(this).attr('rel') "text": \$(this).attr('rel')
}); });
\$('#modal_qr').modal('show'); \$('#modal_qr').modal('show');
// No save on this button click // No save on this button click
e.preventDefault(); e.preventDefault();
}); });
\$('.generate_cert').click(function(e) { \$('.generate_cert').click(function(e) {
@@ -282,6 +318,28 @@
if(\$('#https_cert').val() != 'server.cert') { if(\$('#https_cert').val() != 'server.cert') {
\$('.generate_cert').attr('disabled', 'disabled') \$('.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, '');
if(bandwithLimitNumber) {
\$('#bandwidth_max_value').val(bandwithLimitNumber)
\$('#bandwidth_max_dropdown').val(bandwithLimitText)
}
}
// Update the value
\$('#bandwidth_max_value, #bandwidth_max_dropdown').on('change', function() {
if(\$('#bandwidth_max_value').val()) {
\$('#bandwidth_max').val(\$('#bandwidth_max_value').val() + \$('#bandwidth_max_dropdown').val())
} else {
\$('#bandwidth_max').val('')
}
})
}); });
</script> </script>
+182 -604
View File
@@ -1,7 +1,30 @@
<!--#set global $pane="Email"#--> <!--#set global $pane="Email"#-->
<!--#set global $help_uri="configuration/1.1/notifications"#--> <!--#set global $help_uri="configuration/2.2/notifications"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#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#-->
<!--#def show_cat_box($section_label)#-->
<div class="col2-cats" <!--#if int($getVar($section_label + '_enable')) > 0 then '' else 'style="display:none"'#-->>
<hr>
<b>$T('affectedCat')</b><br/>
<select name="${section_label}_cats" multiple="multiple" class="multiple_cats">
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct in $getVar($section_label + '_cats') then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
<!--#end for#-->
</select>
</div>
<!--#end def#-->
<div class="colmask"> <div class="colmask">
<form action="saveEmail" method="post" name="fullform" class="fullform" autocomplete="off" novalidate> <form action="saveEmail" method="post" name="fullform" class="fullform" autocomplete="off" novalidate>
<input type="hidden" id="session" name="session" value="$session" /> <input type="hidden" id="session" name="session" value="$session" />
@@ -9,7 +32,15 @@
<div class="section" id="email"> <div class="section" id="email">
<div class="col2"> <div class="col2">
<h3>$T('cmenu-email') <a href="$helpuri$help_uri#toc0" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3> <h3>$T('cmenu-email') <a href="$helpuri$help_uri#toc0" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 --> <div class="col2-cats" <!--#if int($email_endjob) > 0 then '' else 'style="display:none"'#-->>
<b>$T('affectedCat')</b><br/>
<select name="email_cats" multiple="multiple" class="multiple_cats">
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct in $email_cats then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
<!--#end for#-->
</select>
</div>
</div>
<div class="col1"> <div class="col1">
<fieldset> <fieldset>
<div class="field-pair"> <div class="field-pair">
@@ -38,7 +69,7 @@
<div class="field-pair"> <div class="field-pair">
<label class="config" for="email_server">$T('opt-email_server')</label> <label class="config" for="email_server">$T('opt-email_server')</label>
<input type="text" name="email_server" id="email_server" value="$email_server" /> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="email_to">$T('opt-email_to')</label> <label class="config" for="email_to">$T('opt-email_to')</label>
@@ -68,8 +99,8 @@
<div class="alert"></div> <div class="alert"></div>
</div> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div>
</div><!-- /section --> </div>
<!--#if $have_ncenter#--> <!--#if $have_ncenter#-->
<div class="section"> <div class="section">
<div class="col2"> <div class="col2">
@@ -80,53 +111,10 @@
<td><label for="ncenter_enable"> $T('opt-ncenter_enable')</label></td> <td><label for="ncenter_enable"> $T('opt-ncenter_enable')</label></td>
</tr> </tr>
</table> </table>
</div><!-- /col2 --> </div>
<div class="col1" <!--#if int($ncenter_enable) > 0 then '' else 'style="display:none"'#-->> <div class="col1" <!--#if int($ncenter_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset> <fieldset>
<div class="field-pair"> $show_notify_checkboxes('ncenter')
<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_new_login">$T($notify_texts['new_login'])</label>
<input type="checkbox" name="ncenter_prio_new_login" id="ncenter_prio_new_login" value="1" <!--#if int($ncenter_prio_new_login) > 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"> <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 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> <button class="btn btn-default" type="button" id="test_notif"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
@@ -135,8 +123,8 @@
<div class="alert"></div> <div class="alert"></div>
</div> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div>
</div><!-- /section --> </div>
<!--#end if#--> <!--#end if#-->
<!--#if $nt#--> <!--#if $nt#-->
<div class="section"> <div class="section">
@@ -148,53 +136,11 @@
<td><label for="acenter_enable"> $T('opt-acenter_enable')</label></td> <td><label for="acenter_enable"> $T('opt-acenter_enable')</label></td>
</tr> </tr>
</table> </table>
</div><!-- /col2 --> $show_cat_box('acenter')
</div>
<div class="col1" <!--#if int($acenter_enable) > 0 then '' else 'style="display:none"'#-->> <div class="col1" <!--#if int($acenter_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset> <fieldset>
<div class="field-pair"> $show_notify_checkboxes('acenter')
<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_new_login">$T($notify_texts['new_login'])</label>
<input type="checkbox" name="acenter_prio_new_login" id="acenter_prio_new_login" value="1" <!--#if int($acenter_prio_new_login) > 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"> <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 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> <button class="btn btn-default" type="button" id="test_windows"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
@@ -203,8 +149,8 @@
<div class="alert"></div> <div class="alert"></div>
</div> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div>
</div><!-- /section --> </div>
<!--#end if#--> <!--#end if#-->
<!--#if $have_ntfosd#--> <!--#if $have_ntfosd#-->
<div class="section"> <div class="section">
@@ -216,53 +162,11 @@
<td><label for="ntfosd_enable"> $T('opt-ntfosd_enable')</label></td> <td><label for="ntfosd_enable"> $T('opt-ntfosd_enable')</label></td>
</tr> </tr>
</table> </table>
</div><!-- /col2 --> $show_cat_box('ntfosd')
</div>
<div class="col1" <!--#if int($ntfosd_enable) > 0 then '' else 'style="display:none"'#-->> <div class="col1" <!--#if int($ntfosd_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset> <fieldset>
<div class="field-pair"> $show_notify_checkboxes('ntfosd')
<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_new_login">$T($notify_texts['new_login'])</label>
<input type="checkbox" name="ntfosd_prio_new_login" id="ntfosd_prio_new_login" value="1" <!--#if int($ntfosd_prio_new_login) > 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"> <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 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> <button class="btn btn-default" type="button" id="test_osd"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
@@ -271,9 +175,48 @@
<div class="alert"></div> <div class="alert"></div>
</div> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div>
</div><!-- /section --> </div>
<!--#end if#--> <!--#end if#-->
<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><br><a href="$helpuri$help_uri#nscript" target="_blank">$T('readwiki')</a>
$show_cat_box('nscript')
</div>
<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>
</div>
<div class="section" id="growl"> <div class="section" id="growl">
<div class="col2"> <div class="col2">
<h3>$T('growlSettings') <a href="$helpuri$help_uri#toc3" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3> <h3>$T('growlSettings') <a href="$helpuri$help_uri#toc3" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
@@ -283,7 +226,8 @@
<td><label for="growl_enable"> $T('opt-growl_enable')</label></td> <td><label for="growl_enable"> $T('opt-growl_enable')</label></td>
</tr> </tr>
</table> </table>
</div><!-- /col2 --> $show_cat_box('growl')
</div>
<div class="col1" <!--#if int($growl_enable) > 0 then '' else 'style="display:none"'#-->> <div class="col1" <!--#if int($growl_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset> <fieldset>
<div class="field-pair"> <div class="field-pair">
@@ -296,50 +240,7 @@
<input type="text" name="growl_password" id="growl_password" value="$growl_password" /> <input type="text" name="growl_password" id="growl_password" value="$growl_password" />
<span class="desc">$T('explain-growl_password')</span> <span class="desc">$T('explain-growl_password')</span>
</div> </div>
<div class="field-pair"> $show_notify_checkboxes('growl')
<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 ""#--> />
</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>
<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_new_login">$T($notify_texts['new_login'])</label>
<input type="checkbox" name="growl_prio_new_login" id="growl_prio_new_login" value="1" <!--#if int($growl_prio_new_login) > 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"> <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 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> <button class="btn btn-default" type="button" id="test_growl"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
@@ -348,8 +249,8 @@
<div class="alert"></div> <div class="alert"></div>
</div> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div>
</div><!-- /section --> </div>
<div class="section" id="prowl"> <div class="section" id="prowl">
<div class="col2"> <div class="col2">
<h3>$T('section-Prowl')</h3> <h3>$T('section-Prowl')</h3>
@@ -360,7 +261,8 @@
</tr> </tr>
</table> </table>
<em>$T('explain-prowl_enable')</em> <em>$T('explain-prowl_enable')</em>
</div><!-- /col2 --> $show_cat_box('prowl')
</div>
<div class="col1" <!--#if int($prowl_enable) > 0 then '' else 'style="display:none"'#-->> <div class="col1" <!--#if int($prowl_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset> <fieldset>
<div class="field-pair"> <div class="field-pair">
@@ -368,127 +270,22 @@
<input type="text" name="prowl_apikey" id="prowl_apikey" value="$prowl_apikey" /> <input type="text" name="prowl_apikey" id="prowl_apikey" value="$prowl_apikey" />
<span class="desc">$T('explain-prowl_apikey')</span> <span class="desc">$T('explain-prowl_apikey')</span>
</div> </div>
<div class="field-pair"> <!--#set $section_label = 'prowl'#-->
<label class="config" for="prowl_prio_startup">$T($notify_texts['startup']).replace('/', ' / ')</label> <!--#for $type in $notify_keys#-->
<select name="prowl_prio_startup" id="prowl_prio_startup"> <div class="field-pair">
<option value="-3" <!--#if $prowl_prio_startup == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option> <label class="config" for="${section_label}_prio_$type">
<option value="-2" <!--#if $prowl_prio_startup == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option> $T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
<option value="-1" <!--#if $prowl_prio_startup == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option> </label>
<option value="0" <!--#if $prowl_prio_startup == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option> <select name="${section_label}_prio_$type" id="${section_label}_prio_$type">
<option value="1" <!--#if $prowl_prio_startup == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option> <option value="-3" <!--#if $getVar($section_label + '_prio_' + $type) == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="2" <!--#if $prowl_prio_startup == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option> <option value="-2" <!--#if $getVar($section_label + '_prio_' + $type) == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
</select> <option value="-1" <!--#if $getVar($section_label + '_prio_' + $type) == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
</div> <option value="0" <!--#if $getVar($section_label + '_prio_' + $type) == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<div class="field-pair"> <option value="1" <!--#if $getVar($section_label + '_prio_' + $type) == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<label class="config" for="prowl_prio_download">$T($notify_texts['download']) / $T('link-pause') / $T('link-resume')</label> <option value="2" <!--#if $getVar($section_label + '_prio_' + $type) == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
<select name="prowl_prio_download" id="prowl_prio_download"> </select>
<option value="-3" <!--#if $prowl_prio_download == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option> </div>
<option value="-2" <!--#if $prowl_prio_download == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option> <!--#end for#-->
<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_new_login">$T($notify_texts['new_login'])</label>
<select name="prowl_prio_new_login" id="prowl_prio_new_login">
<option value="-3" <!--#if $prowl_prio_new_login == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_new_login == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_new_login == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_new_login == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_new_login == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_new_login == 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"> <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 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> <button class="btn btn-default" type="button" id="test_prowl"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
@@ -497,12 +294,12 @@
<div class="alert"></div> <div class="alert"></div>
</div> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div>
</div><!-- /section --> </div>
<div class="section" id="pushover"> <div class="section" id="pushover">
<div class="col2"> <div class="col2">
<h3>$T('section-Pushover')</h3> <h3>$T('section-Pushover')</h3>
<table> <table>
<tr> <tr>
<td><input type="checkbox" name="pushover_enable" id="pushover_enable" value="1" <!--#if int($pushover_enable) > 0 then 'checked="checked"' else ""#--> /></td> <td><input type="checkbox" name="pushover_enable" id="pushover_enable" value="1" <!--#if int($pushover_enable) > 0 then 'checked="checked"' else ""#--> /></td>
@@ -510,146 +307,41 @@
</tr> </tr>
</table> </table>
<em>$T('explain-pushover_enable')</em> <em>$T('explain-pushover_enable')</em>
</div><!-- /col2 --> $show_cat_box('pushover')
</div>
<div class="col1" <!--#if int($pushover_enable) > 0 then '' else 'style="display:none"'#-->> <div class="col1" <!--#if int($pushover_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset> <fieldset>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="pushover_token">$T('opt-pushover_token')</label> <label class="config" for="pushover_token">$T('opt-pushover_token')</label>
<input type="text" name="pushover_token" id="pushover_token" value="$pushover_token" /> <input type="text" name="pushover_token" id="pushover_token" value="$pushover_token" />
<span class="desc">$T('explain-pushover_token')</span> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="pushover_prio_new_login">$T($notify_texts['new_login'])</label> <label class="config" for="pushover_userkey">$T('opt-pushover_userkey')</label>
<select name="pushover_prio_new_login" id="pushover_prio_new_login"> <input type="text" name="pushover_userkey" id="pushover_userkey" value="$pushover_userkey" />
<option value="-3" <!--#if $pushover_prio_new_login == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option> <span class="desc">$T('explain-pushover_userkey')</span>
<option value="-2" <!--#if $pushover_prio_new_login == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $pushover_prio_new_login == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $pushover_prio_new_login == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $pushover_prio_new_login == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $pushover_prio_new_login == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="pushover_prio_warning">$T($notify_texts['warning'])</label> <label class="config" for="pushover_device">$T('opt-pushover_device')</label>
<select name="pushover_prio_warning" id="pushover_prio_warning"> <input type="text" name="pushover_device" id="pushover_device" value="$pushover_device" />
<option value="-3" <!--#if $pushover_prio_warning == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option> <span class="desc">$T('explain-pushover_device')</span>
<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>
<!--#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"> <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 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> <button class="btn btn-default" type="button" id="test_pushover"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
@@ -658,11 +350,11 @@
<div class="alert"></div> <div class="alert"></div>
</div> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div>
</div><!-- /section --> </div>
<div class="section" id="pushbullet"> <div class="section" id="pushbullet">
<div class="col2"> <div class="col2">
<h3>$T('section-Pushbullet')</h3> <h3>$T('section-Pushbullet')</h3>
<table> <table>
<tr> <tr>
<td><input type="checkbox" name="pushbullet_enable" id="pushbullet_enable" value="1" <!--#if int($pushbullet_enable) > 0 then 'checked="checked"' else ""#--> /></td> <td><input type="checkbox" name="pushbullet_enable" id="pushbullet_enable" value="1" <!--#if int($pushbullet_enable) > 0 then 'checked="checked"' else ""#--> /></td>
@@ -670,156 +362,33 @@
</tr> </tr>
</table> </table>
<em>$T('explain-pushbullet_enable')</em> <em>$T('explain-pushbullet_enable')</em>
</div><!-- /col2 --> $show_cat_box('pushbullet')
</div>
<div class="col1" <!--#if int($pushbullet_enable) > 0 then '' else 'style="display:none"'#-->> <div class="col1" <!--#if int($pushbullet_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset> <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"> <div class="field-pair">
<label class="config wide" for="pushbullet_prio_new_login">$T($notify_texts['new_login'])</label> <label class="config" for="pushbullet_apikey">$T('opt-pushbullet_apikey')</label>
<input type="checkbox" name="pushbullet_prio_new_login" id="pushbullet_prio_new_login" value="1" <!--#if int($pushbullet_prio_new_login) > 0 then 'checked="checked"' else ""#--> /> <input type="text" name="pushbullet_apikey" id="pushbullet_apikey" value="$pushbullet_apikey" />
<span class="desc">$T('explain-pushbullet_apikey')</span>
</div> </div>
<div class="field-pair"> <!--#if 0#-->
<label class="config wide" for="pushbullet_prio_warning">$T($notify_texts['warning'])</label> <div class="field-pair">
<input type="checkbox" name="pushbullet_prio_warning" id="pushbullet_prio_warning" value="1" <!--#if int($pushbullet_prio_warning) > 0 then 'checked="checked"' else ""#--> /> <label class="config" for="pushbullet_device">$T('opt-pushbullet_device')</label>
</div> <input type="text" name="pushbullet_device" id="pushbullet_device" value="$pushbullet_device" />
<div class="field-pair"> <span class="desc">$T('explain-pushbullet_device')</span>
<label class="config wide" for="pushbullet_prio_error">$T($notify_texts['error'])</label> </div>
<input type="checkbox" name="pushbullet_prio_error" id="pushbullet_prio_error" value="1" <!--#if int($pushbullet_prio_error) > 0 then 'checked="checked"' else ""#--> /> <!--#end if#-->
</div> $show_notify_checkboxes('pushbullet')
<div class="field-pair"> <div class="field-pair">
<label class="config wide" for="pushbullet_prio_other">$T($notify_texts['other'])</label> <button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<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>
<button class="btn btn-default" type="button" id="test_pushbullet"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button> <button class="btn btn-default" type="button" id="test_pushbullet"><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 -->
<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 $script_list#-->
<option value="$sc" <!--#if $nscript_script.lower()==$sc.lower() 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>
<div class="field-pair">
<label class="config wide" for="nscript_prio_startup">$T($notify_texts['startup']).replace('/', ' / ')</label>
<input type="checkbox" name="nscript_prio_startup" id="nscript_prio_startup" value="1" <!--#if int($nscript_prio_startup) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_download">$T($notify_texts['download']) / $T('link-pause') / $T('link-resume')</label>
<input type="checkbox" name="nscript_prio_download" id="nscript_prio_download" value="1" <!--#if int($nscript_prio_download) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_pp">$T($notify_texts['pp'])</label>
<input type="checkbox" name="nscript_prio_pp" id="nscript_prio_pp" value="1" <!--#if int($nscript_prio_pp) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_complete">$T($notify_texts['complete'])</label>
<input type="checkbox" name="nscript_prio_complete" id="nscript_prio_complete" value="1" <!--#if int($nscript_prio_complete) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_failed">$T($notify_texts['failed'])</label>
<input type="checkbox" name="nscript_prio_failed" id="nscript_prio_failed" value="1" <!--#if int($nscript_prio_failed) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_queue_done">$T($notify_texts['queue_done'])</label>
<input type="checkbox" name="nscript_prio_queue_done" id="nscript_prio_queue_done" value="1" <!--#if int($nscript_prio_queue_done) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_disk_full">$T($notify_texts['disk_full'])</label>
<input type="checkbox" name="nscript_prio_disk_full" id="nscript_prio_disk_full" value="1" <!--#if int($nscript_prio_disk_full) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_new_login">$T($notify_texts['new_login'])</label>
<input type="checkbox" name="nscript_prio_new_login" id="nscript_prio_new_login" value="1" <!--#if int($nscript_prio_new_login) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_warning">$T($notify_texts['warning'])</label>
<input type="checkbox" name="nscript_prio_warning" id="nscript_prio_warning" value="1" <!--#if int($nscript_prio_warning) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_error">$T($notify_texts['error'])</label>
<input type="checkbox" name="nscript_prio_error" id="nscript_prio_error" value="1" <!--#if int($nscript_prio_error) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="nscript_prio_other">$T($notify_texts['other'])</label>
<input type="checkbox" name="nscript_prio_other" id="nscript_prio_other" value="1" <!--#if int($nscript_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_nscript"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div> </div>
<div class="field-pair result-box"> <div class="field-pair result-box">
<div class="alert"></div> <div class="alert"></div>
</div> </div>
</fieldset> </fieldset>
</div><!-- /col1 --> </div>
</div><!-- /section --> </div>
</form> </form>
</div><!-- /colmask --> </div><!-- /colmask -->
@@ -827,17 +396,26 @@
\$(document).ready(function(){ \$(document).ready(function(){
// Autocomplete and filebrowser // Autocomplete and filebrowser
\$('#email_dir').typeahead().fileBrowser(); \$('#email_dir').typeahead().fileBrowser();
// Expand on enable // Expand on enable
\$('.col2 input[name$="enable"]').change(function() { \$('.col2 input[name$="enable"]').change(function() {
if(this.checked) { if(this.checked) {
\$(this).parents('.section').find('.col1').show() \$(this).parents('.section').find('.col1').show()
\$(this).parents('.col2').find('.col2-cats').show()
} else { } else {
\$(this).parents('.section').find('.col1').hide() \$(this).parents('.section').find('.col1').hide()
\$(this).parents('.col2').find('.col2-cats').hide()
} }
\$('form').submit() \$('form').submit()
}) })
\$('#email_endjob').change(function() {
if(\$(this).val() > 0) {
\$(this).parents('.section').find('.col2-cats').show()
} else {
\$(this).parents('.section').find('.col2-cats').hide()
}
})
/** /**
Testing functions Testing functions
**/ **/
+147 -79
View File
@@ -1,10 +1,10 @@
<!--#set global $pane="RSS"#--> <!--#set global $pane="RSS"#-->
<!--#set global $help_uri="configuration/1.1/rss"#--> <!--#set global $help_uri="configuration/2.2/rss"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask"> <div class="colmask">
<!--#if not $active_feed#--> <!--#if not $active_feed#-->
<div class="section"> <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> <a class="main-helplink" href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
<p>$T('explain-RSS')</p> <p>$T('explain-RSS')</p>
<form action="add_rss_feed" method="post" autocomplete="off" novalidate> <form action="add_rss_feed" method="post" autocomplete="off" novalidate>
@@ -24,7 +24,7 @@
<input type="text" name="feed" class="smaller_input" value="$feed" /> <input type="text" name="feed" class="smaller_input" value="$feed" />
</td> </td>
<td> <td>
<input type="text" name="uri" /> <input type="text" name="uri" placeholder="$T('addMultipleFeeds')" />
</td> </td>
<td class="nowrap"> <td class="nowrap">
<button type="submit" class="btn btn-default Save"><span class="glyphicon glyphicon-plus"></span> $T('button-add')</button> <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#--> <!--#set $odd = not $odd#-->
<tr class="data-row <!--#if $odd then " alt " else " "#-->"> <tr class="data-row <!--#if $odd then " alt " else " "#-->">
<td class="chk"> <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>
<td class="title"> <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'#-->"> <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" data-domain="$rss[$feed_item]['baselink']"></div> $feed_item $feed_item
</a> </a>
</td> </td>
<td class="controls"> <td class="controls">
<button type="button" class="btn btn-default testFeed" rel="$feed_item"><span class="glyphicon glyphicon-sort"></span> $T('button-preFeed')</button> <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 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> <button type="button" class="btn btn-default delFeed" rel="$feed_item"><span class="glyphicon glyphicon-trash"></span></button>
</td> </td>
</tr> </tr>
<tr class="feed-row <!--#if $odd then " alt " else " "#-->"> <!--#for $uri_index, $uri in enumerate($rss[$feed_item]['uri'])#-->
<td></td> <tr class="feed-row <!--#if $odd then " alt " else " "#-->">
<td colspan="2"> <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>
<div>$rss[$feed_item]['uri']</div> <td colspan="2"><div>$uri</div></td>
</td> </tr>
</tr> <!--#end for#-->
<!--#end for#--> <!--#end for#-->
</tbody> </tbody>
</table> </table>
@@ -89,9 +89,9 @@
<div class="field-pair"> <div class="field-pair">
<label class="config narrow" for="rss_rate">$T('opt-rss_rate')</label> <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" /> <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> <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="config narrow">&nbsp;&nbsp;$T('Next scan at:')&nbsp;$rss_next</span>
<span class="desc narrow">$T('explain-rss_rate')</span> <span class="desc narrow">$T('explain-rss_rate')</span>
</div> </div>
</fieldset> </fieldset>
</div> </div>
@@ -105,7 +105,7 @@
<div class="padTable"> <div class="padTable">
<a class="main-helplink" href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a> <a class="main-helplink" href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
<h2 class="nomargin activeRSS"> <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> <a href="$rss[$active_feed]['uri']" onclick="window.open(this.href); return false;">$active_feed</a>
</h2> </h2>
<!--#if $error#--> <!--#if $error#-->
@@ -117,7 +117,7 @@
<form action="upd_rss_feed" method="post"> <form action="upd_rss_feed" method="post">
<input type="hidden" name="session" value="$session" /> <input type="hidden" name="session" value="$session" />
<input type="hidden" name="feed" value="$feed" /> <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"> <table class="catTable">
<thead> <thead>
<tr> <tr>
@@ -140,10 +140,10 @@
<tbody> <tbody>
<tr class="default"> <tr class="default">
<td> <td>
<input type="checkbox" disabled="disabled" class="hidden" /> <input type="checkbox" disabled="disabled" class="hidden" />
</td> </td>
<td> <td>
<input type="text" disabled="disabled" size="1" class="hidden" /> <input type="text" disabled="disabled" size="1" class="hidden" />
</td> </td>
<td> <td>
<select name="filter_type" disabled="disabled" class="hidden"> <select name="filter_type" disabled="disabled" class="hidden">
@@ -154,15 +154,16 @@
<option value=">"> $T('rss-atleast')</option> <option value=">"> $T('rss-atleast')</option>
<option value="<"> $T('rss-atmost')</option> <option value="<"> $T('rss-atmost')</option>
<option value="F"> $T('rss-from')</option> <option value="F"> $T('rss-from')</option>
<option value="F"> $T('rss-from-show')</option>
</select> </select>
</td> </td>
<td> <td>
<input type="text" disabled="disabled" value="" class="hidden" /> <input type="text" disabled="disabled" value="" class="hidden" />
</td> </td>
<!--#if $rss[$feed]['pick_cat']#--> <!--#if $rss[$feed]['pick_cat']#-->
<td> <td>
<select name="cat"> <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> <option value="$ct" <!--#if $ct==$rss[$feed]['cat'] then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
<!--#end for#--> <!--#end for#-->
</select> </select>
@@ -190,7 +191,7 @@
<!--#if $rss[$feed]['pick_script']#--> <!--#if $rss[$feed]['pick_script']#-->
<td> <td>
<select name="script"> <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> <option value="$sc" <!--#if $sc==$rss[$feed]['script'] then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end for#--> <!--#end for#-->
</select> </select>
@@ -212,10 +213,10 @@
<tbody> <tbody>
<tr> <tr>
<td> <td>
<input type="checkbox" name="enabled" value="1" checked="checked" /> <input type="checkbox" name="enabled" value="1" checked="checked" />
</td> </td>
<td> <td>
<input type="text" name="new_index" size="1" /> <input type="text" name="new_index" size="1" />
</td> </td>
<td> <td>
<select name="filter_type"> <select name="filter_type">
@@ -225,16 +226,17 @@
<option value="C"> $T('rss-mustcat')</option> <option value="C"> $T('rss-mustcat')</option>
<option value=">"> $T('rss-atleast')</option> <option value=">"> $T('rss-atleast')</option>
<option value="<"> $T('rss-atmost')</option> <option value="<"> $T('rss-atmost')</option>
<option value="S"> $T('rss-from-show')</option>
<option value="F"> $T('rss-from')</option> <option value="F"> $T('rss-from')</option>
</select> </select>
</td> </td>
<td> <td>
<input type="text" name="filter_text" value="" /> <input type="text" name="filter_text" value="" />
</td> </td>
<!--#if $rss[$feed]['pick_cat']#--> <!--#if $rss[$feed]['pick_cat']#-->
<td> <td>
<select name="cat"> <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> <option value="$ct" <!--#if $ct=='Default' then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
<!--#end for#--> <!--#end for#-->
</select> </select>
@@ -262,7 +264,7 @@
<!--#if $rss[$feed]['pick_script']#--> <!--#if $rss[$feed]['pick_script']#-->
<td> <td>
<select name="script"> <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> <option value="$sc" <!--#if $sc=='Default' then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end for#--> <!--#end for#-->
</select> </select>
@@ -285,12 +287,12 @@
<input type="hidden" name="feed" value="$feed" /> <input type="hidden" name="feed" value="$feed" />
<table class="catTable"> <table class="catTable">
<tbody> <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> <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>
<td> <td>
<input type="text" name="new_index" value="$fnum" size="1" /> <input type="text" name="new_index" value="$fnum" size="1" />
</td> </td>
<td> <td>
<select name="filter_type"> <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="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-atleast')</option>
<option value="<" <!--#if $filter[3]=="<" then 'selected="selected"' else ""#-->> $T('rss-atmost')</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> <option value="F" <!--#if $filter[3]=="F" then 'selected="selected"' else ""#-->> $T('rss-from')</option>
</select> </select>
</td> </td>
<td> <td>
<input type="text" name="filter_text" value="$filter[4]" /> <input type="text" name="filter_text" value="$filter[4]" />
</td> </td>
<!--#if $rss[$feed]['pick_cat']#--> <!--#if $rss[$feed]['pick_cat']#-->
<td> <td>
<select name="cat"> <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> <option value="$ct" <!--#if $ct==$filter[0] then 'selected="selected"' else ""#-->>$Tspec($ct)</option>
<!--#end for#--> <!--#end for#-->
</select> </select>
@@ -337,7 +340,7 @@
<!--#if $rss[$feed]['pick_script']#--> <!--#if $rss[$feed]['pick_script']#-->
<td> <td>
<select name="script"> <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> <option value="$sc" <!--#if $sc==$filter[2] then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end for#--> <!--#end for#-->
</select> </select>
@@ -362,7 +365,10 @@
<div class="padding"> <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="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="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> </div>
</form> </form>
</div> </div>
@@ -381,11 +387,12 @@
<table class="catTable"> <table class="catTable">
<thead> <thead>
<tr> <tr>
<th>$T('link-download')</th> <th class="no-sort">$T('link-download')</th>
<th>$T('rss-skip')</th>
<th>$T('rss-filter')</th> <th>$T('rss-filter')</th>
<th>$T('size')</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> </tr>
</thead> </thead>
<!--#for $job in $matched#--> <!--#for $job in $matched#-->
@@ -394,15 +401,16 @@
<form action="download" method="get"> <form action="download" method="get">
<input type="hidden" value="$feed" name="feed" /> <input type="hidden" value="$feed" name="feed" />
<input type="hidden" name="session" value="$session" /> <input type="hidden" name="session" value="$session" />
<input type="hidden" name="url" value="$job[0]" /> <input type="hidden" name="url" value="$job['url']" />
<input type="hidden" name="nzbname" value="$job[4]" /> <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> <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-plus-sign"></span> $T('link-download')</button>
</form> </form>
</td> </td>
<td class="align-center">$job[2]</td> <td>$job['rule'] $job['skip']</td>
<td class="align-center"> <span>$job[3]</span> </td> <td data-sort-value="$job['size']">$job['size_units']</td>
<td>$job[5]</td> <td>$job['title']</td>
<td>$job[1]</td> <td>$job['cat']</td>
<td data-sort-value="$job['age_ms']">$job['age']</td>
</tr> </tr>
<!--#end for#--> <!--#end for#-->
</table> </table>
@@ -415,11 +423,12 @@
<table class="catTable"> <table class="catTable">
<thead> <thead>
<tr> <tr>
<th>$T('link-download')</th> <th class="no-sort">$T('link-download')</th>
<th>$T('rss-skip')</th>
<th>$T('rss-filter')</th> <th>$T('rss-filter')</th>
<th>$T('size')</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> </tr>
</thead> </thead>
<!--#for $job in $unmatched#--> <!--#for $job in $unmatched#-->
@@ -428,15 +437,16 @@
<form action="download" method="get"> <form action="download" method="get">
<input type="hidden" value="$feed" name="feed" /> <input type="hidden" value="$feed" name="feed" />
<input type="hidden" name="session" value="$session" /> <input type="hidden" name="session" value="$session" />
<input type="hidden" name="url" value="$job[0]" /> <input type="hidden" name="url" value="$job['url']" />
<input type="hidden" name="nzbname" value="$job[4]" /> <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> <button type="submit" class="btn btn-default"><span class="glyphicon glyphicon-plus-sign"></span> $T('link-download')</button>
</form> </form>
</td> </td>
<td class="align-center">$job[2]</td> <td>$job['rule'] $job['skip']</td>
<td class="align-center"> <span>$job[3]</span> </td> <td data-sort-value="$job['size']">$job['size_units']</td>
<td>$job[5]</td> <td>$job['title']</td>
<td>$job[1]</td> <td>$job['cat']</td>
<td data-sort-value="$job['age_ms']">$job['age']</td>
</tr> </tr>
<!--#end for#--> <!--#end for#-->
</table> </table>
@@ -452,12 +462,24 @@
<table class="catTable"> <table class="catTable">
<thead> <thead>
<tr> <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> </tr>
</thead> </thead>
<!--#for $job in $downloaded#--> <!--#for $job in $downloaded#-->
<tr class="infoTableSeperator"> <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> </tr>
<!--#end for#--> <!--#end for#-->
</table> </table>
@@ -468,29 +490,28 @@
</div> </div>
<!--#end if#--> <!--#end if#-->
</div> </div>
</div> </div>
<!-- /colmask --> <!-- /colmask -->
<script type="text/javascript" src="${root}staticcfg/js/jquery.tablesort.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
function urlencode(str) { 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(){ \$(document).ready(function(){
\$('.favicon').each(function(i, theContainer) { \$('.favicon').each(function(i, theContainer) {
// Easy favicon grabber // Easy favicon grabber
var favUrl = '//' + \$(theContainer).data('domain') + '/favicon.ico' var favUrl = '//' + \$(theContainer).data('domain') + '/favicon.ico'
\$(theContainer).css({'background': 'url('+favUrl+')', 'background-size': '16px 16px'})
// Does the image exist? Otherwise place a glyphicon // Does the image exist? Otherwise place a glyphicon
var testFavImg= new Image(); var testFavImg = new Image();
testFavImg.src = favUrl; testFavImg.src = favUrl;
testFavImg.onerror = function (evt){ testFavImg.onerror = function (evt){
\$(theContainer).append('<span class="glyphicon glyphicon-list"></span>') \$(theContainer).append('<span class="glyphicon glyphicon-list"></span>')
} }
}) })
\$('.tabs a').click(function (e) { \$('.tabs a').click(function (e) {
e.preventDefault() e.preventDefault()
\$(this).tab('show') \$(this).tab('show')
@@ -498,7 +519,7 @@ function urlencode(str) {
\$('.editFeed').click(function(){ \$('.editFeed').click(function(){
var oldURI = \$(this).prev().val(); 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) { if(newURI != "" && newURI !== null) {
var whichFeed = \$(this).attr("rel"); var whichFeed = \$(this).attr("rel");
var isEnabled = \$('.toggleFeedCheckbox[rel="'+whichFeed+'"]').attr('checked') == "checked"? 1 : 0; var isEnabled = \$('.toggleFeedCheckbox[rel="'+whichFeed+'"]').attr('checked') == "checked"? 1 : 0;
@@ -525,27 +546,12 @@ function urlencode(str) {
}).done(function( msg ) { }).done(function( msg ) {
// Let us leave! // Let us leave!
formWasSubmitted = true; formWasSubmitted = true;
formHasChanged = false; formHasChanged = false;
location.reload(); location.reload();
}); });
} }
}); });
\$('.testFeed').click(function(){
var whichFeed = \$(this).attr("rel");
\$(this).attr('disabled', true)
\$.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(){ \$('.toggleFeedCheckbox').click(function(){
var whichFeed = \$(this).attr("rel"); var whichFeed = \$(this).attr("rel");
\$.ajax({ \$.ajax({
@@ -555,20 +561,82 @@ function urlencode(str) {
}).done(function() { }).done(function() {
// Let us leave! // Let us leave!
formWasSubmitted = true; formWasSubmitted = true;
formHasChanged = false; formHasChanged = false;
location.reload(); 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(){ \$('.cleanFeed').click(function(){
setActiveIcon(this)
var theForm = \$(this).closest("form"); var theForm = \$(this).closest("form");
theForm.attr("action", "clean_rss_jobs").submit(); 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(){ \$('.delFilter').click(function(){
var theForm = \$(this).closest("form"); var theForm = \$(this).closest("form");
theForm.attr("action", "del_rss_filter").submit(); 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> </script>
@@ -1,5 +1,5 @@
<!--#set global $pane="Scheduling"#--> <!--#set global $pane="Scheduling"#-->
<!--#set global $help_uri="configuration/1.1/scheduling"#--> <!--#set global $help_uri="configuration/2.2/scheduling"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<% <%
@@ -59,6 +59,13 @@ else:
<option value="$server" data-action="0" data-noarg="1">$T('sch-disable_server') "$actions_servers[$server]"</option> <option value="$server" data-action="0" data-noarg="1">$T('sch-disable_server') "$actions_servers[$server]"</option>
<!--#end for#--> <!--#end for#-->
</optgroup> </optgroup>
<optgroup label="$T('cmenu-cat')">
<!--#for $cat in $categories#-->
<!--#set $cat_text = $T('Default') if $cat == '*' else $cat#-->
<option value="pause_cat" data-action="$cat" data-noarg="1">$T('sch-pause_cat') "$cat_text"</option>
<option value="resume_cat" data-action="$cat" data-noarg="1">$T('sch-resume_cat') "$cat_text"</option>
<!--#end for#-->
</optgroup>
</select> </select>
</div> </div>
<div class="field-pair" id="hidden_arguments" style="display: none"> <div class="field-pair" id="hidden_arguments" style="display: none">
@@ -72,7 +79,7 @@ else:
</div><!-- /col1 --> </div><!-- /col1 -->
</form> </form>
</div><!-- /section --> </div><!-- /section -->
<div class="section"> <div class="section">
<div class="col2"> <div class="col2">
<h3>$T('currentSchedules')</h3> <h3>$T('currentSchedules')</h3>
@@ -80,21 +87,20 @@ else:
<div class="col1"> <div class="col1">
<fieldset> <fieldset>
<!--#if $schedlines#--> <!--#if $schedlines#-->
<!--#set $schednum = 0#-->
<!--#set $odd = True#--> <!--#set $odd = True#-->
<!--#for $line in $schedlines#--> <!--#for $schednum, $line in enumerate($schedlines)#-->
<!--#set $odd = not $odd#--> <!--#set $odd = not $odd#-->
<form action="delSchedule" method="post"> <form action="delSchedule" method="post">
<input type="hidden" name="session" value="$session"/> <input type="hidden" name="session" value="$session"/>
<input type="hidden" name="line" id="line" value="$line"/> <input type="hidden" name="line" id="line" value="$line"/>
<div class="field-pair infoTableSeperator <!--#if $odd then "" else " alt"#-->"> <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> <button class="btn btn-default float-left"><span class="glyphicon glyphicon-trash"></span></button>
<div class="scheduleEntry"> <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> <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>
</div> </div>
</form> </form>
<!--#set $schednum = $schednum+1#-->
<!--#end for#--> <!--#end for#-->
<!--#else#--> <!--#else#-->
<div class="field-pair"> <div class="field-pair">
@@ -109,7 +115,7 @@ else:
\$('#action').on('change', function() { \$('#action').on('change', function() {
// Set the action // Set the action
\$('#arguments').val((\$(this).find('option:selected').data('action'))) \$('#arguments').val((\$(this).find('option:selected').data('action')))
// Is it speedlimit? // Is it speedlimit?
if(\$(this).find('option:selected').val() == 'speedlimit') { if(\$(this).find('option:selected').val() == 'speedlimit') {
\$('#hidden_arguments').show() \$('#hidden_arguments').show()
@@ -126,5 +132,18 @@ else:
\$('#hidden_arguments').show() \$('#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> </script>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#--> <!--#include $webdir + "/_inc_footer_uc.tmpl"#-->
+338 -238
View File
@@ -1,5 +1,5 @@
<!--#set global $pane="Servers"#--> <!--#set global $pane="Servers"#-->
<!--#set global $help_uri="configuration/1.1/servers"#--> <!--#set global $help_uri="configuration/2.2/servers"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask"> <div class="colmask">
@@ -27,7 +27,12 @@
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="port">$T('srv-port')</label> <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="ssl">$T('srv-ssl')</label>
<input type="checkbox" name="ssl" id="ssl" value="1" />
<span class="desc">$T('explain-ssl')</span>
</div> </div>
<!-- Tricks to avoid browser auto-fill, fixed on-submit with javascript --> <!-- Tricks to avoid browser auto-fill, fixed on-submit with javascript -->
<div class="field-pair"> <div class="field-pair">
@@ -40,80 +45,50 @@
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="connections">$T('srv-connections')</label> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="priority">$T('srv-priority')</label> <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> <input type="number" name="priority" id="priority" min="0" max="100" /> <i>$T('explain-svrprio')</i>
</div> </div>
<div class="field-pair"> <div class="field-pair advanced-settings">
<label class="config" for="retention">$T('srv-retention')</label> <label class="config" for="retention">$T('srv-retention')</label>
<input type="number" name="retention" id="retention" min="0" /> <i>$T('days')</i> <input type="number" name="retention" id="retention" min="0" /> <i>$T('days')</i>
</div> </div>
<div class="field-pair"> <div class="field-pair advanced-settings">
<label class="config" for="timeout">$T('srv-timeout')</label> <label class="config" for="timeout">$T('srv-timeout')</label>
<input type="number" name="timeout" id="timeout" min="30" /> <i>$T('seconds')</i> <input type="number" name="timeout" id="timeout" min="30" /> <i>$T('seconds')</i>
</div> </div>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->"> <div class="field-pair <!--#if int($have_ssl_context) == 0 then "disabled" else ""#--> advanced-settings">
<label class="config" for="ssl">$T('srv-ssl')</label> <label class="config" for="ssl_verify">$T('opt-ssl_verify')</label>
<input type="checkbox" name="ssl" id="ssl" value="1" <!--#if int($have_ssl) == 0 then "disabled=\"disabled\"" else ""#--> /> <select name="ssl_verify" id="ssl_verify" <!--#if int($have_ssl_context) == 0 then "disabled=\"disabled\"" else ""#-->>
<span class="desc">$T('srv-ssl')</span> <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>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->"> <div class="field-pair advanced-settings">
<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">
<label class="config" for="send_group">$T('srv-send_group')</label> <label class="config" for="send_group">$T('srv-send_group')</label>
<input type="checkbox" name="send_group" id="send_group" value="1" /> <input type="checkbox" name="send_group" id="send_group" value="1" />
<span class="desc">$T('srv-explain-send_group')</span> <span class="desc">$T('srv-explain-send_group')</span>
</div> </div>
<div class="field-pair"> <div class="field-pair advanced-settings">
<label class="config" for="optional">$T('srv-optional')</label> <label class="config" for="optional">$T('srv-optional')</label>
<input type="checkbox" name="optional" id="optional" value="1" /> <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>
<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#-->
<option value="$cat" <!--#if $cat == "Default"#-->selected<!--#end if#-->>
<!--#if $cat == "Default" then $T('Default') else $cat#-->
</option>
<!--#end for#-->
</select>
<span class="desc">$T('srv-explain-categories')</span>
</div>
<div class="field-pair">
<label class="config" for="displayname">$T('srv-displayname')</label> <label class="config" for="displayname">$T('srv-displayname')</label>
<input type="text" name="displayname" id="displayname" /> <input type="text" name="displayname" id="displayname" />
</div> </div>
<div class="field-pair"> <div class="field-pair advanced-settings">
<label class="config" for="notes">$T('srv-notes')</label> <label class="config" for="notes">$T('srv-notes')</label>
<textarea name="notes" id="notes" rows="3" cols="50"></textarea> <textarea name="notes" id="notes" rows="3" cols="50"></textarea>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<button class="btn btn-default"><span class="glyphicon glyphicon-plus"></span> $T('button-addServer')</button> <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> <button class="btn btn-default testServer" type="button"><span class="glyphicon glyphicon-sort"></span> $T('button-testServer')</button>
</div> </div>
<div class="field-pair result-box"> <div class="field-pair result-box">
@@ -124,10 +99,63 @@
</div><!-- /section --> </div><!-- /section -->
</form> </form>
<!--#set $cur = 0#--> <script type="text/javascript" src="${root}staticcfg/js/chartist.min.js"></script>
<!--#for $server in $servers#--> <script type="text/javascript">
<!--#set $cur = $cur + 1#--> // Define variables needed for the server-plots
var serverData = {}
var chartOptions = {
fullWidth: true,
showArea: true,
axisX: {
labelOffset: {
x: -5
},
showGrid: false
},
axisY: {
labelOffset: {
y: 7
},
scaleMinSpace: 30
},
chartPadding: {
top: 9,
bottom: 0,
left: 30,
right: 20
}
}
</script>
<!--
We need to find how many months we have recorded so far, so we
loop over all the dates to find the lowest value and then use
this to calculate the date-selector
-->
<!--#import json#-->
<!--#import datetime#-->
<!--#import sabnzbd.misc#-->
<!--#def show_date_selector($server, $id)#-->
<!--#set month_names = [$T('January'), $T('February'), $T('March'), $T('April'), $T('May'), $T('June'), $T('July'), $T('August'), $T('September'), $T('October'), $T('November'), $T('December')] #-->
<!--#set min_date = datetime.date.today()#-->
<!--#for date in $server['amounts'][4]#-->
<!--#set split_date = $date.split('-')#-->
<!--#set min_date = min(min_date, datetime.date(int(split_date[0]), int(split_date[1]), 1))#-->
<!--#end for#-->
<!--#set months_recorded = list(sabnzbd.misc.monthrange(min_date, datetime.date.today()))#-->
<!--#$months_recorded.reverse()#-->
<select class="chart-selector" name="chart-selector-${id}" id="chart-selector-${id}" data-id="${id}">
<!--#for $cur_date in months_recorded#-->
<option value="<!--#echo '%d-%02d' % ($cur_date.year, $cur_date.month)#-->">$month_names[$cur_date.month-1] $cur_date.year</option>
<!--#end for#-->
</select>
<!--#end def#-->
<!--#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> <form action="saveServer" method="post" class="fullform" autocomplete="off" novalidate>
<input type="hidden" name="session" value="$session" /> <input type="hidden" name="session" value="$session" />
<input type="hidden" name="server" value="$server['name']" /> <input type="hidden" name="server" value="$server['name']" />
@@ -135,9 +163,14 @@
<div class="section <!--#if int($server['enable']) == 0 then 'server-disabled' else ""#-->"> <div class="section <!--#if int($server['enable']) == 0 then 'server-disabled' else ""#-->">
<div class="col2 <!--#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> <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> <!--#if int($server['enable']) != 0 #-->
<span class="label label-primary" data-priority="$server['priority']#-->">$T('srv-priority'):</span> <!--#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#--> <!--#end if#-->
<table><tr> <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> <td><input type="checkbox" class="toggleServerCheckbox" id="enable_$cur" name="$server['name']" value="1" <!--#if int($server['enable']) != 0 then 'checked="checked"' else ""#--> /></td>
@@ -158,6 +191,11 @@
<label class="config" for="port$cur">$T('srv-port')</label> <label class="config" for="port$cur">$T('srv-port')</label>
<input type="number" name="port" id="port$cur" value="$server['port']" size="8" /> <input type="number" name="port" id="port$cur" value="$server['port']" size="8" />
</div> </div>
<div class="field-pair">
<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 --> <!-- Tricks to avoid browser auto-fill, fixed on-submit with javascript -->
<div class="field-pair"> <div class="field-pair">
<label class="config" for="${pid}_${cur}0">$T('srv-username')</label> <label class="config" for="${pid}_${cur}0">$T('srv-username')</label>
@@ -175,78 +213,45 @@
<label class="config" for="priority$cur">$T('srv-priority')</label> <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> <input type="number" name="priority" id="priority$cur" value="$server['priority']" min="0" max="100" /> <i>$T('explain-svrprio')</i>
</div> </div>
<div class="field-pair"> <div class="field-pair advanced-settings">
<label class="config" for="retention$cur">$T('srv-retention')</label> <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> <input type="number" name="retention" id="retention$cur" value="$server['retention']" min="0" /> <i>$T('days')</i>
</div> </div>
<div class="field-pair"> <div class="field-pair advanced-settings">
<label class="config" for="timeout$cur">$T('srv-timeout')</label> <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> <input type="number" name="timeout" id="timeout$cur" value="$server['timeout']" min="30" /> <i>$T('seconds')</i>
</div> </div>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="ssl$cur">$T('srv-ssl')</label> <div class="field-pair <!--#if int($have_ssl_context) == 0 then "disabled" else ""#--> advanced-settings">
<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 ""#--> /> <label class="config" for="ssl_verify$cur">$T('opt-ssl_verify')</label>
<span class="desc">$T('srv-ssl')</span> <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>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->"> <div class="field-pair advanced-settings">
<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">
<label class="config" for="optional$cur">$T('srv-optional')</label> <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 ""#--> /> <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>
<div class="field-pair"> <div class="field-pair advanced-settings">
<label class="config" for="send_group$cur">$T('srv-send_group')</label> <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 ""#--> /> <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> <span class="desc">$T('srv-explain-send_group')</span>
</div> </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#-->
<option value="$cat" <!--#if $cat in $server['categories'] then 'selected' else ""#-->>
<!--#if $cat == "Default" then $T('Default') else $cat#-->
</option>
<!--#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">
<label class="config" for="displayname$cur">$T('srv-displayname')</label> <label class="config" for="displayname$cur">$T('srv-displayname')</label>
<input type="text" name="displayname" id="displayname$cur" value="$server['displayname']" /> <input type="text" name="displayname" id="displayname$cur" value="$server['displayname']" />
</div> </div>
<div class="field-pair"> <div class="field-pair advanced-settings">
<label class="config" for="notes$cur">$T('srv-notes')</label> <label class="config" for="notes$cur">$T('srv-notes')</label>
<textarea name="notes" id="notes$cur" rows="3" cols="50">$server['notes']</textarea> <textarea name="notes" id="notes$cur" rows="3" cols="50">$server['notes']</textarea>
</div> </div>
<div class="field-pair"> <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 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 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> <button class="btn btn-default delServer"><span class="glyphicon glyphicon-trash"></span> $T('button-delServer')</button>
</div> </div>
@@ -254,157 +259,252 @@
<div class="alert"></div> <div class="alert"></div>
</div> </div>
</fieldset> </fieldset>
</div><!-- /col1 -->
<div class="col2" style="display:block;">
<!--#if 'amounts' in $server#-->
<b>$T('srv-bandwidth'):</b><br/>
$T('total'): $(server['amounts'][0])B<br/>
$T('today'): $(server['amounts'][3])B<br/>
$T('thisWeek'): $(server['amounts'][2])B<br/>
$T('thisMonth'): $(server['amounts'][1])B
<!--#end if#-->
</div> </div>
</div><!-- /section --> <div class="col1" style="display:block;">
<!--#if 'amounts' in $server#-->
<div class="server-amounts-text">
<b>$T('srv-bandwidth'):</b><br/>
$T('total'): $(server['amounts'][0])B<br/>
$T('today'): $(server['amounts'][3])B<br/>
$T('thisWeek'): $(server['amounts'][2])B<br/>
$T('thisMonth'): $(server['amounts'][1])B
</div>
<div class="server-chart">
$show_date_selector($server, $cur)
<div id="server-chart-${cur}" class="ct-chart"></div>
</div>
<script type="text/javascript">
// Server data
serverData[${cur}] = <!--#echo json.dumps($server['amounts'][4])#-->
\$(document).ready(function() {
showChart(${cur}, \$('#chart-selector-${cur}').val())
})
</script>
<!--#end if#-->
</div>
</div>
</form> </form>
<!--#end for#--> <!--#end for#-->
</div><!-- /colmask --> </div><!-- /colmask -->
<script type="text/javascript"> <script type="text/javascript">
\$(document).ready(function(){ function showChart(server_id, month) {
// Exception when change of priority, reload // This month
\$('input[name="priority"], input[name="displayname"]').on('change', function() { var thisDay = new Date()
\$('.fullform').submit(function() {
// Skip the fancy stuff, just submit
this.submit()
})
})
/**
Color the priority labels
**/
// 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)
})
/** // What month are we doing?
Message on no Default category selected var inputDate = new Date(month+'-01')
**/ var baseDate = new Date(inputDate.getUTCFullYear(), inputDate.getUTCMonth(), 1)
function checkServerCats() { var maxDaysInMonth = new Date(baseDate.getFullYear(), baseDate.getMonth()+1, 0).getDate()
// Now we check all of them
var hasDefault = false; // Fill the data array
// Only check the active servers, not the add-server one var data = {
\$('.section:not(#addServerContent) select[name="categories"]').each(function() { labels: [],
// See if this server is enabled series: [[]]
if(!\$(this).parents('.section').find('.col2').hasClass('server-disabled') ) { };
// Is there Default? var largestVal = 0
if(\$(this).val().indexOf('Default') > -1) { for(var i = 1; i < maxDaysInMonth+1; i++) {
// Hide // Add X-label
\$('.alert-no-category').hide() if(i % 3 == 1) {
hasDefault = true data['labels'].push(i)
// All good! } else {
return true data['labels'].push(NaN)
}
} }
// Get formatted date
baseDate.setDate(i)
var dateCheck = toFormattedDate(baseDate)
// Add data if we have it
if(dateCheck in serverData[server_id]) {
data['series'][0].push(serverData[server_id][dateCheck])
largestVal = Math.max(largestVal, serverData[server_id][dateCheck])
} else if(thisDay.getYear() == baseDate.getYear() && thisDay.getMonth() == baseDate.getMonth() && thisDay.getDate() < i) {
data['series'][0].push(NaN)
} else {
data['series'][0].push(0)
}
}
// Check if we should shrink the Y-axis values
var devideBy = 1024
var axisLabel = 'KB'
if(largestVal > 1024*1024) {
devideBy = 1024*1024
axisLabel = 'MB'
}
if(largestVal > 1024*1024*1024) {
devideBy = 1024*1024*1024
axisLabel = 'GB'
}
if(largestVal > 1024*1024*1024*1024) {
devideBy = 1024*1024*1024*1024
axisLabel = 'TB'
}
// Shrink the value
data['series'][0] = data['series'][0].map(function(num) {
return num / devideBy;
}) })
// We found nothing.. Let's show a warning
if(!hasDefault) \$('.alert-no-category').show() // Show the chart
chart = new Chartist.Line('#server-chart-'+server_id, data, chartOptions);
chart.on('created', function(context) {
// Make sure to add this as the first child so it's at the bottom
context.svg.elem('rect', {
x: context.chartRect.x1,
y: context.chartRect.y2-1,
width: context.chartRect.width(),
height: context.chartRect.height()+2,
fill: 'none',
stroke: '#B9B9B9',
'stroke-width': '1px'
}, '', context.svg, true)
\$('#server-chart-'+server_id+' .ct-label.ct-vertical').each(function(index, elmn) {
elmn.innerHTML += axisLabel
})
});
} }
\$('select[name="categories"]').on('change', checkServerCats) // Need to mitigate timezone effects!
checkServerCats() function toFormattedDate(date) {
var local = new Date(date);
/** local.setMinutes(date.getMinutes() - date.getTimezoneOffset());
Click events return local.toJSON().slice(0, 10);
**/ }
\$('.showserver').click(function () {
if(\$(this).parent().hasClass('server-disabled')) {
\$(this).parent().parent().toggleClass('server-disabled')
}
\$(this).parent().next().toggle();
\$(this).parent().next().next().toggle();
if (\$(this).attr("value") == "$T('showDetails')") {
\$(this).attr("value", "$T('hideDetails')");
} else {
\$(this).attr("value", "$T('showDetails')");
}
});
\$('#addServerButton').click(function(){
\$('#addServer').hide();
\$('#addServerContent').show();
});
\$('.testServer').click(function(event){
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&output=json&name=test_server&" + \$(this).parents('form:first').serialize()
}).then(function(data) {
// Fill the box and enable the button
resultBox.removeClass('alert-success alert-danger').show()
resultBox.text(data.value.message)
theButton.removeAttr("disabled")
theButton.find('span').toggleClass('glyphicon-sort glyphicon-refresh spin-glyphicon')
// Succes or not? /**
if(data.value.result) { When finished loading
resultBox.addClass('alert-success') **/
resultBox.prepend('<span class="glyphicon glyphicon-ok-sign"></span> ') \$(document).ready(function(){
// Exception when change of priority, reload
\$('input[name="priority"], input[name="displayname"]').on('change', function() {
\$('.fullform').submit(function() {
// Skip the fancy stuff, just submit
this.submit()
})
})
/**
Update charts when changed
**/
\$('.chart-selector').on('change', function(elemn) {
showChart(\$(elemn.target).data('id'), \$(elemn.target).val())
// Lets us leave (needs to be called after the change event)
setTimeout(function() {
formWasSubmitted = true;
formHasChanged = false;
}, 100)
})
/**
Click events
**/
\$('.showserver').click(function () {
if(\$(this).parent().hasClass('server-disabled')) {
\$(this).parent().parent().toggleClass('server-disabled')
}
\$(this).parent().next().toggle();
\$(this).parent().next().next().toggle();
if (\$(this).attr("value") == "$T('showDetails')") {
\$(this).attr("value", "$T('hideDetails')");
} else { } else {
resultBox.addClass('alert-danger') \$(this).attr("value", "$T('showDetails')");
resultBox.prepend('<span class="glyphicon glyphicon-exclamation-sign"></span> ')
} }
}); });
});
\$('.delServer').click(function(){ \$('#addServerButton').click(function(){
if( confirm("$T('Plush-confirm')") ) { \$('#addServer').hide();
\$(this).parents('form:first').attr('action','delServer').submit(); \$('#addServerContent').show();
// Let us leave! });
formWasSubmitted = true;
formHasChanged = false; \$('[name="ssl"]').click(function() {
setTimeout(function() { location.reload(); }, 500) // Use CSS transitions to do some highlighting
} var portBox = \$(this).parent().parent().find('[name="port"]')
return false; if(this.checked) {
}); // Enabled SSL change port when not already a custom port
\$('.clrServer').click(function(){ if(portBox.val() == '119') {
if( confirm("$T('Plush-confirm')") ) { portBox.val('563')
\$(this).parents('form:first').attr('action','clrServer').submit(); portBox.addClass('port-highlight')
// Let us leave! }
formWasSubmitted = true; } else {
formHasChanged = false; // Remove SSL port
setTimeout(function() { location.reload(); }, 500) if(portBox.val() == '563') {
} portBox.val('119')
return false; portBox.addClass('port-highlight')
}); }
\$('.toggleServerCheckbox').click(function(){ }
var whichServer = \$(this).attr("name"); setTimeout(function() { portBox.removeClass('port-highlight') }, 2000)
\$.ajax({ })
type: "POST",
url: "toggleServer", \$('.testServer').click(function(event){
data: {server: whichServer, session: "$session" } removeObfuscation()
}).done(function() { var theButton = \$(this)
// Let us leave! var resultBox = theButton.parents('.col1').find('.result-box .alert');
formWasSubmitted = true; theButton.attr("disabled", "disabled")
formHasChanged = false; theButton.find('span').toggleClass('glyphicon-sort glyphicon-refresh spin-glyphicon')
setTimeout(function() { location.reload(); }, 100) \$.ajax({
type: "POST",
url: "../../tapi",
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>')
msg = msg.replace('-', '<br>')
// 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(); }, 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(); }, 500)
}
return false;
});
\$('.toggleServerCheckbox').click(function(){
var whichServer = \$(this).attr("name");
\$.ajax({
type: "POST",
url: "toggleServer",
data: {server: whichServer, session: "$session" }
}).done(function() {
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
setTimeout(function() { location.reload(); }, 100)
});
}); });
}); });
});
</script> </script>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#--> <!--#include $webdir + "/_inc_footer_uc.tmpl"#-->
+19 -19
View File
@@ -1,5 +1,5 @@
<!--#set global $pane="Sorting"#--> <!--#set global $pane="Sorting"#-->
<!--#set global $help_uri="configuration/1.1/sorting"#--> <!--#set global $help_uri="configuration/2.2/sorting"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask"> <div class="colmask">
@@ -9,10 +9,10 @@
<div class="section"> <div class="section">
<div class="col2"> <div class="col2">
<h3>$T('seriesSorting') <a href="$helpuri$help_uri#toc0" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3> <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/> <b>$T('affectedCat')</b><br/>
<select name="tv_cat" multiple="multiple" class="multiple_cats"> <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> <option value="$ct" <!--#if $ct in $tv_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#--> <!--#end for#-->
</select> </select>
@@ -22,11 +22,11 @@
<div class="col1"> <div class="col1">
<fieldset> <fieldset>
<div class="field-pair"> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config wide" for="enable_tv_sorting">$T('opt-tvsort')</label> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="tvfoldername">$T('sortString')</label> <label class="config" for="tvfoldername">$T('sortString')</label>
@@ -39,7 +39,7 @@
<input type="button" class="btn btn-default" 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 - S%0sE%0e - %en.%ext')" value="$T('button-SeasonS01E05')" />
<br/> <br/>
<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/%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')" /> <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> </div>
<div id="previewtv" class="example"> <div id="previewtv" class="example">
@@ -48,7 +48,7 @@
<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>
<div class="field-pair"> <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> </div>
<div class="field-pair"> <div class="field-pair">
@@ -162,10 +162,10 @@
<div class="section"> <div class="section">
<div class="col2"> <div class="col2">
<h3>$T('movieSort') <a href="$helpuri$help_uri#toc6" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3> <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/> <b>$T('affectedCat')</b><br/>
<select name="movie_cat" multiple="multiple" class="multiple_cats"> <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> <option value="$ct" <!--#if $ct in $movie_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#--> <!--#end for#-->
</select> </select>
@@ -175,7 +175,7 @@
<div class="col1"> <div class="col1">
<fieldset> <fieldset>
<div class="field-pair"> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config wide" for="enable_movie_sorting">$T('opt-movieSort')</label> <label class="config wide" for="enable_movie_sorting">$T('opt-movieSort')</label>
@@ -183,7 +183,7 @@
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config wide" for="movie_extra_folder">$T('opt-movieExtra')</label> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="moviefoldername">$T('sortString')</label> <label class="config" for="moviefoldername">$T('sortString')</label>
@@ -198,7 +198,7 @@
<div class="presets float-left"> <div class="presets float-left">
<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)/%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('%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" /> <input type="button" class="btn btn-default" onclick="movieSet('%0decade/%title (%y).%ext',' CD%1');movieExtraFolder(true)" value="$T('decade')" />
</div> </div>
</div> </div>
<div id="previewmovie" class="example"> <div id="previewmovie" class="example">
@@ -303,7 +303,7 @@
<p> <p>
<b>$T('affectedCat')</b><br/> <b>$T('affectedCat')</b><br/>
<select name="date_cat" multiple="multiple" class="multiple_cats"> <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> <option value="$ct" <!--#if $ct in $date_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#--> <!--#end for#-->
</select> </select>
@@ -313,11 +313,11 @@
<div class="col1"> <div class="col1">
<fieldset> <fieldset>
<div class="field-pair"> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config wide" for="enable_date_sorting">$T('opt-dateSort')</label> <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>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="datefoldername">$T('sortString')</label> <label class="config" for="datefoldername">$T('sortString')</label>
@@ -328,7 +328,7 @@
<div class="presets float-left"> <div class="presets float-left">
<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('%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/%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')" /> <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> </div>
<div id="previewdate" class="example"> <div id="previewdate" class="example">
@@ -337,7 +337,7 @@
<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>
<div class="field-pair"> <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> </div>
<div class="field-pair"> <div class="field-pair">
@@ -519,12 +519,12 @@
else else
\$('#previewdate').hide(); \$('#previewdate').hide();
} }
\$(document).ready(function(){ \$(document).ready(function(){
new_previewtv(); new_previewtv();
new_previewmovie(); new_previewmovie();
new_previewdate(); new_previewdate();
\$('#tvfoldername, #tvsamplename').bind("keyup focus", new_previewtv); \$('#tvfoldername, #tvsamplename').bind("keyup focus", new_previewtv);
\$('#moviefoldername, #movieextra, #moviesamplename').bind("keyup focus", new_previewmovie); \$('#moviefoldername, #movieextra, #moviesamplename').bind("keyup focus", new_previewmovie);
\$('#datefoldername, #datesamplename').bind("keyup focus", new_previewdate); \$('#datefoldername, #datesamplename').bind("keyup focus", new_previewdate);
@@ -1,5 +1,5 @@
<!--#set global $pane="Special"#--> <!--#set global $pane="Special"#-->
<!--#set global $help_uri="configuration/1.1/special"#--> <!--#set global $help_uri="configuration/2.2/special"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask"> <div class="colmask">
@@ -17,7 +17,7 @@
<!--#for $option in $switches#--> <!--#for $option in $switches#-->
<div class="field-pair"> <div class="field-pair">
<label class="config wide" for="$option[0]"> <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 ''#--> <!--#if $option[1] != $option[2] then '<span class="glyphicon glyphicon-asterisk"></span>' else ''#-->
</label> </label>
<input type="checkbox" name="$option[0]" id="$option[0]" value="1" <!--#if int($option[1]) > 0 then 'checked="checked"' else ""#--> /> <input type="checkbox" name="$option[0]" id="$option[0]" value="1" <!--#if int($option[1]) > 0 then 'checked="checked"' else ""#--> />
+111 -104
View File
@@ -1,67 +1,36 @@
<!--#set global $pane="Switches"#--> <!--#set global $pane="Switches"#-->
<!--#set global $help_uri="configuration/1.1/switches"#--> <!--#set global $help_uri="configuration/2.2/switches"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#--> <!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask"> <div class="colmask">
<form action="saveSwitches" method="post" name="fullform" class="fullform" autocomplete="off"> <form action="saveSwitches" method="post" name="fullform" class="fullform" autocomplete="off">
<input type="hidden" id="session" name="session" value="$session" /> <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">
<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 ""#--> />
<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>
<button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section"> <div class="section">
<div class="col2"> <div class="col2">
<h3>$T('swtag-server') <a href="$helpuri$help_uri#toc1" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3> <h3>$T('swtag-server') <a href="$helpuri$help_uri#toc1" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 --> </div><!-- /col2 -->
<div class="col1"> <div class="col1">
<fieldset> <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"> <div class="field-pair">
<label class="config" for="max_art_tries">$T('opt-max_art_tries')</label> <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" /> <input type="number" name="max_art_tries" id="max_art_tries" value="$max_art_tries" min="2" max="2000" />
<span class="desc">$T('explain-max_art_tries')</span> <span class="desc">$T('explain-max_art_tries')</span>
</div> </div>
<div class="field-pair">
<label class="config" for="max_art_opt">$T('opt-max_art_opt')</label>
<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"> <div class="field-pair">
<label class="config" for="auto_disconnect">$T('opt-auto_disconnect')</label> <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 ""#--> /> <input type="checkbox" name="auto_disconnect" id="auto_disconnect" value="1" <!--#if int($auto_disconnect) > 0 then 'checked="checked"' else ""#--> />
@@ -83,7 +52,7 @@
<div class="field-pair"> <div class="field-pair">
<label class="config" for="pre_script">$T('opt-pre_script')</label> <label class="config" for="pre_script">$T('opt-pre_script')</label>
<select name="pre_script" id="pre_script"> <select name="pre_script" id="pre_script">
<!--#for $sc in $script_list#--> <!--#for $sc in $scripts#-->
<!--#if $sc.lower() == $pre_script.lower()#--> <!--#if $sc.lower() == $pre_script.lower()#-->
<option value="$sc" selected="selected">$Tspec($sc)</option> <option value="$sc" selected="selected">$Tspec($sc)</option>
<!--#else#--> <!--#else#-->
@@ -117,8 +86,10 @@
<label class="config" for="no_dupes">$T('opt-no_dupes')</label> <label class="config" for="no_dupes">$T('opt-no_dupes')</label>
<select name="no_dupes" id="no_dupes"> <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="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="4" <!--#if int($no_dupes) == 4 then 'selected="selected"' else ""#--> >$T('nodupes-tag')</option>
<option value="2" <!--#if int($no_dupes) == 2 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</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> </select>
<span class="desc">$T('explain-no_dupes')</span> <span class="desc">$T('explain-no_dupes')</span>
</div> </div>
@@ -126,11 +97,18 @@
<label class="config" for="no_series_dupes">$T('opt-no_series_dupes')</label> <label class="config" for="no_series_dupes">$T('opt-no_series_dupes')</label>
<select name="no_series_dupes" id="no_series_dupes"> <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="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="4" <!--#if int($no_series_dupes) == 4 then 'selected="selected"' else ""#--> >$T('nodupes-tag')</option>
<option value="2" <!--#if int($no_series_dupes) == 2 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</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> </select>
<span class="desc">$T('explain-no_series_dupes')</span> <span class="desc">$T('explain-no_series_dupes')</span>
</div> </div>
<div class="field-pair">
<label class="config" for="series_propercheck">$T('opt-series_propercheck')</label>
<input type="checkbox" name="series_propercheck" id="series_propercheck" value="1" <!--#if int($series_propercheck) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-series_propercheck')</span>
</div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="pause_on_pwrar">$T('opt-pause_on_pwrar')</label> <label class="config" for="pause_on_pwrar">$T('opt-pause_on_pwrar')</label>
<select name="pause_on_pwrar" id="pause_on_pwrar"> <select name="pause_on_pwrar" id="pause_on_pwrar">
@@ -159,6 +137,11 @@
<input type="checkbox" name="auto_sort" id="auto_sort" value="1" <!--#if int($auto_sort) > 0 then 'checked="checked"' else ""#--> /> <input type="checkbox" name="auto_sort" id="auto_sort" value="1" <!--#if int($auto_sort) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-auto_sort')</span> <span class="desc">$T('explain-auto_sort')</span>
</div> </div>
<div class="field-pair">
<label class="config" for="direct_unpack">$T('opt-direct_unpack')</label>
<input type="checkbox" name="direct_unpack" id="direct_unpack" value="1" <!--#if int($direct_unpack) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-direct_unpack').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair"> <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 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> <button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
@@ -180,17 +163,7 @@
<div class="field-pair"> <div class="field-pair">
<label class="config" for="enable_all_par">$T('opt-enable_all_par')</label> <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 ""#--> /> <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').replace('.', '<br/>')</span> <span class="desc">$T('explain-enable_all_par').replace('. ', '.<br/>')</span>
</div>
<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 "" #--> />
<span class="desc">$T('explain-par2_multicore')</span>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="par_option">$T('opt-par_option')</label> <label class="config" for="par_option">$T('opt-par_option')</label>
@@ -217,17 +190,6 @@
<input type="checkbox" name="flat_unpack" id="flat_unpack" value="1" <!--#if int($flat_unpack) > 0 then 'checked="checked"' else ""#--> /> <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> <span class="desc">$T('explain-flat_unpack')</span>
</div> </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="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"> <div class="field-pair">
<label class="config" for="script_can_fail">$T('opt-script_can_fail')</label> <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 ""#--> /> <input type="checkbox" name="script_can_fail" id="script_can_fail" value="1" <!--#if int($script_can_fail) > 0 then 'checked="checked"' else ""#--> />
@@ -255,11 +217,28 @@
<input type="checkbox" name="ignore_samples" id="ignore_samples" value="1" <!--#if int($ignore_samples) > 0 then 'checked="checked"' else ""#--> /> <input type="checkbox" name="ignore_samples" id="ignore_samples" value="1" <!--#if int($ignore_samples) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-ignore_samples') $T('igsam-del').</span> <span class="desc">$T('explain-ignore_samples') $T('igsam-del').</span>
</div> </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').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="cleanup_list">$T('opt-cleanup_list')</label> <label class="config" for="cleanup_list">$T('opt-cleanup_list')</label>
<input type="text" name="cleanup_list" id="cleanup_list" value="$cleanup_list"/> <input type="text" name="cleanup_list" id="cleanup_list" value="$cleanup_list"/>
<span class="desc">$T('explain-cleanup_list')</span> <span class="desc">$T('explain-cleanup_list')</span>
</div> </div>
<div class="field-pair">
<label class="config" for="history_retention_select">$T('opt-history_retention')</label>
<input type="hidden" name="history_retention" id="history_retention" value="$history_retention">
<select name="history_retention_select" id="history_retention_select">
<option value="0">$T('history_retention-all')</option>
<option value="n">$T('history_retention-number')</option>
<option value="d">$T('history_retention-days')</option>
<option value="-1">$T('history_retention-none')</option>
</select>
<input type="number" id="history_retention_number" name="history_retention_number" min="1">
<span class="desc">$T('explain-history_retention').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair"> <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 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> <button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
@@ -276,7 +255,7 @@
<div class="field-pair"> <div class="field-pair">
<label class="config" for="folder_rename">$T('opt-folder_rename')</label> <label class="config" for="folder_rename">$T('opt-folder_rename')</label>
<input type="checkbox" name="folder_rename" id="folder_rename" value="1" <!--#if int($folder_rename) > 0 then 'checked="checked"' else ""#--> /> <input type="checkbox" name="folder_rename" id="folder_rename" value="1" <!--#if int($folder_rename) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-folder_rename')</span> <span class="desc">$T('explain-folder_rename').replace('. ', '.<br/>')</span>
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="replace_spaces">$T('opt-replace_spaces')</label> <label class="config" for="replace_spaces">$T('opt-replace_spaces')</label>
@@ -288,11 +267,6 @@
<input type="checkbox" name="replace_dots" id="replace_dots" value="1" <!--#if int($replace_dots) > 0 then 'checked="checked"' else ""#--> /> <input type="checkbox" name="replace_dots" id="replace_dots" value="1" <!--#if int($replace_dots) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-replace_dots')</span> <span class="desc">$T('explain-replace_dots')</span>
</div> </div>
<div class="field-pair">
<label class="config" for="replace_illegal">$T('opt-replace_illegal')</label>
<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#--> <!--#if not $nt#-->
<div class="field-pair"> <div class="field-pair">
<label class="config" for="sanitize_safe">$T('opt-sanitize_safe')</label> <label class="config" for="sanitize_safe">$T('opt-sanitize_safe')</label>
@@ -300,11 +274,6 @@
<span class="desc">$T('explain-sanitize_safe')</span> <span class="desc">$T('explain-sanitize_safe')</span>
</div> </div>
<!--#end if#--> <!--#end if#-->
<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>
<div class="field-pair"> <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 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> <button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
@@ -358,16 +327,7 @@
<div class="field-pair"> <div class="field-pair">
<label class="config" for="rating_enable">$T('opt-rating_enable')</label> <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 ""#--> /> <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_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_host">$T('opt-rating_host')</label>
<input type="text" name="rating_host" id="rating_host" value="$rating_host" />
</div> </div>
<div class="field-pair"> <div class="field-pair">
<label class="config" for="rating_api_key">$T('opt-rating_api_key')</label> <label class="config" for="rating_api_key">$T('opt-rating_api_key')</label>
@@ -385,7 +345,7 @@
<p> <p>
<label for="rating_filter_abort_video">$T('opt-rating_filter_video')</label> <label for="rating_filter_abort_video">$T('opt-rating_filter_video')</label>
<select name="rating_filter_abort_video" id="rating_filter_abort_video"> <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#--> <!--#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> </select>
</p> </p>
@@ -405,7 +365,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 ""#--> /> <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> <label for="rating_filter_abort_encrypted_confirm">$T('opt-rating_filter_confirmed')</label>
</span> </span>
</p> </p>
<p> <p>
<span> <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 ""#--> /> <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 ""#--> />
@@ -415,7 +375,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 ""#--> /> <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> <label for="rating_filter_abort_spam_confirm">$T('opt-rating_filter_confirmed')</label>
</span> </span>
</p> </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 ""#--> /> <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> <label for="rating_filter_abort_downvoted">$T('opt-rating_filter_downvoted')</label>
@@ -453,7 +413,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 ""#--> /> <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> <label for="rating_filter_pause_encrypted_confirm">$T('opt-rating_filter_confirmed')</label>
</span> </span>
</p> </p>
<p> <p>
<span> <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 ""#--> /> <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 ""#--> />
@@ -463,7 +423,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 ""#--> /> <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> <label for="rating_filter_pause_spam_confirm">$T('opt-rating_filter_confirmed')</label>
</span> </span>
</p> </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 ""#--> /> <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> <label for="rating_filter_pause_downvoted">$T('opt-rating_filter_downvoted')</label>
@@ -487,20 +447,67 @@
<script type="text/javascript"> <script type="text/javascript">
\$(document).ready(function() { \$(document).ready(function() {
if (!\$('#rating_filter_enable').is(":checked")) { if (!\$('#rating_filter_enable').is(":checked")) {
\$("#rating_filter_abort").hide(); \$("#rating_filter_abort").hide();
\$("#rating_filter_pause").hide(); \$("#rating_filter_pause").hide();
} }
\$('#rating_filter_enable').change(function () { \$('#rating_filter_enable').change(function () {
if (\$(this).is(":checked")) { if (\$(this).is(":checked")) {
\$("#rating_filter_abort").show(); \$("#rating_filter_abort").show();
\$("#rating_filter_pause").show(); \$("#rating_filter_pause").show();
} }
else { else {
\$("#rating_filter_abort").hide(); \$("#rating_filter_abort").hide();
\$("#rating_filter_pause").hide(); \$("#rating_filter_pause").hide();
} }
}); });
\$('#history_retention_select, #history_retention_number').on('change', updateHistoryRetention)
function updateHistoryRetention() {
var retention_setting = \$('#history_retention')
var retention_select = \$('#history_retention_select').val()
var retention_number = \$('#history_retention_number')
// Keep all or keep none
if(retention_select == "0" || retention_select == "-1") {
retention_number.hide()
retention_number.val('')
retention_number.attr('placeholder', '')
retention_setting.val(retention_select)
} else {
retention_number.show()
// Days or number?
if(retention_select.indexOf("d") !== -1) {
retention_number.attr('placeholder', '$T('days').capitalize()')
if(retention_number.val()) {
retention_setting.val(retention_number.val() + 'd')
} else if(parseInt(retention_setting.val()) > 0) {
retention_number.val(parseInt(retention_setting.val()))
}
} else {
retention_number.attr('placeholder', '$T('history_retention-limit')')
if(retention_number.val()) {
retention_setting.val(retention_number.val())
} else if(parseInt(retention_setting.val()) > 0) {
retention_number.val(parseInt(retention_setting.val()))
}
}
}
}
// Set the history-retention settig
var retention_setting_value = \$('#history_retention').val()
if(parseInt(retention_setting_value) > 0) {
// Days or number?
if(retention_setting_value.indexOf("d") !== -1) {
\$('#history_retention_select').val("d")
} else {
\$('#history_retention_select').val("n")
}
\$('#history_retention_number').val(parseInt(retention_setting_value))
} else {
// Keep all or keep none
\$('#history_retention_select').val(retention_setting_value)
\$('#history_retention_number').hide()
}
\$('.restoreDefaults').click(function(e) { \$('.restoreDefaults').click(function(e) {
// Get section name // Get section name
var sectionName = \$(this).parents('.section').find('.col2 h3').text().trim() var sectionName = \$(this).parents('.section').find('.col2 h3').text().trim()
+12 -12
View File
@@ -1,26 +1,26 @@
<html lang="$active_lang"> <html lang="$active_lang">
<head> <head>
<title>SABnzbd</title> <title>SABnzbd - $T('login')</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1" />
<meta name="apple-mobile-web-app-title" content="SABnzbd" /> <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="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="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="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="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="apple-touch-icon" sizes="192x192" href="../staticcfg/ico/android-192x192.png" />
<link rel="shortcut icon" href="../staticcfg/ico/favicon.ico?v=1.1.0" /> <link rel="shortcut icon" href="../staticcfg/ico/favicon.ico?v=$version" />
<link rel="stylesheet" type="text/css" href="../staticcfg/bootstrap/css/bootstrap.min.css" /> <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?p=$pid" /> <link rel="stylesheet" type="text/css" href="../staticcfg/css/login.css?v=$version" />
<script type="text/javascript" src="../staticcfg/js/jquery-1.11.2.min.js"></script> <script type="text/javascript" src="../staticcfg/js/jquery-3.2.1.min.js?v=$version"></script>
<script type="text/javascript" src="../staticcfg/bootstrap/js/bootstrap.min.js"></script> <script type="text/javascript" src="../staticcfg/bootstrap/js/bootstrap.min.js?v=$version"></script>
</head> </head>
<html> <html>
<body> <body>
<div class="account-wall"> <div class="account-wall">
<div class="text-center logo-header"> <div class="text-center logo-header">
<!--#include $webdir + "/staticcfg/images/logo-full.svg"#--> <!--#include $webdir + "/staticcfg/images/logo-full.svg"#-->
@@ -32,10 +32,10 @@
<!--#if $error#--> <!--#if $error#-->
<div class="alert alert-danger" role="alert">$error</div> <div class="alert alert-danger" role="alert">$error</div>
<!--#end if#--> <!--#end if#-->
<input type="text" class="form-control" name="username" placeholder="$T('srv-username')" required autofocus> <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> <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> <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')"> <div class="checkbox text-center" data-toggle="tooltip" data-placement="bottom" title="$T('explain-sessionExpire')">
File diff suppressed because one or more lines are too long.
File diff suppressed because one or more lines are too long.
@@ -13,62 +13,62 @@ body {
} }
.logo-header { .logo-header {
padding-bottom: 10px; padding-bottom: 10px;
} }
.logo-header svg { .logo-header svg {
height: 55px; height: 55px;
width: 198px; width: 198px;
} }
.account-wall { .account-wall {
width: 300px; width: 300px;
position: absolute; position: absolute;
top: 30%; top: 30%;
left: 50%; left: 50%;
transform: translate(-50%, -30%); transform: translate(-50%, -30%);
} }
.text-center a { .text-center a {
position: absolute; position: absolute;
right: 13px; right: 13px;
top: 12px; top: 12px;
color: #636363; color: #636363;
font-size: 1.2em font-size: 1.2em
} }
.text-center a:hover { .text-center a:hover {
color: black !important; color: black !important;
} }
.form-signin .alert { .form-signin .alert {
margin: 5px 0px; margin: 5px 0px;
} }
.form-signin input { .form-signin input {
margin-top: 5px margin-top: 5px
} }
.form-signin .btn { .form-signin .btn {
width: 100%; width: 100%;
margin-top: 10px; margin-top: 10px;
font-weight: bold; font-weight: bold;
} }
.btn .glyphicon { .btn .glyphicon {
margin-left: 3px; margin-left: 3px;
float: right; float: right;
top: 1px; top: 1px;
font-size: 1.2em; font-size: 1.2em;
} }
input[type="checkbox"] { input[type="checkbox"] {
width: 16px; width: 16px;
height: 16px; height: 16px;
margin-top: 2px; margin-top: 2px;
} }
label { label {
color: #636363; color: #636363;
} }
@@ -106,7 +106,7 @@ label.config,
} }
label.config-hover { label.config-hover {
cursor: pointer; cursor: pointer;
} }
label.narrow { label.narrow {
width: 200px!important; width: 200px!important;
@@ -131,13 +131,14 @@ label.wide,
} }
input[type="checkbox"]+.desc { input[type="checkbox"]+.desc {
padding-top: 6px; padding-top: 6px;
} }
.desc-check { .desc-check {
font-size: 12px; font-size: 12px;
font-style: italic; font-style: italic;
padding: 0 1px; padding: 0 1px;
} }
.col2 p { .col2 p,
.col2-cats {
font-size: 12px; font-size: 12px;
color: #666; color: #666;
margin: 1em 0; margin: 1em 0;
@@ -174,8 +175,8 @@ input[type="checkbox"]+.desc {
padding-left: 12px; padding-left: 12px;
font-size: 13px; font-size: 13px;
} }
.typeahead.dropdown-menu>.active>a, .typeahead.dropdown-menu>.active>a,
.typeahead.dropdown-menu>.active>a:focus, .typeahead.dropdown-menu>.active>a:focus,
.typeahead.dropdown-menu>.active>a:hover { .typeahead.dropdown-menu>.active>a:hover {
color: black; color: black;
background-color: #E5E5E5; background-color: #E5E5E5;
@@ -272,6 +273,9 @@ textarea:hover, input[type="date"]:hover, input[type="datetime"]:hover, input[ty
overflow: auto; overflow: auto;
clear: both; clear: both;
} }
.label {
font-style: normal;
}
.padTable h3 { .padTable h3 {
margin-top: 0; margin-top: 0;
} }
@@ -284,6 +288,27 @@ tr.separator {
.catTable th { .catTable th {
text-align: center; 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"] { .RSS .rss-section input[type="text"] {
max-width: 180px; max-width: 180px;
@@ -300,6 +325,37 @@ tr.separator {
.RSS form[action="add_rss_feed"] tr:nth-child(even) { .RSS form[action="add_rss_feed"] tr:nth-child(even) {
border: 1px solid #E5E5E5; 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 { .Config .table {
margin: 0; margin: 0;
border: 1px solid #ddd border: 1px solid #ddd
@@ -496,6 +552,7 @@ h2.activeRSS {
} }
.favicon { .favicon {
background-position: center center!important; background-position: center center!important;
background-size: 16px 16px;
opacity: 1; opacity: 1;
top: -1px; top: -1px;
height: 16px; height: 16px;
@@ -504,6 +561,9 @@ h2.activeRSS {
margin: 0 6px 0 2px; margin: 0 6px 0 2px;
text-align: center; text-align: center;
} }
.source-icon span {
top: -3px;
}
.feed { .feed {
text-decoration: none; text-decoration: none;
} }
@@ -533,6 +593,9 @@ h2.activeRSS {
padding-right: .3em; padding-right: .3em;
width: auto; width: auto;
} }
#subscriptions .favicon {
margin-left: 8px;
}
.ie6 .subscription-title { .ie6 .subscription-title {
width: 20em; width: 20em;
} }
@@ -556,7 +619,6 @@ h2.activeRSS {
padding: 0 0 .5em; padding: 0 0 .5em;
} }
.feed-row div { .feed-row div {
padding-right: 10px;
overflow:hidden; overflow:hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -564,12 +626,18 @@ h2.activeRSS {
.tab-content .catTable { .tab-content .catTable {
width: 100%; width: 100%;
} }
.tab-content .catTable th {
text-align: left;
}
.tab-content .catTable tbody { .tab-content .catTable tbody {
border: 1px solid #ddd; border: 1px solid #ddd;
} }
.tab-content .catTable td { .tab-content .catTable td {
line-height: 2em; line-height: 2em;
} }
.tab-content .catTable td:nth-child(4) {
word-break: break-all;
}
.tab-content .catTable tr:hover td { .tab-content .catTable tr:hover td {
background-color: #F2F2F2; background-color: #F2F2F2;
} }
@@ -681,7 +749,7 @@ ul.tabs li.active a {
transition: none; transition: none;
} }
input[type=checkbox], input[type=checkbox],
input[type=radio] { input[type=radio] {
margin-top: 2px; margin-top: 2px;
} }
@@ -725,10 +793,10 @@ select {
box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
display:inline-block; display:inline-block;
vertical-align:middle; vertical-align:middle;
max-width: 100%; max-width: 100%;
min-height: 34px; min-height: 34px;
font-size: 13px; font-size: 13px;
background-color: white; background-color: white;
} }
input[type="text"]:not([size]), input[type="text"]:not([size]),
@@ -793,15 +861,21 @@ input[type="checkbox"] {
max-width: 150px; max-width: 150px;
} }
.Categories input { .Categories #content input {
max-width: 150px !important; max-width: 150px !important;
} }
.Scheduling input[type="checkbox"] { .Scheduling form[action="addSchedule"] input[type="checkbox"] {
margin-top: 0px; margin-top: 0px;
margin-left: -20px; margin-left: -20px;
} }
.Scheduling form[action="delSchedule"] input[type="checkbox"] {
position: initial;
float: left;
margin: 9px 10px 0px 5px;
}
.navbar .container { .navbar .container {
padding-right: 0; padding-right: 0;
} }
@@ -829,7 +903,26 @@ input[type="checkbox"] {
margin-bottom: -1px; margin-bottom: -1px;
} }
#search-menu .glyphicon {
top: 3px;
}
#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 { .navbar-logo-small svg {
height: 40px; height: 40px;
@@ -876,8 +969,8 @@ input[type="checkbox"] {
width: 1em; width: 1em;
height: 1em; height: 1em;
} }
.rss-symbol { .rss-symbol {
stroke: none; stroke: none;
fill: white; fill: white;
} }
@@ -900,6 +993,63 @@ input[type="checkbox"] {
opacity: 0.7; opacity: 0.7;
} }
.Servers .server-amounts-text {
width: 20%;
float: left;
}
.Servers .server-chart {
float: right;
width: calc(100% - 250px - 25%);
text-align: center;
position: relative;
}
.Servers .ct-series-a .ct-line {
stroke: #666666;
}
.Servers .ct-series-a .ct-point {
stroke: #666666;
stroke-width: 4px;
}
.Servers .ct-series-a .ct-area {
fill: #666666
}
.Servers .ct-label {
font-size: 1em;
color: black;
}
.Servers .chart-selector {
position: absolute;
display: block;
top: -7px;
left: 50%;
width: 120px;
margin-left: -40px;
min-width: initial;
opacity: 0.8;
}
.Servers .chart-selector:hover {
opacity: 1;
}
.Servers .ct-grid.ct-vertical:first-of-type {
display: none;
}
.advanced-settings {
display: none;
}
.Servers .port-highlight {
animation: highlight-fade 1s ease-in 1;
}
.Servers .col2 label, .Servers .col2 label,
.Email .col2 label { .Email .col2 label {
margin: 0; margin: 0;
@@ -917,7 +1067,6 @@ input[type="checkbox"] {
display: none; display: none;
} }
.alert-no-category,
.alert-translate { .alert-translate {
display: none; display: none;
margin: 5px 0px 0px; margin: 5px 0px 0px;
@@ -969,6 +1118,12 @@ input[type="checkbox"] {
.main-restarting small { .main-restarting small {
font-size: 3rem !important; font-size: 3rem !important;
} }
.value-and-select input {
margin-right: -5px;
}
.value-and-select select {
min-width: 30px;
}
.dotOne, .dotTwo, .dotThree { .dotOne, .dotTwo, .dotThree {
opacity: 0; opacity: 0;
@@ -990,37 +1145,51 @@ input[type="checkbox"] {
100% { opacity: 1; } 100% { opacity: 1; }
} }
@keyframes highlight-fade {
0% { background: #FEFEDA; }
100% { background: none; }
}
.spin-glyphicon { .spin-glyphicon {
animation: spin 2s infinite linear; animation: spin 2s infinite linear;
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); } 100% { transform: rotate(359deg); }
} }
@media screen and (min-width: 1200px) { @media screen and (min-width: 1200px) {
.Categories input[name="dir"] { .Categories input[name="dir"] {
max-width: 240px !important; max-width: 240px !important;
} }
.navbar-nav {
/* For extra wide languages like Polish */
margin-right: -150px;
}
} }
@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>span,
.navbar-default .navbar-nav>li>a:hover>strong, .navbar-default .navbar-nav>li>a:hover>strong,
.navbar-default .navbar-nav>li>a.active>strong { .navbar-default .navbar-nav>li>a.active>strong {
display: inline; display: inline;
} }
.navbar-default .navbar-nav>li>a>svg { .navbar-default .navbar-nav>li>a>svg {
display: inline-block; display: inline-block;
} }
.navbar-default .navbar-nav>li>a>strong, .navbar-default .navbar-nav>li>a>strong,
.navbar-default .navbar-nav:hover>li>a.active>strong { .navbar-default .navbar-nav:hover>li>a.active>strong {
display: none; display: none;
} }
#search-menu>a>strong {
display: none;
}
.RSS #content, .RSS #content,
.Categories #content { .Categories #content {
width: 100%; width: 100%;
@@ -1032,23 +1201,32 @@ input[type="checkbox"] {
float: right; float: right;
padding: 5px 0px; padding: 5px 0px;
} }
.col1 h5 { .col1 h5 {
margin-left: 20px; margin-left: 20px;
} }
.col2 { .col2 {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
} }
.col2 p { .col2 p {
margin-left: 20px; margin-left: 20px;
} }
.Servers .col2 button:first-of-type { .Servers .col2 button:first-of-type {
margin-bottom: 0; margin-bottom: 0;
} }
.Servers .server-chart {
display: none;
}
.Servers .server-amounts-text {
padding: 0px 15px 10px;
width: inherit;
}
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
@@ -1063,33 +1241,41 @@ input[type="checkbox"] {
} }
.navbar-collapse.in .navbar-nav>li>a>span, .navbar-collapse.in .navbar-nav>li>a>span,
.navbar-collapse.collapsing .navbar-nav>li>a>span { .navbar-collapse.collapsing .navbar-nav>li>a>span {
display: inline-block; display: inline-block;
width: 1.5em; width: 1.5em;
} }
.navbar-collapse.in .navbar-nav>li>a>svg, .navbar-collapse.in .navbar-nav>li>a>svg,
.navbar-collapse.collapsing .navbar-nav>li>a>svg { .navbar-collapse.collapsing .navbar-nav>li>a>svg {
display: inline-block; display: inline-block;
margin-right: 0.5em; margin-right: 0.5em;
} }
.navbar .nav>li>a {
height: auto;
}
#search-menu {
display: none;
}
.section .col2 h3 { .section .col2 h3 {
padding-right: 25px; padding-right: 25px;
} }
.section .col2 h3 a { .section .col2 h3 a {
display: inline-block; display: inline-block;
} }
.main-restarting.in strong { .main-restarting.in strong {
font-size: 2em; font-size: 2em;
font-weight: bold; font-weight: bold;
} }
.main-restarting.in span { .main-restarting.in span {
top: 2px; top: 2px;
} }
.main-restarting.in small { .main-restarting.in small {
font-size: 2rem !important; font-size: 2rem !important;
} }
@@ -1099,55 +1285,55 @@ input[type="checkbox"] {
padding: 0 !important; padding: 0 !important;
padding-top: 55px !important; padding-top: 55px !important;
} }
#content fieldset { #content fieldset {
min-width: 0; min-width: 0;
padding: 0 !important; padding: 0 !important;
} }
.colmask { .colmask {
border: none !important; border: none !important;
} }
.col2 { .col2 {
padding: 10px; padding: 10px;
} }
.col2 h3 { .col2 h3 {
margin: -10px -10px 0px; margin: -10px -10px 0px;
padding-left: 10px; padding-left: 10px;
} }
.col2 p { .col2 p {
margin-left: 0px; margin-left: 0px;
margin-bottom: 0; margin-bottom: 0;
} }
.col1 h5 { .col1 h5 {
margin-left: 0px; margin-left: 0px;
} }
label.config { label.config {
margin-left: 0px; margin-left: 0px;
margin-bottom: 2px; margin-bottom: 2px;
width: 100%; width: 100%;
float: none; float: none;
} }
.desc { .desc {
margin: 0; margin: 0;
margin-left: 3px; margin-left: 3px;
margin-top: 2px; margin-top: 2px;
padding: 0 !important; padding: 0 !important;
} }
.col1 input[type="checkbox"] { .col1 input[type="checkbox"] {
position: inherit; position: inherit;
float: left; float: left;
margin-right: 5px; margin-right: 5px;
margin-top: 0px; margin-top: 0px;
} }
div.field-pair{ div.field-pair{
min-width: 0; min-width: 0;
padding: 5px 10px; padding: 5px 10px;
@@ -1155,8 +1341,8 @@ input[type="checkbox"] {
.fileBrowserField { .fileBrowserField {
max-width: 65% !important; max-width: 65% !important;
} }
ul.tabs a { ul.tabs a {
padding-left: 5px; padding-left: 5px;
padding-right: 5px; padding-right: 5px;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 982 B

File diff suppressed because one or more lines are too long.
File diff suppressed because one or more lines are too long.
File diff suppressed because one or more lines are too long.
Loaded 100 of 345 files, more files were not shown because too many files have changed in this diff. Show more