Compare commits

...

1584 Commits

Author SHA1 Message Date
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
shypike
3d4cdd7230 Merge pull request #693 from Safihre/develop
Safer limits for CPU limitation in Downloader
2016-10-01 16:56:33 +02:00
Safihre
5df3c24313 Fix order-selection box on Plush NZO-details page 2016-09-28 16:28:15 +02:00
Safihre
eb737619d9 RSS icon margin fix 2016-09-28 15:26:52 +02:00
Safihre
b120fb722d Safer limits for CPU limitation in Downloader 2016-09-28 15:02:10 +02:00
shypike
bd199627f9 Merge pull request #683 from Safihre/develop
Generate new HTTPS-certificates and more
2016-09-19 22:03:15 +02:00
Safihre
e27f0c5053 Show example for speedlimit in scheduler 2016-09-18 11:07:28 +02:00
Safihre
792d0ba319 Do not use Google to get RSS favicon 2016-09-17 20:17:12 +02:00
Safihre
e2a6517a89 #684 Update jQueryUi to 1.12.1 2016-09-16 18:52:27 +02:00
Safihre
1764fa29d1 Padding to None in tabbed warning section 2016-09-15 14:34:10 +02:00
Safihre
cdf0b0186a Add button re-generate self-signed certificates in Config 2016-09-15 14:34:10 +02:00
shypike
790bcd7bd6 Prepare text files for 1.2.0 2016-09-14 09:04:08 +02:00
shypike
a10a5a5f63 Fix crash in CherryPy when it reports problems with some IPv6 addresses.
A bug in Python's traceback logging causes a crash when an IPv6 address with an embedded % is reported.
2016-09-14 08:53:28 +02:00
shypike
53051a94ee Accept MIME records that have only LF line endings.
Some tool developers just ignore the rule requiring CRLF.
2016-09-14 08:53:14 +02:00
shypike
8abcf0856e Update translations 2016-09-10 10:37:36 +02:00
shypike
891798ec4d Update README.mkd 2016-09-10 10:34:37 +02:00
shypike
126321e431 Update text files for release 1.1.0 2016-09-09 23:25:31 +02:00
shypike
86348dee11 Update required CherryPy to 6.0.2 and correct INSTALL.txt 2016-09-09 23:09:06 +02:00
shypike
748ca0ea27 Re-use the NZO_ID after the pre-check run.
This lets 3rd party tools follow job progress through the API.
2016-09-06 20:53:33 +02:00
shypike
e63d5be38c Merge pull request #675 from Safihre/develop
Bugfixes after RC4
2016-09-05 21:55:17 +02:00
Safihre
b26a02371c Improve tabbed layout warnings/mobile 2016-09-05 21:37:06 +02:00
Safihre
f76a886622 More conservative decoding article-cache reserving 2016-09-05 17:36:09 +02:00
Safihre
2ee63f6c98 Testing server needs username/password obfuscation removed 2016-09-04 09:47:40 +02:00
Safihre
83164e221f Set can also not exist in partable 2016-09-03 20:43:41 +02:00
shypike
f9131f591f Force fail_hopeless_jobs to "enabled" by renaming it.
It's the best setting nowadays and most people don't change defaults.
2016-09-02 21:46:15 +02:00
shypike
e6b573e5ec Retries URL fetches should retain their Post-processing setting.
Closes #509
2016-09-02 19:42:19 +02:00
shypike
efa0d5590d Fix typos in README.mkd 2016-09-02 19:25:53 +02:00
shypike
e0189f0e6d Merge pull request #670 from Safihre/develop
Show Glitter tips only once
2016-08-30 10:03:40 +02:00
Safihre
4d34742f46 Show Glitter tips only once 2016-08-30 09:07:51 +02:00
shypike
34c24acb20 Update text files for 1.1.0RC4 2016-08-29 21:57:55 +02:00
shypike
7055b0a814 Merge pull request #668 from sanderjo/develop
Happy Eyeballs: Improved logging and comments
2016-08-26 21:06:08 +02:00
Sander
260c6b906c Happy Eyeballs: Improved logging and comments 2016-08-26 07:52:40 +02:00
shypike
adda8c8898 Merge pull request #660 from sanderjo/develop
Improved network stuff: unresponsive DNS and HappyEyeballs-with-memory
2016-08-25 18:14:58 +02:00
shypike
8e612d1945 Merge pull request #663 from Safihre/develop
Prospective par2 was broken
2016-08-25 18:13:51 +02:00
Safihre
1cf2c1a075 Don't count 'Checking' bytes in Queue-count 2016-08-23 20:05:49 +02:00
sanderjo
8db75be5a5 Happy Eyeballs: remember results to avoid redundant lookups 2016-08-23 18:42:14 +02:00
Safihre
732bf0cfa8 Par2 doesn't need to get removed again, done in add_parfile() 2016-08-23 18:17:39 +02:00
Safihre
6e803fa350 Multi-delete would refresh the queue for every deleted item
Wayyyyy too often
2016-08-23 18:17:39 +02:00
Safihre
a515194b1e Par2's were never really gone
This way postproc thinks that there were still par's left to try, putting the job back, causing a stall in the queue
2016-08-23 18:17:39 +02:00
Safihre
2485bf5c74 Don't start par2 with non-existing .par-file
If the first .par2 was corrupt, there can be a whole bunch more
2016-08-23 18:17:39 +02:00
Safihre
181969d4c6 True prevention of autofills 2016-08-23 18:17:39 +02:00
Safihre
4f972eeaf8 Prospective par2 was broken 2016-08-23 18:17:39 +02:00
Safihre
4ffc4d0879 Make bytes-downloaded correct after Retry 2016-08-23 15:55:53 +02:00
Safihre
604472afca Real bytes counter 2016-08-23 15:23:43 +02:00
shypike
96847baf08 Prevent crashes in dirscanner when encountering badly encoded file names.
Closes #644
2016-08-20 00:01:33 +02:00
sanderjo
332d3a9418 Solve delays in case of non-responding DNS 2016-08-19 23:55:28 +02:00
shypike
776a185fc5 Merge pull request #658 from Safihre/develop
Fixes and Rating upgrade
2016-08-19 14:25:26 +02:00
Safihre
0757706ad3 Stop and remove files in assembler when job removed 2016-08-19 13:58:24 +02:00
Safihre
12a622a21f Use NZO.is_gone() for status 2016-08-19 13:12:46 +02:00
Safihre
619f553de1 Don't send rating-apikey if not filled 2016-08-18 23:19:47 +02:00
Safihre
785bf3f2a3 Update Wiki-links to /1.1/ 2016-08-18 12:33:35 +02:00
Safihre
fbee96d62f SSL level will be set by default to v23 using ssl_method 2016-08-17 10:02:32 +02:00
Safihre
2253e7b09f Change Error to Warning for temporary server errors
Keep the Error for failed login, this is bad.
2016-08-17 10:01:56 +02:00
Safihre
eb3801a918 Extend Rating functionality
Fields can now also be submitted as X-RATING headers during the URL-grab. Instead of just supplying a host, it is also possible to send a X-RATING-URL for more flexability
More: https://github.com/nZEDb/nZEDb/issues/2235
2016-08-17 08:34:44 +02:00
Safihre
aaf3bbb631 Fix mistake in rating adding
Would never allow values longer than 1, like a score of 10. And all info is also in nzo_info.
2016-08-16 17:22:50 +02:00
Safihre
1a66372e9b #657 Add days when hours > 23 2016-08-16 11:06:55 +02:00
Safihre
f5a28a2e56 Proper Script-output escaping
urllib.quote() makes everything to go in an URL, that's not what we need. 
Now also there will not be shown a "(more)" button when there is just 1 output line
2016-08-16 09:25:22 +02:00
Safihre
1fd6a8b1ac Remove obsolete part of history 2016-08-16 08:46:47 +02:00
Safihre
e632f8065d Upgrade jQuery (2.2.2->2.2.4) and jQueryUI (1.11->1.12)
No real changes for us, we already didn't support IE lower than 10.
2016-08-15 10:30:28 +02:00
Safihre
2e0e94a164 #656 Move switches to Specials for Unrar/Unzip/7zip/FileJoin/TSJoin 2016-08-15 08:12:16 +02:00
Safihre
7078af7d23 SSL was not set to highest-possible by default 2016-08-13 12:21:48 +02:00
Safihre
0a8a22090c #649 Warnings in seperate tab when Tabbed Layout 2016-08-13 11:20:23 +02:00
shypike
4d193061cc Merge pull request #655 from Safihre/develop
Safhire's fixes.
2016-08-13 10:31:07 +02:00
shypike
14c6eeef69 Prevent issues when decoding unknown 8-bit ASCII names. 2016-08-13 10:13:10 +02:00
Safihre
2e7f2d2272 Downloader optimzation only when #con >= 8 2016-08-12 21:49:57 +02:00
Safihre
803a695c00 Lower CPU usage when maxing out connection 2016-08-12 21:39:52 +02:00
Safihre
cced1b317c Windows tray icon wasn't terminated properly on restart
So if you restart SABnzbd, the icon would stay untill you hover your mouse over it
2016-08-12 21:39:52 +02:00
Safihre
641b6ecf35 Encrypted=2 is wrong status code 2016-08-12 21:39:52 +02:00
Safihre
ba9952ca50 disk2 info would show sometimes due to timing
Sometimes if there's only once a tiny difference between disk1 and disk2, it will always show disk2 because it never gets removed again. This can be caused by slight timing issue when calculating free space for disk1 and disk2. Mostly on very fast connections.
2016-08-12 21:39:52 +02:00
paradix
5aac0999ed Handling archives with multiple NZBs from API add_url call (#502)
* archive file per url support

* archive file handling from url

* append .nzb extension when needed

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

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

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

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

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

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

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

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

Closes #368
2015-10-16 22:39:10 +02:00
shypike
cf1d57e6e0 Override IPv6 test when Special ipv6_test_host is empty.
Assume proper working of IPv6.
Closes #367
2015-10-15 10:20:46 +02:00
shypike
396449664b Update translations 2015-10-14 22:37:47 +02:00
shypike
6fcb656292 Prevent crash when reporting overlong Windows download-folder path. 2015-10-14 21:43:18 +02:00
shypike
71d41bf0df Fix problem with duplicate checking.
Fixes commit 74e5f19252
2015-10-11 17:11:50 +02:00
shypike
b27280c752 Remove noofslots_total from History.
Variable is for queue, not history.
2015-10-10 18:02:09 +02:00
shypike
51db79d970 Update main POT file. 2015-10-10 17:35:22 +02:00
shypike
62c066e3f8 Fix error in handling ZIP files retrieved from an indexer. 2015-10-10 17:12:30 +02:00
shypike
8fcb5cf1c1 Update translations 2015-10-10 16:50:37 +02:00
shypike
ed091dd43e Server priority should ignore blocked servers. 2015-10-10 15:29:48 +02:00
shypike
5c29852033 Replace --no-api-log command line switch with the "special" option "api_logging".
More elegant code and automatically persistent.
2015-10-10 12:10:08 +02:00
Jonathon Saine
65f8fa7b5d Removed addID from api, changed reference in Plush to use addurl instead. Tested and ensured plush still worked fine. 2015-10-10 11:56:30 +02:00
shypike
74e5f19252 Series duplicate action and normal duplicate action were not independent. 2015-10-10 11:54:34 +02:00
Jonathon Saine
811e93e926 Fix dupe check logic, re-order so md5sum is used first (quickier / less prone for false positives). Since the same NZB release can actually be different depending on how the indexer obtained it (provided from user, vs indexed from usenet manually), added some debug logging. 2015-10-10 00:29:58 -05:00
shypike
4ce3c89928 Current post-processing job was shown as an orphan.
Side effect of an unneeded change in commit 6eedd99 "Redesign of retry for failed URL fetches".
2015-10-09 20:38:40 +02:00
Safihre
b6d4d7ab92 When searching, go to page 1 of results 2015-10-08 22:27:32 +02:00
Safihre
43abe40579 Added text to Plush speedlimit 2015-10-08 22:27:30 +02:00
Safihre
5857a60ceb Use localStorage smarter in Knockout 2015-10-08 22:27:29 +02:00
Safihre
c038395d17 Option to disable confirmation on removal
For Queue (queue items & warnings) and History (history-items and
orphaned jobs)
2015-10-08 22:27:27 +02:00
Safihre
655d623040 Adding more date formats 2015-10-08 22:27:25 +02:00
Safihre
1809a5a1fc Less translations 2015-10-08 22:27:23 +02:00
Safihre
43f9350491 Fix the enable button in Server-config 2015-10-08 22:27:21 +02:00
Safihre
08d4cb3204 Fixing tooltips for new Bootstrap 2015-10-08 22:27:20 +02:00
Jonathon Saine
6fb4d7ece3 More pep8 cleanup, (revert) unused variables by import / duplicate imports / misspelling / some minor renames to avoid using built-in names (list/set/id/etc). Will cleanup addID in another pull. 2015-10-08 22:04:20 +02:00
shypike
e840b8251b Queue size not influenced by filtering. Add total number of queue slots.
The "to do" size of the queue is not reduced by any search criteria.
Add template variable noofslots_total to reflect the total number of un-paused items.
2015-10-08 22:01:00 +02:00
shypike
466bbf2694 Corrections in duplicate checking.
Fix incorrect duplicate decision logic.
Remove "fetch" entry after duplicate job has been ignored.
2015-10-05 21:35:50 +02:00
shypike
3293a8b91d Sort servers in Status view too.
And correct an error in the Config->Servers sort.
2015-10-05 20:40:02 +02:00
shypike
6b547b35ae Fix access issue with folder browser.
Folder browsers should use TAPI calls instead of API calls.
Would fail when a username/password is set, while API-key disabled.
Solves #353
2015-10-05 09:40:33 +02:00
Safihre
48520c1015 Making the logo a link to reload the page 2015-10-04 20:42:39 +02:00
Safihre
147f909495 Adding optional column for extra information 2015-10-04 20:42:39 +02:00
Safihre
a97e7c86f1 Adding tabs to the Status and interface options 2015-10-04 20:42:39 +02:00
shypike
cfed5f8978 Fix issues with speed limiter. 2015-10-04 20:37:39 +02:00
shypike
37f8114d3f Accept both percentage and absolute speedlimit in Scheduler.
Also accept KMG notation for absolute limits.
2015-10-03 22:45:37 +02:00
shypike
189dca1e76 Sort servers on priority and then on display name. 2015-10-03 19:40:26 +02:00
Safihre
4f5ba027ba Resolving scrollbar issue 2015-10-03 16:27:11 +02:00
Safihre
460c51896b Updating bootstrap
However, not updating the Glyphicons because the new ones are not as
sharp as the old ones. Especially on Windows.
2015-10-03 16:14:54 +02:00
Safihre
3acfdc3e56 Adding search function to the queue 2015-10-02 14:04:06 +02:00
Safihre
f739d86112 Limit upload file types 2015-10-02 13:44:23 +02:00
Safihre
275f8d7ff8 Bugfix for long names and 10 video/audio ratings 2015-10-01 15:46:05 +02:00
Safihre
23bf17135f Adding server-priority to main server-view 2015-10-01 13:05:08 +02:00
Safihre
282b3c00c5 Removing obsolete template variables
They are really not used anymore in the interface.
2015-10-01 02:07:36 +02:00
shypike
4e5badf832 Add absolute speed limit to output of API-calls "qstatus" and "queue".
Add parameter "speedlimit_abs" to API-call "queue".
Add parameters "speedlimit" and "speedlimit_abs" to API-call "qstatus".
2015-09-30 22:20:06 +02:00
shypike
80326e3898 Remove https/ipv6 work-around for Python 2.5 on Windows.
We no longer support Python 2.5
2015-09-30 21:23:49 +02:00
Jonathon Saine
f75cb44a24 moved regex define before use (save memory when code branch isnt ran), calculate expire time once (save cpu cycles), reserved word (only some) & undefined & unused variables, depreciated has_key, more spelling and docstring cleanup 2015-09-30 21:03:21 +02:00
Safihre
da300e0676 Removing last bits of Mobile template 2015-09-30 16:52:24 +02:00
shypike
dd3ec6ab7a Merge pull request #348 from shypike/happy-eyeballs
Happyeyeballs improvements.
2015-09-30 08:42:25 +02:00
sanderjo
b883875025 Happyeyeballs improvements. 2015-09-29 21:41:53 +02:00
shypike
47c33467ee Merge pull request #346 from thezoggy/develop--pep8_pt3
More pep8 non-agressive fixes.
2015-09-28 21:46:37 +02:00
Jonathon Saine
cc0b15eb46 More pep8 non-agressive fixes. 2015-09-28 12:58:30 -05:00
shypike
31f60a5395 Fix typo in interface.py 2015-09-28 16:44:13 +02:00
shypike
ed3633c8d7 Merge pull request #343 from Safihre/develop
One-time notice of new skin
2015-09-28 16:24:53 +02:00
Safihre
036d7cd8a0 One-time notice of new skin
Only for existing users
2015-09-28 16:09:37 +02:00
shypike
86a7c0a593 Merge pull request #342 from Safihre/develop
Fixes and adding option for custom pausing
2015-09-28 13:37:54 +02:00
Safihre
75b7602e0c Adding an option for custom pause duration 2015-09-28 12:19:43 +02:00
shypike
511a96fbb4 Merge pull request #341 from thezoggy/develop--pep8_pt2
Develop  pep8 pt2
2015-09-28 10:03:13 +02:00
shypike
1d49f8380b Update skin & color table
Remove mobile and add Glitter.
2015-09-28 09:47:21 +02:00
Safihre
d8573e6bd5 Fixing Unblock server button 2015-09-28 09:26:42 +02:00
Safihre
9bf3c68702 Fixing warnings on queue > HD space 2015-09-28 08:42:02 +02:00
Jonathon Saine
40afd2e4d1 More PEP8 non-agressive whitespace cleanup. 2015-09-27 11:05:49 -05:00
Jonathon Saine
c015ac2656 Remove legacy nzbsrus logic, pep8 cleanup (non-agressive) 2015-09-27 10:57:31 -05:00
shypike
28c805ffde Update main POT file. 2015-09-25 21:36:44 +02:00
shypike
3c4b09af75 Make more error and warning strings translatable. 2015-09-25 21:35:47 +02:00
shypike
b6aff4272f Don't send a notification when reloading a pre-checked job. 2015-09-25 21:16:04 +02:00
shypike
8567fd71a2 Merge pull request #337 from thezoggy/develop--pep8
sab 0.8.x -- dev pep8
2015-09-25 20:24:09 +02:00
shypike
449111ccf8 Merge pull request #338 from Safihre/develop
Adding dark mode to Glitter
2015-09-25 20:22:37 +02:00
Safihre
b6681d4b67 Adding dark mode to Glitter 2015-09-24 18:51:07 +02:00
Jonathon Saine
565f5fd762 Some pep8 cleanup (whitespace/docstring only), focus on root dir along with scripts/tools/util. 2015-09-23 08:47:18 -05:00
shypike
bcf5343617 Merge pull request #332 from Safihre/develop
Interfaces fixes part 5
2015-09-20 19:59:20 +02:00
Safihre
026bab8249 Adding clear button to search field 2015-09-19 23:06:01 +02:00
Safihre
745c8b904e CSS improvements 2015-09-19 23:06:00 +02:00
Safihre
200b293135 Showing 'Default' instead of * 2015-09-19 23:05:58 +02:00
shypike
1ba588f530 Text files for 0.8.0Beta1 2015-09-17 19:08:15 +02:00
shypike
bcbe15e8c9 Update translations 2015-09-17 18:54:22 +02:00
shypike
04198f6606 Merge pull request #331 from sanderjo/geiranger-sab-better-test_ipv6
Geiranger commit: better test_ipv6()
2015-09-16 10:13:46 +02:00
sanderjo
26712d85bb Geiranger commit: better test_ipv6() 2015-09-15 19:18:09 +02:00
shypike
687aab7f90 Windows: prevent temp paths from ending with a period
The misc.trim_win_path() function failed to remove trailing spaces and periods.
2015-09-14 11:40:26 +02:00
shypike
c6dece737f Move sample scripts to a separate folder. 2015-09-11 21:16:04 +02:00
shypike
d3fb46433b Merge pull request #325 from Safihre/develop
Interface fixes part 4
2015-09-11 20:32:23 +02:00
Safihre
fa49a26ca6 Fixing load-balancing setting in Config 2015-09-11 18:28:38 +02:00
Safihre
b10344545b Style fixes for Config 2015-09-11 16:17:42 +02:00
shypike
f7e69b3603 Fix typo in commit 7bb3dd39d4 2015-09-11 07:15:01 +02:00
shypike
2f88a2085e Update translations 2015-09-10 21:06:49 +02:00
shypike
6737937f93 Merge pull request #322 from Safihre/develop
Interface fixes part 3
2015-09-10 21:06:00 +02:00
shypike
7bb3dd39d4 Fix first script parameter when running on Windows.
Bug in Python's path library on Windows.
os.path.normpath() does not convert long-path notation "\\?\d:\tv\bla\bla\.." to "\\?\d:\tv\bla".
Instead use os.path.abspath() in tvsort's function move_to_parent_folder().
2015-09-10 21:01:28 +02:00
Safihre
378ed873b7 CSS fixes for Glitter 2015-09-10 16:45:10 +02:00
Safihre
63fd88a04f Adding back text that should not have been removed
Oops, my fault!
2015-09-10 13:44:46 +02:00
Safihre
97343a9318 Auto-scroll to Multi-edit form
In case the queue is so long the MultiEdit is out of view, it will
scroll to it.
2015-09-10 13:44:44 +02:00
Safihre
858e19fffd Fixing amount left in title 2015-09-09 11:49:34 +02:00
Safihre
dd4fe01036 Moving sort button to top of queue 2015-09-09 11:42:57 +02:00
shypike
355ae2d60d Merge pull request #321 from Safihre/develop
Interface fixes part 2
2015-09-08 17:12:31 +02:00
Safihre
7f94f7856b Adding age on queue hover 2015-09-08 14:32:02 +02:00
Safihre
3618182483 Fixing freeze after sorting 2015-09-08 10:27:05 +02:00
Safihre
5d72e2a208 Pagination fix, human-readable queue size left 2015-09-08 10:27:01 +02:00
Safihre
7718af966d Fancy file button in retry dialog of Glitter 2015-09-08 10:26:57 +02:00
Safihre
7f1a007136 Correctly display long server-names in Config 2015-09-08 09:36:28 +02:00
shypike
986eb039a2 Fix IPv6 test.
Scope issue prevented correct result.
Solves #319
2015-09-07 23:35:09 +02:00
shypike
bde88cba7a Merge pull request #317 from Safihre/develop
Interface fixes
2015-09-07 14:29:16 +02:00
shypike
74168869cf Make the host used for testing IPv6 user configurable. 2015-09-07 13:49:41 +02:00
Safihre
302ecbe7fd Adding icon when a Special is different from default 2015-09-07 11:48:12 +02:00
Safihre
bb2bb5374a Show indicator of number of errors on top
This in case of very long queue the errors might be hidden
2015-09-07 10:33:25 +02:00
Safihre
18b5900226 Removing Info sign from history-info
Overlapped too much with the text
2015-09-07 10:33:22 +02:00
Safihre
ec1ea2e51d Update script log modal 2015-09-07 10:33:19 +02:00
Safihre
38ddef5cd9 Hide notifications options that cannot be used anyway 2015-09-07 10:33:15 +02:00
Safihre
70716e47be Adding more tooltips 2015-09-07 10:33:11 +02:00
shypike
891100343d Make error detection in urlgrabber more flexible.
Different platform have different exceptions for incorrect SSL certificates.
2015-09-07 10:03:15 +02:00
shypike
42cad7309a Make https certificate verification the default. 2015-09-07 09:26:01 +02:00
shypike
d19c65ae8f Return correct disk usage even if destination folder doesn't exist yet.
This prevents error messages in the UI.
2015-09-07 09:25:11 +02:00
shypike
ba6376f453 Update translations 2015-09-07 08:50:23 +02:00
shypike
940e1a1d69 Update main POT file. 2015-09-07 08:50:05 +02:00
shypike
9c51acdc0b Merge pull request #309 from Safihre/develop-skinny
Cleaning skin texts
2015-09-07 08:45:31 +02:00
shypike
75da736f64 Merge pull request #314 from Safihre/develop
Update Mime-type to Gzip also fonts
2015-09-06 13:21:24 +02:00
Safihre
c05459d71c Config Server Toggle now has label
For @thezoggy
2015-09-06 00:09:42 +02:00
Safihre
13b97203fc Update Mime-type to Gzip also fonts 2015-09-06 00:05:26 +02:00
Safihre
bb01be4af5 Fix for language not being saved on Back in Wizard 2015-09-05 23:25:25 +02:00
Safihre
6c9b074cd4 Adding translations to wizard page 1 2015-09-05 23:25:24 +02:00
Safihre
6c98332b78 Cleaning skin texts 2015-09-05 23:25:22 +02:00
shypike
adb95c365e Add --no-api-log to prevent a polluted log during testing.
Will not log API calls.
2015-09-05 15:29:31 +02:00
shypike
72547abe06 Merge pull request #311 from sanderjo/develop
A 404's redirect now keeps the FQDN in place
2015-09-05 10:17:15 +02:00
shypike
ed27256d44 Fix logging of call to user script.
Also fix an incorrect comment.
2015-09-05 10:11:15 +02:00
sanderjo
2c461ce2ed sabnzbd.BROWSER_URL still needed 2015-09-04 21:32:57 +02:00
sanderjo
5ed9f6f30c A 404's redirect now keeps the FQDN in place 2015-09-04 19:10:01 +02:00
Safihre
1c5b9c650f Comment update IPv6 2015-09-04 10:26:01 +02:00
shypike
62e80940a0 Merge pull request #308 from Safihre/develop
IPv6 testhost and HappyEyeballs
2015-09-03 22:47:07 +02:00
Safihre
dbac3a463d Making HappyEyeballs default 2015-09-03 17:12:09 +02:00
Safihre
2d671883e8 Changing IPv6 testhost to test-ipv6.sabnzbd.org 2015-09-03 17:11:57 +02:00
shypike
f224b265d4 Merge pull request #307 from Safihre/develop
Improve CPU usage on Firefox
2015-09-03 12:27:36 +02:00
Safihre
02b655ae50 Improve CPU usage on Firefox
Firefox uses very high CPU for anything animated.
Disables animations on progress-bar and make the History-'processing' a
block animation
Can be removed if it's performance gets better in the future..
2015-09-03 12:17:54 +02:00
shypike
1fae29f125 Merge pull request #306 from Safihre/develop
Glitter improvements.
2015-09-03 10:20:31 +02:00
Safihre
aeeb2e9421 Adding host:port text to SMTP in Config 2015-09-03 10:07:03 +02:00
Safihre
7d73799f5b Remove Json.py license 2015-09-03 09:46:09 +02:00
Safihre
c19fb43f80 Better handling of removed orphaned jobs in GUI of Glitter 2015-09-03 09:46:05 +02:00
shypike
d0409ea706 Update main POT file. 2015-09-01 21:32:41 +02:00
shypike
4314787283 Merge pull request #305 from Safihre/develop-plushfix
Plush fix for reporting
2015-09-01 08:09:14 +02:00
shypike
1d1eddabea Merge pull request #300 from Safihre/develop-newage
Removing Mobile template
2015-08-31 22:54:42 +02:00
Safihre
b4bbb27c55 Plush fix for reporting 2015-08-31 13:58:02 +02:00
shypike
ce55e0f4b2 Merge pull request #304 from Safihre/develop-plushfix
Fix for hover problems in Plush
2015-08-31 13:28:08 +02:00
Safihre
c0c2037a52 Fix for hover problems in Plush 2015-08-31 11:16:19 +02:00
shypike
76bdef4573 Merge pull request #303 from Safihre/develop
Adding translations to dashboard in Plush + bugfix
2015-08-30 22:40:27 +02:00
Safihre
7956e9f5d1 Updating Knockout 2015-08-30 11:26:57 +02:00
Safihre
693f79f09c Adding translations to Dashboard in Plush 2015-08-30 11:13:33 +02:00
Safihre
d81c389904 Bugfix for sortable in Glitter 2015-08-30 10:10:32 +02:00
shypike
6cc9e8e661 Fix crash error in logging
Fix problem introduced in commit 5f1c9cc21a
2015-08-28 14:15:56 +02:00
shypike
4d19f756cb Merge pull request #299 from Safihre/develop
Adding animations to Glitter
2015-08-28 08:53:02 +02:00
shypike
4cd99e0446 Update main POT file. 2015-08-27 22:13:34 +02:00
Safihre
13ec1cd2aa Changing dashboard text to 'Local IPv6'
Different thing!
2015-08-27 21:08:28 +02:00
Safihre
c0a8feb324 Remove empty directories of Classic/Mobile template 2015-08-27 20:41:37 +02:00
Safihre
34d4776f30 Removing Mobile template
Glitterrified!
2015-08-27 20:41:32 +02:00
Safihre
b54af908b1 Adding animations to Glitter
A little more fun!
2015-08-27 20:27:52 +02:00
shypike
13cee2dd99 Merge pull request #297 from Safihre/develop
Fix for toggle server in config
2015-08-26 22:03:57 +02:00
Safihre
afbfe809ad Fix for toggle server in config 2015-08-26 08:44:46 +02:00
shypike
a703873652 Improve logging of failed urlgrabber attempts. 2015-08-25 21:47:40 +02:00
shypike
5f1c9cc21a Log unpack passwords that are read from a file. 2015-08-25 21:24:21 +02:00
shypike
2df2bfff3e Merge pull request #296 from sanderjo/happy-eyeballs-gui
GUI for Happy Eyeballs
2015-08-25 20:47:10 +02:00
sanderjo
091756aa31 GUI for Happy Eyeballs 2015-08-25 20:10:51 +02:00
shypike
ac89838369 Improve error messages when using incorrect login date for Usenet server.
Replace cryptic server message with readable text.
2015-08-25 20:04:59 +02:00
shypike
a339ef53d3 Merge pull request #295 from Safihre/develop
Updating KnockoutJS to latest version
2015-08-25 18:42:14 +02:00
Safihre
97fd225eaf Updating KnockoutJS to latest version
Performance improvements for Firefox and Chrome.
2015-08-25 15:23:04 +02:00
shypike
77537d2dc7 Merge pull request #294 from jcfp/patch-1
README.md: fix command for bg process
2015-08-25 13:19:03 +02:00
jcfp
6ecdcedc35 README.md: fix command for bg process
re: https://forums.sabnzbd.org/viewtopic.php?f=11&t=19403
2015-08-25 12:51:47 +02:00
shypike
f3f3842545 Merge pull request #290 from Safihre/develop-unite
Removing skin-specific config pages
2015-08-25 12:03:06 +02:00
shypike
0841ebcdfc Merge pull request #288 from Safihre/develop
Adding info message for orphaned folders and updates.Fixing clear-speedlimit and storage/path in history.
2015-08-25 11:54:05 +02:00
Safihre
504097027f Adding info message for orphaned folders and updates
If more than 3 orphaned folders are found, display a message. Check this
every other day.
2015-08-24 11:45:30 +02:00
Safihre
f07897a34c Fixing clear-speedlimit and storage/path in history
Clear speedlimit icon was only clickable on Chrome. History only needed
path, not storage.
2015-08-22 20:43:58 +02:00
Safihre
f4b8524b76 Removing skin-specific config pages
All should use Uni-config.
2015-08-21 13:41:36 +02:00
shypike
6c2ce3be64 Merge pull request #284 from Safihre/develop
Adding missing options to Glitter
2015-08-18 22:13:30 +02:00
Safihre
91f4f47df4 Adding missing options to Glitter
Options that were in Plush but not yet in Glitter
2015-08-16 22:01:46 +02:00
shypike
48d83974f9 Merge pull request #281 from Safihre/develop
Adding toggle Show All/Show Failed to Glitter History. Setting CherryPy default favicon.
2015-08-16 17:15:24 +02:00
Safihre
ee39ec8139 Setting default favicon for CherryPy to SABnzbd 2015-08-16 10:11:00 +02:00
Safihre
a71bc99bc0 Adding toggle Show All/Show Failed to Glitter History. 2015-08-15 19:07:18 +02:00
shypike
e331a57303 API-call "history" now accepts one or more "category" parameters in order to filter.
api?mode=history&category=TV&category=*
This will select for category "TV" AND "Default"
2015-08-15 15:10:41 +02:00
shypike
b8889756d4 API-call "addurl" now returns a list of nzo_id's, which will be valid for the actual jobs.
The nzo_id of a "future" entry will now be re-used for the actual job.
Rename api.addid to api.addurl because API-call addid is deprecated.
2015-08-15 10:54:20 +02:00
shypike
c823de5d91 Fix selection of wrong category when the default category of an RSS stream has been removed from categories. 2015-08-14 23:42:59 +02:00
shypike
4ec8b47cf1 Improve error handling in url grabber. No retries on unresolvable errors. 2015-08-14 23:42:48 +02:00
shypike
e1c91ab001 Don't return a work_path for future jobs, because there is none. 2015-08-14 23:42:37 +02:00
shypike
6eedd99deb Redesign of retry for failed URL fetches.
Remove the clumsy embedded link in the error message.
Instead use the Retry for normal jobs.
Use the obsolete "report" field in the database to store the type of the job, "future" for failed fetches.
2015-08-14 23:42:26 +02:00
shypike
87949e25b3 Another patch for CherryPy 3.8.0
This error keeps coming back.
https://bitbucket.org/cherrypy/cherrypy/issues/1296/attributeerror-module-object-has-no
2015-08-14 21:06:01 +02:00
shypike
c0dcc4df12 Merge pull request #280 from Safihre/develop
Integrating icon font Woff as Base64 into CSS for Config and Glitter. Adding file-size to Job-details popup in Glitter.
2015-08-14 20:04:57 +02:00
Safihre
d9d27df77d Adding file-size of each file to job-popup in Glitter
Filenames get truncated in the center for longer names.
2015-08-14 15:07:40 +02:00
Safihre
72b1f31f44 Integrating icon font Woff as Base64 into CSS for Config and Glitter
This fixes any problems that Chrome has with fonts on auth/https
connections that don't have valid certificates. Both Firefox and Chrome
use Woff font's, so only for IE it will have to download 20kb extra of
the CSS while for FF/Chrome it will lower the number of requests by 1.
2015-08-14 11:28:20 +02:00
shypike
05d2f9f2f9 Fix URL for version retrieval. 2015-08-13 21:36:06 +02:00
shypike
7c9c0aa52e Improve check_version() and clean code
Remove unused functions.
Move bad_fetch() from misc to url grabber.
Move OrderedDict port from misc to its own file.
Improve check_version().
2015-08-13 21:26:14 +02:00
shypike
edf39f8e7d Bump required CherryPy to 3..8.0
Avoid CP settings that will become obsolete.
2015-08-13 19:49:57 +02:00
shypike
a7c38434d7 Patch CherryPy to avoid Unicode bugs in PyOpenSSL 0.14
On some systems this resulted in a crash.
This was patched before in the previous CherryPy in
commit d1a87c4564
2015-08-12 22:38:25 +02:00
shypike
30be2c447a Add patch file for CherryPy 3.8.0 2015-08-12 22:24:06 +02:00
shypike
da9e39367d Patch CherryPy to support 301 redirection.
Needed to support the broken Bonjour/ZeroConfig protocol that
only allows an HTTP address to set, even for a HTTPS-only server.
2015-08-12 22:24:06 +02:00
shypike
1889ecd6fb Add official CherryPy 3.8.0 release. 2015-08-12 22:24:06 +02:00
shypike
23815b6a8e Allow all CherryPy releases, starting with 3.2.2 2015-08-12 22:24:06 +02:00
shypike
3c5fe11299 Update main POT file. 2015-08-12 22:20:51 +02:00
shypike
3c442966c4 Replace own JSON module by the standard library. 2015-08-12 21:52:47 +02:00
shypike
c589b23708 Merge pull request #275 from Safihre/develop
Opt-out for global options in Glitter. RSS config page fixup. Config restart SAB fixup. Fixes for mobile/tablet.
2015-08-12 14:54:47 +02:00
Safihre
be96b84e7b Improving Glitter and Config for mobile/tablet 2015-08-12 14:30:43 +02:00
shypike
826173cf5f Merge pull request #278 from sanderjo/one-traceback-less
less traceback logging in cases like ipv6-only server on ipv4 connection
2015-08-10 20:50:37 +02:00
Safihre
c601168d07 Config restart update
Config restart now shows a splash screen. On the General-tab choosing
'Save settings' will now ask if you also want to Restart SAB as well.
2015-08-10 12:57:17 +02:00
Safihre
1cc6e323b9 RSS Config page fixup 2015-08-10 12:57:15 +02:00
Safihre
cf8ddb6e7c Making global interface settings optional in Glitter 2015-08-10 12:57:13 +02:00
SanderJ
f940d61c52 less traceback logging in cases like ipv6-only server on ipv4 connection
https://github.com/sabnzbd/sabnzbd/issues/277
2015-08-09 16:52:40 +02:00
shypike
a706820ee7 Merge pull request #270 from Safihre/develop
Saving Glitter refresh-rate and page-limits in general config. Adding a central speedhistory. Adding icons to Config.
2015-08-07 20:38:18 +02:00
shypike
755deb94d9 Update unrar to release 5.21 2015-08-06 20:42:07 +02:00
Safihre
29cbc6e10b Adding central speedhistory
Adding a central speed history within SAB, a feature many download
programs have and I think SAB could use! This way you can actually see
what happened when the browser was not open.
When there is no download-activity nothing really happens, it only calls
and updates (in case 0's need to be added) when the main.tmpl file is
called. In Glitter this is only once.
It does add consecutive numbers to a list, but this should not be a
memory hog.
2015-08-06 20:39:27 +02:00
shypike
ccf7060932 Merge pull request #271 from sanderjo/HappyEyeballs
Implement HappyEyeballs
2015-08-06 19:51:21 +02:00
sanderjo
4571ea414a Implement HappyEyeballs 2015-08-05 20:00:40 +02:00
Safihre
b1bb6d2a10 Adding icons to Config
TODO: RSS-details page
2015-08-05 10:41:19 +02:00
Safihre
20b98e95f5 Saving refresh-rate and page-limits in general config
refresh_rate and history_limit were already in the config but not used.
Added queue_limit.
This way these settings are the same no matter what device or browser
you use to connect to your SAB system.
2015-08-05 10:41:15 +02:00
shypike
bfa06e79c5 Windows: fix Unicode crash when encountering odd names in par2 file sets.
On Windows, file names coming from console output of par2.exe needs to be converted to Unicode.
2015-08-04 23:09:52 +02:00
shypike
f9ab3f193d Merge pull request #267 from Safihre/develop
Added 'Browse' to translation, graphic fixes. Replacing tabs by spaces in Glitter.
2015-08-04 20:45:53 +02:00
Safihre
6f7adb8e29 Replacing tabs by spaces in HTML/CSS of Glitter 2015-08-03 13:52:14 +02:00
Safihre
f0cc967c0c Config - Added 'Browse' to translation, graphic fixes
Both in Glitter (Add-NZB) and in Config added the word 'Browse' so that
is now also in Config multi-languages.
2015-08-02 19:22:17 +02:00
shypike
b48db17f37 Update main POT file. 2015-08-01 21:25:36 +02:00
shypike
76b9971d06 Merge pull request #266 from Safihre/develop
Customized Add-NZB in Glitter, added extra tooltips
2015-08-01 21:17:11 +02:00
Safihre
3564fc3c5c Customized Add-NZB in Glitter, added extra tooltips 2015-08-01 16:06:42 +02:00
shypike
b9bc0cf344 Decrease size of preamble in Config->Special. 2015-08-01 15:20:18 +02:00
shypike
1b4e30bcee Update main POT file. 2015-08-01 15:12:44 +02:00
shypike
c8e11549d1 Some touch-ups of Config. 2015-08-01 15:11:45 +02:00
shypike
993733cc8b Remove tab characters. 2015-08-01 15:01:11 +02:00
shypike
19ff1db3c4 Merge pull request #260 from Safihre/develop-newconfig
Redesigning the Config look.
2015-08-01 14:30:57 +02:00
Safihre
ac7607e952 Redesigning the Config 2015-08-01 14:08:08 +02:00
shypike
10d345f9c4 Enable renaming of Usenet servers.
- Renaming by showing a separate display name
- Backwards compatible: no damage to schedules and server counters
- Add multi-line field for personal server notes
2015-08-01 10:43:33 +02:00
shypike
d56c19ae1d The 301 redirection message should use the IP of the caller.
301 should not use "localhost" but the actual IP of the calling client.
Needed to support Bonjour/Zerconfig.
2015-07-31 20:53:39 +02:00
shypike
d1a631f801 Fix broken Speedlimit in Scheduler. 2015-07-30 21:09:00 +02:00
shypike
bd953c5e9f Update main POT file. 2015-07-29 19:48:14 +02:00
shypike
87dd4f1975 Fix inconsistent casing in scheduler events. 2015-07-28 22:28:01 +02:00
shypike
639114dbb5 Add English pseudo translation PO files.
Enables correction of English texts without invalidating existing translations.
These files are not maintained in the translation service, but are edited directly.
2015-07-28 19:11:53 +02:00
shypike
3a8b01e5c1 Plush main page, Queue menu: move "Retry All Failed" before Sort.
Prevents fold-out of Sort from obscuring "Retry All Failed".
2015-07-28 19:06:39 +02:00
shypike
7642d2a1e4 Fix Dutch translation. 2015-07-28 19:01:58 +02:00
shypike
68b44a7310 Drop support for Python 2.5
Also for SABHelper.py
2015-07-28 08:30:27 +02:00
shypike
17130c5208 Merge pull request #256 from Safihre/develop
Adding changing of loglevel in Glitter
2015-07-27 23:30:32 +02:00
shypike
3b0ed33598 Merge pull request #254 from thezoggy/develop--smpl_white-navigation
smpl - remove trailing | in nvaigation
2015-07-27 23:26:40 +02:00
shypike
0cb68f6f81 Merge pull request #252 from thezoggy/develop--minor
Minor fixes/spelling mistakes.
2015-07-27 23:26:19 +02:00
shypike
b2a44c66f1 Fix typo in Config->Notifications
Incorrect text type file.
2015-07-27 16:13:54 +02:00
Jonathon Saine
f2a332d3f9 Removing trailing | from navigation. 2015-07-26 03:02:34 -05:00
Jonathon Saine
8dd55b8d81 Minor fixes/spelling mistakes. 2015-07-25 21:42:53 -05:00
shypike
fe5da44602 Don't send a test notification when required data is missing. 2015-07-25 19:02:43 +02:00
Safihre
284dc10cc5 Adding changing of loglevel in Glitter 2015-07-25 17:58:16 +02:00
shypike
19d97a3c3a When auto-launching the Wizard, use the "/sabnzbd/wizard" URL.
Better compatible when configured behind another HTTP server.
2015-07-25 14:34:23 +02:00
shypike
500906e802 Update main POT file 2015-07-25 14:09:16 +02:00
shypike
7d162c2111 Add Pushbullet support. 2015-07-25 14:08:14 +02:00
shypike
f12011468d Implement Pushover support.
Users have to register their own Application key.
2015-07-24 18:04:23 +02:00
shypike
16b358bdb5 Merge pull request #248 from Safihre/develop
Updating setup wizard and Glitter upgrades
2015-07-24 11:19:53 +02:00
Safihre
2921f0825d Adding NZB-name option, minor fixes 2015-07-24 09:21:20 +02:00
Safihre
822a6103bb Delete All for Orphaned jobs in Glitter
In status-window
2015-07-23 11:06:15 +02:00
shypike
05dc4e4177 Remove potential XSS vulnerability in the Status-QueueRepair when folder names contain "<", ">" and "&".
Status did not XML-ify names.
2015-07-23 09:13:30 +02:00
shypike
496c0fd928 Remove potential XSS vulnerability in the History when folder names contain "<", ">" and "&".
Unlike build_queue, build_history did not XML-ify names.
Patch required for Plush.js, due to the way the download-report pop-up title is created.
2015-07-23 09:07:54 +02:00
Safihre
0e62433af1 Updating Wizzard
Updating Wizzard to be proper HTML and look like Glitter
2015-07-22 13:05:29 +02:00
Safihre
cc41c4881b Bugfixes to Glitter 2
Adding the right HTML-language attribute and fixing for big screens.
2015-07-22 13:05:26 +02:00
Safihre
26564b55e4 Minor bug fixes in Glitter 2015-07-22 13:05:24 +02:00
Safihre
11d1729a33 Make Glitter the default skin for new users.
Also supports config-less skins by setting the standard "Config"-skin.
2015-07-20 21:13:06 +02:00
Safihre
574f176615 Adding OZnzb to Glitter
Changed OZnzb so it will allow changing of up/down voting.
For out of retention it will only list the user's servers, not all of them.
In Glitter you can only give a audio/video score once (just like on the website).
Plush allows changing of audio/video rating and shows that change, but it is not actually changed on the website.
2015-07-19 22:08:05 +02:00
shypike
bf46ba035c Support the use of an unpatched release of CherryPy.
Use the '"redirect_url" patch only when actually present.
Only needed to support https on zeroconfig/Bonjour systems.
2015-07-19 16:57:34 +02:00
shypike
729b368b56 Remove obsolete references to indexers. 2015-07-19 16:37:15 +02:00
shypike
548f448b9d Add support for processing .nzb.bz2 files.
Support the BZIP2 format.
2015-07-18 13:00:28 +02:00
shypike
915a444a3a Get rid of non-standard indenting.
Tabs, yuk!
2015-07-18 12:37:34 +02:00
oznzb
c7ac896125 Rating functionality
Port rating functionality from 0.7.x to develop.
2015-07-15 19:49:46 +02:00
shypike
f238c8bb15 Prevent crash in zero-config/Bonjour service due toi unexpected errors. 2015-07-14 21:48:15 +02:00
shypike
d90ca686c1 Improve logging of API-calls "addlocalfile" and "addurl". 2015-07-13 21:22:50 +02:00
shypike
a7264c92d1 Fix change that disabled password recognition in the Watched folder.
Fixes side-effect of "Setting a password in Retry dialog did not work"
2015-07-13 20:58:16 +02:00
shypike
0566510b07 Prevent crash when URL grab runs into failing server. 2015-07-08 17:37:16 +02:00
shypike
41869e115c Merge pull request #240 from Safihre/develop
Fixing the favicons in all templates
2015-07-08 17:14:10 +02:00
Safihre
ac26de4578 Fixing the favicons in all templates 2015-07-06 17:56:58 +02:00
shypike
78a9c8512b Setting a password in Retry dialog did not work.
Race-condition prevented password from being added to NZO in time.
Instead set password before adding job to the queue.
2015-07-05 16:23:20 +02:00
shypike
c4618bbafb Fix skintext.py
.lower() construction used by Glitter, is not supported by translation tools.
2015-07-04 14:46:43 +02:00
shypike
3b72d5a8cf Update Dutch translation. 2015-07-04 14:45:44 +02:00
shypike
e75aad12b1 Update translations 2015-07-04 14:29:57 +02:00
shypike
a6eb72abb1 Update Dutch translation. 2015-07-04 14:29:38 +02:00
shypike
d1b49d10f1 Update text files for 0.8.0Alpha3 2015-07-04 14:06:10 +02:00
shypike
9e555cb3dd Merge pull request #236 from Safihre/develop
Updating Glitter for fixed retry in API and new icons
2015-07-04 13:03:47 +02:00
Safihre
cd2ba68f4f Fixing Favicon 16x16 in all templates 2015-07-04 12:54:08 +02:00
shypike
1994e0535e On Windows, mass-purging failed jobs from History left some files behind.
Cause: path check did not take long-path notation into account.
Solves issue #227
2015-07-04 12:39:49 +02:00
Safihre
36f2e2037f Merge remote-tracking branch 'sabnzbd/develop' into develop 2015-07-04 12:08:51 +02:00
shypike
4577390775 Update main POT file. 2015-07-04 12:01:23 +02:00
Safihre
f99e768572 Adding the icons of @MasterRoot24 to Glitter
Also changed browserconfig, should include a . in the path.
2015-07-04 11:52:30 +02:00
shypike
ec866c0daa Always show server statistics for a job, also when only one server was used. 2015-07-04 11:49:03 +02:00
shypike
5b93657e83 Remove left-over "fillserver" fields in original Plush and smpl Config->Server pages. 2015-07-04 11:30:13 +02:00
shypike
acee0a30d7 Merge pull request #228 from cdheiser/develop
Add support for restricting servers by category
2015-07-04 11:23:12 +02:00
Safihre
a8fec539ba Merge remote-tracking branch 'sabnzbd/develop' into develop 2015-07-04 11:04:05 +02:00
Safihre
47a2a44f5a Fixing retry option now it's fixed in API 2015-07-04 11:02:00 +02:00
shypike
4011dba8d5 Merge pull request #233 from sanderjo/CheckForReceptionOf-ip-address
Dashboard: if we get anything else than a plain IPv4 address, return None
2015-07-04 10:06:19 +02:00
shypike
a2c1b47a8f Fix crash when unrar is missing on Linux/Unix systems.
Solves issue #234
2015-07-04 10:02:12 +02:00
shypike
a1040a144d On Windows, failed jobs removed from History left some files behind.
Cause: path check did not take long-path notation into account.
Solves issue #230
2015-07-04 09:52:36 +02:00
cdheiser
7d790a37a0 Issue #225: Add support for restricting servers by category.
- Config support updated to save categories by server.  Any server without categories defaults to the Default category.
- Downloader now checks categories before selecting a server to use.
  - Servers with the Default category are always eligible.
  - Otherwise, only servers with a matching cagegory are eligible.
  - Servers without categories are not used (this can happen if you delete the last category for a server).
  - Properly skip servers with higher priority if they have no matching category
- Upgrades from previous versions should retain the expected behavior that all servers are eligible until configured otherwise.
2015-07-03 15:05:42 -07:00
sanderjo
501fdb9dd7 if we get anything else than a plain IPv4 address, return None 2015-07-03 23:33:15 +02:00
shypike
d689cb234f Fix the table with the pattern key for Series Sorting.
The table mixed up the Season functions for original and case-adjusted titles.
2015-07-03 23:02:36 +02:00
shypike
f6f923e060 Make sure OSX/Unix and Windows read each others INI files.
The resulting INI file is converted to the native format.
2015-07-03 22:06:52 +02:00
shypike
db5cc40b87 Remove double unicode conversion in server test error messages.
CherryPy can handle Unicode without xml_name() conversion.
2015-07-03 21:17:36 +02:00
shypike
f190d27ff4 Merge pull request #175 from MasterRoot24/icons
Update favicon, apple-touch-icon and mstiles
2015-07-03 20:40:35 +02:00
shypike
296042a491 Make the speed boxes and logo center again in Plush top bar. 2015-07-03 20:34:42 +02:00
shypike
bd3cba4dc4 One time conversion of a "fillserver" to priority 1.
Prevents all servers from getting priority 0 when upgrading to 0.8.0
Also fix explanation text.
2015-07-03 20:28:00 +02:00
shypike
823431b705 Merge pull request #226 from Safihre/develop
Adding Glitter
2015-07-02 21:35:44 +02:00
shypike
f4dcd0ef3b No longer recognize "something-s.avi" constructions as samples. 2015-07-02 21:18:47 +02:00
shypike
1f65507b39 On Windows UNC paths were not handled correctly.
Trimming UNC paths went wrong.
2015-07-02 20:59:49 +02:00
shypike
2f4cebf37a On Windows, failed jobs didn't show the "Retry" button.
Cause: path comparison didn't account for long-path notation.
(Thank you, Safihre)
2015-07-02 20:18:58 +02:00
shypike
5d2ea32217 Try to fall back to 7z if 7za is missing
Only for systems other than Windows and OSX.
2015-07-02 15:06:43 +02:00
Safihre
4c1e63f4ce Bugfixes 2015-07-01 22:17:45 +02:00
shypike
34670d0523 Merge pull request #229 from sanderjo/sab-Dashboard
Sab dashboard: Dashboard cleaning up
2015-06-30 20:23:01 +02:00
Safihre
473492cd31 Added queue sorting and more bugfixes 2015-06-28 23:55:23 +02:00
Safihre
8bc13447a7 Feature and bug update
See Forum!
2015-06-25 23:43:06 +02:00
Safihre
04089dd159 Revert "TEST"
This reverts commit 9902d11770.
2015-06-24 21:28:04 +02:00
Safihre
9902d11770 TEST 2015-06-24 21:27:38 +02:00
Safihre
0c3bc8b75d Add multi-lang 2015-06-24 21:18:10 +02:00
sanderjo
248bfbde99 Dashboard cleaning up 2015-06-23 20:23:00 +02:00
Safihre
dfd3573e99 Phone/tablet fixed, retry function, status window 2015-06-14 22:25:04 +02:00
shypike
f0bac2d1b2 Correct typo 2015-06-12 08:36:01 +02:00
Safihre
78312b00f5 Adding licensing 2015-06-11 16:13:11 +02:00
Safihre
0864fd9a6f Adding Glitter 2015-06-11 15:40:27 +02:00
sanderjo
75f2d23ef2 Dashboard enhancements, among which Pystone 2015-05-29 23:59:57 +02:00
Joe Nyland
5481b6663e Update favicon, apple-touch-icon and mstiles 2014-07-31 22:39:31 +01:00
490 changed files with 89010 additions and 63486 deletions

9
.gitignore vendored
View File

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

View File

@@ -1,5 +1,5 @@
*******************************************
*** This is SABnzbd 0.8.0 ***
*** This is SABnzbd 2.2.0 ***
*******************************************
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
@@ -10,33 +10,19 @@ SABnzbd also has a fully customizable user interface,
and offers a complete API for third-party applications to hook into.
There is an extensive Wiki on the use of SABnzbd.
http://wiki.sabnzbd.org/
IMPORTANT INFORMATION about release 0.8.0:
http://wiki.sabnzbd.org/introducing-0-8-0
https://sabnzbd.org/wiki/
Please also read the file "ISSUES.txt"
The organization of the download queue is different from 0.7.x (and older).
1.0.0 will not finish downloading an existing queue.
Also, your sabnzbd.ini file will be upgraded, making it
incompatible with older releases.
*******************************************
*** Upgrading from 0.7.x or 0.6.x ***
*** Upgrading from 0.7.x and below ***
*******************************************
Empty your current queue
Stop SABnzbd.
Install new version
Start SABnzbd.
*******************************************
*** Upgrading from 0.5.x ***
*******************************************
Stop SABnzbd.
Uninstall current version, keeping the data.
Install new version
Start SABnzbd.
The organization of the download queue is different from 0.7.x (and older).
0.8.0 will not finish downloading an existing queue.
Also, your sabnzbd.ini file will be upgraded, making it
incompatible with older releases.

View File

@@ -1,397 +0,0 @@
-------------------------------------------------------------------------------
0.8.0Aplha2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Server priorities instead of primary/backup
- Work-arounds to avoid bugs in PyOpenSSL 0.14
- Speedlimit 0 wasn't handled properly (in means no limit)
- Add 'action_size' parameter to support drag-and-drop of files
- Remove unnecessary 7zip warnings
- Support RAR's REV files to some extent
- Diagnostic dashboard tab for "Status" page
- Fix UI for Notification Center, Growl and NtfOSD
-------------------------------------------------------------------------------
0.8.0Aplha1 by The SABnzbd-Team
-------------------------------------------------------------------------------
See git log and release notes
-------------------------------------------------------------------------------
0.7.11Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Bad articles from some servers were accepted as valid data
- Show warning when the decoder encounters I/O errors
- Generic Sort failed to rename files when an extra folder level was present in the RAR files
- Obfuscated file name support caused regular NZBs to verify slower
-------------------------------------------------------------------------------
0.7.10Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Disable obsolete newzbin bookmark readout
- Show speed when downloading in Forced mode while paused
- Plush History icons repair and unpack were swapped
- Try to repair rar/par sets with obfuscated names
- Reset "today" byte counters at midnight even when idle
- Display next RSS scan moment in Cfg->RSS
- An email about a failed should say that the download failed
- Report errors coming from fully encrypted rar files
- Accept NNTP error 400 without "too many connection" clues as a transient error.
- Accept %fn (next to %fn.%ext) as end parameter in sorting strings.
-------------------------------------------------------------------------------
0.7.9Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix fatal error in decoder when encountering a malformed article
- Fix compatibility with free.xsusenet.com
- Small fix in smpl-black CSS
-------------------------------------------------------------------------------
0.7.8Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix problem with %fn substitution in Sorting
- Add special "wait_for_dfolder", enables waiting for external temp download folder
- Work-around for servers that do not support STAT command
- Removed articles are now listed separately in download report
- Add "abort" option to encryption detection
- Fix missing Retry link for "Out of retention" jobs.
- Option to abort download when it is clear that not enough data is available
- Support "nzbname" parameter in addfile/addlocalfile api calls for
ZIP files with a single NZB
- Support NZB-1.1 meta data "password" and "category"
- Don't retry an empty but correct NZB from an indexer
-------------------------------------------------------------------------------
0.7.7Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Windows/OSX: Update unrar to 4.20
- Fix some issues with orphaned items
- Generic sort didn't always rename media files in multi-part jobs properly
- Optional web-ui watchdog
- Always show RSS items in the same order as the original RSS feed
- Remove unusable folders from folder selector (Plush skin)
- Remove newzbin support
-------------------------------------------------------------------------------
0.7.6Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Recursive scanning when re-queuing downloaded NZB files
- Log "User-Agent" header of API calls
-------------------------------------------------------------------------------
0.7.6Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- A damaged smallest par2 can block fetching of more par2 files
- Fix evaluation of schedules at startup
- Make check for running SABnzbd instance more robust
-------------------------------------------------------------------------------
0.7.6Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Handle par2 sets that were renamed after creation
- Prevent blocking assembly of completed files, ( this resulted in
excessive CPU and memory usage)
- Fix speed issues with some Usenet servers due to unreachable IPv6 addresses
- Fix issues with SFV-base checks
- Prevent crash on Unix-Pythons that don't have the os.getloadavg() function
- Successfully pre-checked job lost its attributes when those were changed during check
- Remove version check when looking for a running instance of SABnzbd
-------------------------------------------------------------------------------
0.7.5Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Add missing %dn formula to Generic Sort
- Improve RSS logging
-------------------------------------------------------------------------------
0.7.5RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Prevent stuck jobs at end of pre-check.
- Fix issues with accented and special characters in names of downloaded files.
- Adjust nzbmatrix category table.
- Add 'prio_sort_list' special
- Add special option 'empty_postproc'.
- Prevent CherryPy crash when reading a cookie from another app which has a non-standard name.
- Prevent crash when trying to open non-existing "complete" folder from Windows System-tray icon.
- Fix problem with "Read" button when RSS feed name contains "&".
- Prevent unusual SFV files from crashing post-processing.
- OSX: Retina compatible menu-bar icons.
- Don't show speed and ETA when download is paused during post-processing
- Prevent soft-crash when api-function "addfile" is called without parameters.
- Add news channel frame
-------------------------------------------------------------------------------
0.7.4Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Pre-queue script no longer got the show/season/episode information.
- Prevent crash on startup when a fully downloaded job is still in download queue.
- New RSS feed should no longer be considered new after first, but empty readout.
- Make "auth" call backward-compatible with 0.6.x releases.
- Config->Notifications: email and growl server addresses should not be marked as "url" type.
- OSX: fix top menu queue info so that it shows total queue size
-------------------------------------------------------------------------------
0.7.4RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Pre-check failed to consider extra par2 files
- Fixed unjustified warning that can occur with OSX Growl 2.0
- Show memory usage on Linux systems
- Fix incorrect end-of-month quota reset
- Fix UI refresh issue when using Safari on iOS6
-------------------------------------------------------------------------------
0.7.4RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Remove potential queue stalling when downloading extra par2 files
- Make Windows version less eager to use par2-classic
- Fixed DMG images
- Add missing encoding directive to Plush and Classic skins
- Prevent oversized data in API-call "history"
-------------------------------------------------------------------------------
0.7.4Beta3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- All three OSX build in one DMG again
- Minor bugfixes
-------------------------------------------------------------------------------
0.7.4Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix failure to fetch more par2-files for posts with badly formatted subject lines
- After successful pre-check, preserve a job's position in the queue
- Restore SABnzbd icon for Growl
- Fix "check new releases" option in Config skin
- Separate DMG files for OSX Leopard/SL, Lion and MLion
-------------------------------------------------------------------------------
0.7.4Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- OSX Mountain Lion Notification Center support
- OSX Mountain Lion improved "keep awake" support
- OSX: separate builds: one for Mountain Lion and one for all others
- OSX removed 64bit code
- Scheduler: action can now run on multiple weekdays
- Scheduler: add "remove failed jobs" action
- Special option: rss_odd_titles (see Wiki)
- Support for HTTPS chain files (needed when you buy your own certificate)
- Prevent jobs from showing up in queue and history simultaneously
- Add parameter 'pp_active' to history elements in qstatus API call
- Fix some minor par2 handling bugs
- Prevent potential crash when an actively downloading job is deleted from the queue
- Special option: 'overwrite_files' (See Wiki)
- Don't try an SFV check when a retried job was already successfully verified by par2
- Enable compression of API call results
- Log failed attempts to log in to the Web UI
- A job with "forced" priority should keep that when fetching more par2 files
- Updated translations
-------------------------------------------------------------------------------
0.7.3Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Rename Special "random_server_ip" to "randomize_server_ip" so that we
can force the default to "Off". "On" kills speed on some servers.
- Ignore pseudo NZB files that start with a period in the name
- SFV failure now listed in History instead of issuing warnings
- Translation updates
- "502" errors about payments/credits will now block a server
-------------------------------------------------------------------------------
0.7.3Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Try to keep OSX Mountain Lion awake as long as downloading/postprocessing runs
- Prevent queue deadlock in case of fatally damaged par2 files
- Add RSS filter-enable checkboxes to Plush, Smpl and Classic skins
- Fix problem with saving modified paramters of an already enabled server
- Extend "check new release" option with test releases
-------------------------------------------------------------------------------
0.7.3Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Correct several errors in Sort function
- Improve organization of Config->Servers
- Support for nzbxxx.com
- Make detection of samples less aggressive
- Some minor corrections
-------------------------------------------------------------------------------
0.7.2Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix for NZB-icon issue when 0.7.0 was previously installed
- Check validity of totals9.sab file
- Fix startup problem when localhost has unexpected order of IP addresses
-------------------------------------------------------------------------------
0.7.2RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Improve support for nzbsrus.com
- Don't try to show NZB age when not known yet
- Prevent systems with unresolvable hostnames from always using 0.0.0.0
-------------------------------------------------------------------------------
0.7.2RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix fatal error in nzbsrus.com support
- Initial "quota left" was not set correctly when enabling quota
- Report incorrect RSS filter expressions (instead of aborting analysis)
- Improve detection of invalid articles (so that backup server will be tried)
- Windows installer: improve NZB association so that a reboot isn't needed
- Windows installer: don't remove settimngs by default when uninstalling
- Fix sorting of rar files in job so that .rar preceeds .r00
-------------------------------------------------------------------------------
0.7.1Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Disable VC90 check in Windows Installer as long as we're still on Python 2.5
- Windows: make sure \\server\share notation is never seen as a relative path
-------------------------------------------------------------------------------
0.7.1RC5 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix signing of OSX DMG
- Fix endless par2-fetch loop after retrying failed job
- Don't send "bad fetch" email when emailing is off
- Add some support for nzbrus.com's non-VIP limiting
-------------------------------------------------------------------------------
0.7.1RC4 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix failure to grab NZBs from indexers that send compressed files.
-------------------------------------------------------------------------------
0.7.1RC3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fixed stalling par2 fetches (after first verification run)
- Fixed retry behaviour of NZB fetching from URL
and add handling of nzbsrus.com error codes
- Make sure that all malformed articles are retried on another server
- Add no_ipv6 option that suppresses listing on ::1
(to be used if your system cannot handle that)
- Prevent crash in QuickCheck when expected par2 file wasn't downloaded
- Verification/repair would not be executed properly when one more RAR files
missed their first article.
- API calls "addurl" and "addid" (newzbin) can be used interchangeably
(Fixes a problem in Qouch)
-------------------------------------------------------------------------------
0.7.1RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Improved backup of sabnzbd.ini file
Will use backup when original is gone or become corrupt
- Windows: Using ::1 as single webhost address would start IE instead of default browser
-------------------------------------------------------------------------------
0.7.1RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Plush skin: fix problems with pull-down menus in Mobile Safari
- On some Linux and OSX systems using localhost would still make SABnzbd
give access to other computers
- Windows: the installer did not set an icon when associating NZB files with SABnzbd
- Fix problem that the Opera browser had with Config->Servers
- Retry a few times when accessing a mounted drive to create the
final destination folder
- Reduce load caused by WinTray and OSX topmenu
-------------------------------------------------------------------------------
0.7.0Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Updated translations
-------------------------------------------------------------------------------
0.7.0RC2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Suppress permission errors on paths containing ".AppleDouble" or ".DS_Store"
(Required for NAS systems that support Apple AFP shares)
- OSX/Windows: Set article cache to 200M when not already set.
- Pre-check: lower default minimum completion rate to 100.2%
-------------------------------------------------------------------------------
0.7.0RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix for rare crash in par2 fetching
- Another /nomedia fix
- Quota reset wasn't done when quota-reset-time was passed while SABnzbd wasn't running.
- Pre-check: required ratio for NZB without par2 files should be 100%
and not the "safe" ratio
-------------------------------------------------------------------------------
0.7.0Beta8 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Disabled the .nomedia marker file feature.
Those who want to try it, use the "nomedia_marker" setting in Config->Special
It remains an experimental feature without guarantees
- Add missing info in email about failed pre-check
- Updated translations
-------------------------------------------------------------------------------
0.7.0Beta7 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix for .nomedia files not being deleted
- Fix NZB re-queueing (due to .nomedia remaining)
- Polish was missing in Windows installer and Dutch was incorrect
- When Sort renames auxillirary files, it should disregard case
- Fix crash in Wizard on some Linux systems
-------------------------------------------------------------------------------
0.7.0Beta6 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Upgrade unzip for Windows to 6.00 (supports ZIPs above 2G)
- Lower threshold for pre-check to 100.5%
- Fix removal of .nomedia file when using Sorting
- Add Polish translation (using reduced character set)
- Extension-based cleanup list now also removes extension-only files like ".sfv".
- Several small issues
-------------------------------------------------------------------------------
0.7.0Beta5 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Solved serious connection problem with some providers
- Windows Tray has the "restart" entries no under a Troubleshoot menu
- Fix newzbin entries in History's "Source" field
- During unpacking the destination folder will contain a ".nomedia" file
which will keep mediaplayers temporarily from indexing
- Pre-check jobs now require 101% completion rate (with a "special" parameter)
- Unified OSX DMG
-------------------------------------------------------------------------------
0.7.0Beta4 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Add Portuguese (Brazil) language
- Updated translations
- Some odd NZB files led to blank initial filenames in file overview
- Jobs that have 99.91%-99.99% completion rate should not be rounded to 100.0%
- Windows Tray icon now has entry to show "complete" folder
- Some minor fixes in code and Config skin
- Individual RSS filter toggle
-------------------------------------------------------------------------------
0.7.0Beta3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- OSX/Linux: permissions are now also applied to the "temporary download folder"
- Fix some issues in the Config skin.
- The default for "apply max retries only on optional servers" is now 0,
thus enabling the new anti-deadlock behaviour for all servers
- Fix incompatibility with nzbsa.co.za indexer
- Log all API calls (in debug mode)
- Restore Python2.5 compatibility in growler.py
- After a language change, register again with Growl
- Clean up the api-call auth. It will now give preference to 'apikey'.
- Fix detection of retry-able history entries for case-insensitive file systems.
- API-calls "addfile" and "addlocalfile" returned an incorrect status value.
- Add support for the peculiar Usenet provider "free.xsusenet.com".
- OSX menu now uses the same formatting for speed as the skins.
- Accept multiple items in API-calls "addurl" and "addid".
The "name" and "nzbname" keywords can be repeated.
-------------------------------------------------------------------------------
0.7.0Beta2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix behavior when using host address 0.0.0.0 on a system
that doesn't resolve localhost properly
- Add Spanish translation
-------------------------------------------------------------------------------
0.7.0Beta1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Updated nzbmatrix categories
- "Special" option allows incomplete/partial NZB files
- Forbid "complete" being a subfolder of "incomplete"
-------------------------------------------------------------------------------
0.7.0Alpha3 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix failing join-by-par2
- Prevent API crash when deleting non-existing history item
- Prevent UI crash message when looking at NZB details page of finished job
- Config skin: fix path complettion in Config->Folders
- Config skin: fixes to support "hide behind proxy"
- Keep using unrar 4.10 for OSX Leopard and older, due to PPC support
-------------------------------------------------------------------------------
0.7.0Alpha2 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix disabled options in Config skin
- Remove flags from the Wizard and Config skin
- Replace real spaces in RSS-urls with %20
- Prevent double entries in History's "Source" section
- Prevent crash when OSDNotify doesn't work properly
- Small improvents in Windows installer
-------------------------------------------------------------------------------
0.7.0Alpha1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Not tracked
-------------------------------------------------------------------------------
0.6.15Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Flag post-processing as failed when files cannot be moved/copied to destination
- Fixed another newzbin link
-------------------------------------------------------------------------------
0.6.15RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Change newzbin URL
- Prevent setting watched-folder speed to 0 (while having no watched-folder)
from triggering an inifinite loop.
- Move "locale" construction from Plush skin to Python code.
Some embedded Linux platforms show unstable behavior with the original construction.
- Extend OSX menu with troubleshooting options
- Add trailing slashes to internal Plush paths to support reverse proxies better.
- Ignore whitespace around regular expressions in RSS filters.
- Prevent crash on restoring URL-fetches when using --repair-all option
- Fix "Repair" button on smpl Connection page. Current path fails when using a reverse proxy
- Suppress "incompatible feed" error when doing a scheduled/automatic RSS read-out.
- Add special setting to use "pickle" library instead of cPickle.

View File

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

View File

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

View File

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

View File

@@ -12,10 +12,10 @@
Windows-only:
If you keep having trouble with par2 multicore you can disable it
in Config->Switches.
This will force the use of the old and tried, but slower par2-classic program.
This will force the use of the old and tried, but slower par2cmdline program.
- A bug in Windows 7 may cause severe memory leaks when you use SABnzbd in
combination with some virus scanners and firewals.
combination with some virus scanners and firewalls.
Install this hotfix:
Description: http://support.microsoft.com/kb/979223/en-us
Download location: http://support.microsoft.com/hotfix/KBHotfix.aspx?kbnum=979223&kbln=en-us
@@ -24,13 +24,13 @@
For these the server blocking method is not very favourable.
There is an INI-only option that will limit blocks to 1 minute.
no_penalties = 1
See: http://wiki.sabnzbd.org/configure-special-0-8
See: https://sabnzbd.org/wiki/configuration/2.1/special
- Some third-party utilties try to probe SABnzbd API in such a way that you will
often see warnings about unauthenticated access.
If you are sure these probes are harmless, you can suppress the warnings by
setting the option "api_warnings" to 0.
See: http://wiki.sabnzbd.org/configure-special-0-8
See: https://sabnzbd.org/wiki/configuration/2.1/special
- On OSX you may encounter downloaded files with foreign characters.
The par2 repair may fail when the files were created on a Windows system.
@@ -39,9 +39,9 @@
- On Linux when you download files they may have the wrong character encoding.
You will see this only when downloaded files contain accented characters.
You need to fix it yourself by running the convmv utility (availaible for most Linux platforms).
You need to fix it yourself by running the convmv utility (available for most Linux platforms).
Possible the file system override setting 'fsys_type' might be solve things:
See: http://wiki.sabnzbd.org/configure-special-0-8
See: https://sabnzbd.org/wiki/configuration/2.1/special
- The "Watched Folder" sometimes fails to delete the NZB files it has
processed. This happens when other software still accesses these files.
@@ -53,15 +53,15 @@
memory systems (e.g. a NAS device or a router) a challenge.
- SABnzbd is not compatible with some software firewall versions.
The Mircosoft Windows Firewall works fine, but remember to tell this
The Microsoft Windows Firewall works fine, but remember to tell this
firewall that SABnzbd is allowed to talk to other computers.
- When SABnzbd cannot send nofication emails, check your virus scanner,
- When SABnzbd cannot send notification emails, check your virus scanner,
firewall or security suite. It may be blocking outgoing email.
- When you are using external drives or network shares on OSX or Linux
make sure that the drives are mounted.
The operating system wil simply redirect your files to alternative locations.
The operating system will simply redirect your files to alternative locations.
You may have trouble finding the files when mounting the drive later.
On OSX, SABnzbd will not create new folders in /Volumes.
The result will be a failed job that can be retried once the volume has been mounted.
@@ -81,4 +81,4 @@
- Squeeze Linux
There is a "special" option that will allow you to select an alternative library.
use_pickle = 1
See: http://wiki.sabnzbd.org/configure-special-0-8
See: https://sabnzbd.org/wiki/configuration/2.1/special

View File

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

View File

@@ -1,8 +1,8 @@
Metadata-Version: 1.0
Name: SABnzbd
Version: 0.8.0Alpha2
Summary: SABnzbd-0.8.0Alpha2
Home-page: http://sabnzbd.org
Version: 2.2.0Alpha2
Summary: SABnzbd-2.2.0Alpha2
Home-page: https://sabnzbd.org
Author: The SABnzbd Team
Author-email: team@sabnzbd.org
License: GNU General Public License 2 (GPL2 or later)

View File

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

View File

@@ -1,52 +1,74 @@
Release Notes - SABnzbd 0.8.0Alpha2
=====================================
Release Notes - SABnzbd 2.2.0 Alpha 2
=========================================================
## Changes since Alpha2
- Server priorities instead of primary/backup ==> REVIEW YOUR SERVER SETTINGS!
- Work-arounds to avoid bugs in PyOpenSSL 0.14
- Support RAR's REV files to some extent
- Diagnostic dashboard tab for "Status" page
NOTE: Due to changes in this release, the queue will be converted when 2.2.0
is started for the first time. Job order, settings and data will be
preserved, but all jobs will be unpaused and URL's that did not finish
fetching before the upgrade will be lost!
We now also accept donations via Bitcoin, Ethereum and Litecoin:
https://sabnzbd.org/donate/
## Changes since Alpha 1
- Show missing articles in MB instead of number of articles
- 'Download all par2' will download all par2 if par_cleanup is disabled
- Windows: Move enable_multipar to Specials (so MultiPar is always used)
- Windows: Better indication of verification process before and after repair
- Windows: MultiPar verification of a job is skipped after blocks are fetched
## Bugfixes since Alpha 1
- Fixed some "Saving failed" errors
- Fixed crashing URLGrabber
- Disk-space readings could be updated incorrectly
- Correct redirect after enabling HTTPS in the Config
- Fix race-condition in Post-processing
- History would not always show latest changes
- Convert HTML in error messages
- Not all texts were shown in the selected Language
## What's new in 0.8.0
## Changes in 2.2.0
- Reduced memory usage, especially with larger queues
- Slight improvement in download performance by removing internal locks
- Smoother animations in Firefox (disabled previously due to FF high-CPU usage)
- If enabled, replace dots in filenames also when there are spaces already
- Jobs outside server retention are processed faster
- max_art_opt and replace_illegal moved from Switches to Specials
- Full Unicode support with Chinese and Russian translations
- Improved Notifications (including Prowl)
- Duplicate detection for series
- Bonjour/ZeroConfig support
- More filters in RSS
- 7zip support
- Option to save repair time by downloading all par2 files
- Support for long paths in Windows (above 260)
- Improved security for external access
- Lots of small improvements and bug fixes
- The "Classic" skin is gone
## Bugfixes in 2.2.0
- Shutdown/suspend did not work on some Linux systems
- Deleting a job could result in write errors
- Display warning if custom par2 parameters are wrong
- macOS: Catch 'Protocol wrong type for socket' errors
- Windows: Fix error in MultiPar-code when first par2-file was damaged
## 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.
## Translations
- Added Hebrew translation by ION IL, many other languages updated.
(c) Copyright 2007-2015 by "The SABnzbd-team" \<team@sabnzbd.org\>
### IMPORTANT INFORMATION about release 0.8.0
<http://wiki.sabnzbd.org/introducing-0-8-0>
### Known problems and solutions
- Read the file "ISSUES.txt"
### Upgrading from 0.7.x and older
## Upgrading from 0.7.x and older
- Finish queue
- Stop SABnzbd
- Install new version
- Start SABnzbd
The organization of the download queue is different from older versions.
0.8.x will not see the existing queue, but you can go to
Status->QueueRepair and "Repair" the old queue.
Also, your sabnzbd.ini file will be upgraded, making it
incompatible with releases older than 0.7.9
## Upgrade notices (from pre-2.x.x)
- Windows: When starting the Post-Processing script, the path to the job folder
is no longer in short-path notation but includes the full path. To support
long paths (>255), you might need to alter them to long-path notation (\\?\).
- Schedule items are converted when upgrading to 2.x.x and will break when
reverted back to pre-2.x.x releases.
- The organization of the download queue is different from 0.7.x releases.
So 2.x.x will not see the existing queue, but you can go to Status->Queue Repair
and "Repair" the old queue.
## Known problems and solutions
- Read the file "ISSUES.txt"
## About
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically, thanks
to its web-based user interface and advanced built-in post-processing options
that automatically verify, repair, extract and clean up posts downloaded
from Usenet.
(c) Copyright 2007-2017 by "The SABnzbd-team" \<team@sabnzbd.org\>

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,3 +1,5 @@
CherryPy 3.2.2 Official distribution: http://download.cherrypy.org/cherrypy/3.2.2/CherryPy-3.2.2.tar.gz
CherryPy 8.1.2
Official distribution: https://github.com/cherrypy/cherrypy/releases
The folders 'tutorial', 'test' and 'scaffold' have been removed.
This file has been added.

View File

@@ -53,32 +53,34 @@ with customized or extended components. The core API's are:
* Server API
* WSGI API
These API's are described in the CherryPy specification:
http://www.cherrypy.org/wiki/CherryPySpec
These API's are described in the `CherryPy specification <https://bitbucket.org/cherrypy/cherrypy/wiki/CherryPySpec>`_.
"""
__version__ = "3.2.2"
try:
import pkg_resources
except ImportError:
pass
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
from cherrypy._cpcompat import basestring, unicodestr, set
from threading import local as _local
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect # noqa
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError # noqa
from cherrypy import _cpdispatch as dispatch
from cherrypy import _cplogging
from cherrypy import _cpdispatch as dispatch # noqa
from cherrypy import _cptools
tools = _cptools.default_toolbox
Tool = _cptools.Tool
from cherrypy._cptools import default_toolbox as tools, Tool
from cherrypy import _cprequest
from cherrypy.lib import httputil as _httputil
from cherrypy import _cptree
tree = _cptree.Tree()
from cherrypy._cptree import Application
from cherrypy import _cpwsgi as wsgi
from cherrypy._cptree import Application # noqa
from cherrypy import _cpwsgi as wsgi # noqa
from cherrypy import _cpserver
from cherrypy import process
try:
from cherrypy.process import win32
@@ -89,26 +91,33 @@ except ImportError:
engine = process.bus
tree = _cptree.Tree()
__version__ = '8.1.2'
# Timeout monitor. We add two channels to the engine
# to which cherrypy.Application will publish.
engine.listeners['before_request'] = set()
engine.listeners['after_request'] = set()
class _TimeoutMonitor(process.plugins.Monitor):
def __init__(self, bus):
self.servings = []
process.plugins.Monitor.__init__(self, bus, self.run)
def before_request(self):
self.servings.append((serving.request, serving.response))
def after_request(self):
try:
self.servings.remove((serving.request, serving.response))
except ValueError:
pass
def run(self):
"""Check timeout on all responses. (Internal)"""
for req, resp in self.servings:
@@ -125,14 +134,31 @@ engine.thread_manager.subscribe()
engine.signal_handler = process.plugins.SignalHandler(engine)
from cherrypy import _cpserver
class _HandleSignalsPlugin(object):
"""Handle signals from other processes based on the configured
platform handlers above."""
def __init__(self, bus):
self.bus = bus
def subscribe(self):
"""Add the handlers based on the platform"""
if hasattr(self.bus, 'signal_handler'):
self.bus.signal_handler.subscribe()
if hasattr(self.bus, 'console_control_handler'):
self.bus.console_control_handler.subscribe()
engine.signals = _HandleSignalsPlugin(engine)
server = _cpserver.Server()
server.subscribe()
def quickstart(root=None, script_name="", config=None):
def quickstart(root=None, script_name='', config=None):
"""Mount the given root, start the builtin server (and engine), then block.
root: an instance of a "controller class" (a collection of page handler
methods) which represents the root of the application.
script_name: a string containing the "mount point" of the application.
@@ -140,7 +166,7 @@ def quickstart(root=None, script_name="", config=None):
at which to mount the given root. For example, if root.index() will
handle requests to "http://www.example.com:8080/dept/app1/", then
the script_name argument would be "/dept/app1".
It MUST NOT end in a slash. If the script_name refers to the root
of the URI, it MUST be an empty string (not "/").
config: a file or dict containing application config. If this contains
@@ -149,23 +175,18 @@ def quickstart(root=None, script_name="", config=None):
"""
if config:
_global_conf_alias.update(config)
tree.mount(root, script_name, config)
if hasattr(engine, "signal_handler"):
engine.signal_handler.subscribe()
if hasattr(engine, "console_control_handler"):
engine.console_control_handler.subscribe()
engine.signals.subscribe()
engine.start()
engine.block()
from cherrypy._cpcompat import threadlocal as _local
class _Serving(_local):
"""An interface for registering request and response objects.
Rather than have a separate "thread local" object for the request and
the response, this class works as a single threadlocal container for
both objects (and any others which developers wish to define). In this
@@ -173,22 +194,22 @@ class _Serving(_local):
conversation, yet still refer to them as module-level globals in a
thread-safe way.
"""
request = _cprequest.Request(_httputil.Host("127.0.0.1", 80),
_httputil.Host("127.0.0.1", 1111))
request = _cprequest.Request(_httputil.Host('127.0.0.1', 80),
_httputil.Host('127.0.0.1', 1111))
"""
The request object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None."""
response = _cprequest.Response()
"""
The response object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None."""
def load(self, request, response):
self.request = request
self.response = response
def clear(self):
"""Remove all attributes of self."""
self.__dict__.clear()
@@ -197,54 +218,54 @@ serving = _Serving()
class _ThreadLocalProxy(object):
__slots__ = ['__attrname__', '__dict__']
def __init__(self, attrname):
self.__attrname__ = attrname
def __getattr__(self, name):
child = getattr(serving, self.__attrname__)
return getattr(child, name)
def __setattr__(self, name, value):
if name in ("__attrname__", ):
if name in ('__attrname__', ):
object.__setattr__(self, name, value)
else:
child = getattr(serving, self.__attrname__)
setattr(child, name, value)
def __delattr__(self, name):
child = getattr(serving, self.__attrname__)
delattr(child, name)
def _get_dict(self):
child = getattr(serving, self.__attrname__)
d = child.__class__.__dict__.copy()
d.update(child.__dict__)
return d
__dict__ = property(_get_dict)
def __getitem__(self, key):
child = getattr(serving, self.__attrname__)
return child[key]
def __setitem__(self, key, value):
child = getattr(serving, self.__attrname__)
child[key] = value
def __delitem__(self, key):
child = getattr(serving, self.__attrname__)
del child[key]
def __contains__(self, key):
child = getattr(serving, self.__attrname__)
return key in child
def __len__(self):
child = getattr(serving, self.__attrname__)
return len(child)
def __nonzero__(self):
child = getattr(serving, self.__attrname__)
return bool(child)
@@ -258,7 +279,10 @@ request = _ThreadLocalProxy('request')
response = _ThreadLocalProxy('response')
# Create thread_data object as a thread-specific all-purpose storage
class _ThreadData(_local):
"""A container for thread-specific data."""
thread_data = _ThreadData()
@@ -281,29 +305,31 @@ except ImportError:
pass
from cherrypy import _cplogging
class _GlobalLogManager(_cplogging.LogManager):
"""A site-wide LogManager; routes to app.log or global log as appropriate.
This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
cherrypy.log() and cherrypy.log.access(). If either
function is called during a request, the message will be sent to the
logger for the current Application. If they are called outside of a
request, the message will be sent to the site-wide logger.
"""
def __call__(self, *args, **kwargs):
"""Log the given message to the app.log or global log as appropriate."""
# Do NOT use try/except here. See http://www.cherrypy.org/ticket/945
"""Log the given message to the app.log or global log as appropriate.
"""
# Do NOT use try/except here. See
# https://github.com/cherrypy/cherrypy/issues/945
if hasattr(request, 'app') and hasattr(request.app, 'log'):
log = request.app.log
else:
log = self
return log.error(*args, **kwargs)
def access(self):
"""Log an access message to the app.log or global log as appropriate."""
"""Log an access message to the app.log or global log as appropriate.
"""
try:
return request.app.log.access()
except AttributeError:
@@ -317,294 +343,15 @@ log.error_file = ''
# Using an access file makes CP about 10% slower. Leave off by default.
log.access_file = ''
def _buslog(msg, level):
log.error(msg, 'ENGINE', severity=level)
engine.subscribe('log', _buslog)
# Helper functions for CP apps #
def expose(func=None, alias=None):
"""Expose the function, optionally providing an alias or set of aliases."""
def expose_(func):
func.exposed = True
if alias is not None:
if isinstance(alias, basestring):
parents[alias.replace(".", "_")] = func
else:
for a in alias:
parents[a.replace(".", "_")] = func
return func
import sys, types
if isinstance(func, (types.FunctionType, types.MethodType)):
if alias is None:
# @expose
func.exposed = True
return func
else:
# func = expose(func, alias)
parents = sys._getframe(1).f_locals
return expose_(func)
elif func is None:
if alias is None:
# @expose()
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose(alias="alias") or
# @expose(alias=["alias1", "alias2"])
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose("alias") or
# @expose(["alias1", "alias2"])
parents = sys._getframe(1).f_locals
alias = func
return expose_
def popargs(*args, **kwargs):
"""A decorator for _cp_dispatch
(cherrypy.dispatch.Dispatcher.dispatch_method_name).
Optional keyword argument: handler=(Object or Function)
Provides a _cp_dispatch function that pops off path segments into
cherrypy.request.params under the names specified. The dispatch
is then forwarded on to the next vpath element.
Note that any existing (and exposed) member function of the class that
popargs is applied to will override that value of the argument. For
instance, if you have a method named "list" on the class decorated with
popargs, then accessing "/list" will call that function instead of popping
it off as the requested parameter. This restriction applies to all
_cp_dispatch functions. The only way around this restriction is to create
a "blank class" whose only function is to provide _cp_dispatch.
If there are path elements after the arguments, or more arguments
are requested than are available in the vpath, then the 'handler'
keyword argument specifies the next object to handle the parameterized
request. If handler is not specified or is None, then self is used.
If handler is a function rather than an instance, then that function
will be called with the args specified and the return value from that
function used as the next object INSTEAD of adding the parameters to
cherrypy.request.args.
This decorator may be used in one of two ways:
As a class decorator:
@cherrypy.popargs('year', 'month', 'day')
class Blog:
def index(self, year=None, month=None, day=None):
#Process the parameters here; any url like
#/, /2009, /2009/12, or /2009/12/31
#will fill in the appropriate parameters.
def create(self):
#This link will still be available at /create. Defined functions
#take precedence over arguments.
Or as a member of a class:
class Blog:
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
#...
The handler argument may be used to mix arguments with built in functions.
For instance, the following setup allows different activities at the
day, month, and year level:
class DayHandler:
def index(self, year, month, day):
#Do something with this day; probably list entries
def delete(self, year, month, day):
#Delete all entries for this day
@cherrypy.popargs('day', handler=DayHandler())
class MonthHandler:
def index(self, year, month):
#Do something with this month; probably list entries
def delete(self, year, month):
#Delete all entries for this month
@cherrypy.popargs('month', handler=MonthHandler())
class YearHandler:
def index(self, year):
#Do something with this year
#...
@cherrypy.popargs('year', handler=YearHandler())
class Root:
def index(self):
#...
"""
#Since keyword arg comes after *args, we have to process it ourselves
#for lower versions of python.
handler = None
handler_call = False
for k,v in kwargs.items():
if k == 'handler':
handler = v
else:
raise TypeError(
"cherrypy.popargs() got an unexpected keyword argument '{0}'" \
.format(k)
)
import inspect
if handler is not None \
and (hasattr(handler, '__call__') or inspect.isclass(handler)):
handler_call = True
def decorated(cls_or_self=None, vpath=None):
if inspect.isclass(cls_or_self):
#cherrypy.popargs is a class decorator
cls = cls_or_self
setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated)
return cls
#We're in the actual function
self = cls_or_self
parms = {}
for arg in args:
if not vpath:
break
parms[arg] = vpath.pop(0)
if handler is not None:
if handler_call:
return handler(**parms)
else:
request.params.update(parms)
return handler
request.params.update(parms)
#If we are the ultimate handler, then to prevent our _cp_dispatch
#from being called again, we will resolve remaining elements through
#getattr() directly.
if vpath:
return getattr(self, vpath.pop(0), None)
else:
return self
return decorated
def url(path="", qs="", script_name=None, base=None, relative=None):
"""Create an absolute URL for the given path.
If 'path' starts with a slash ('/'), this will return
(base + script_name + path + qs).
If it does not start with a slash, this returns
(base + script_name [+ request.path_info] + path + qs).
If script_name is None, cherrypy.request will be used
to find a script_name, if available.
If base is None, cherrypy.request.base will be used (if available).
Note that you can use cherrypy.tools.proxy to change this.
Finally, note that this function can be used to obtain an absolute URL
for the current request path (minus the querystring) by passing no args.
If you call url(qs=cherrypy.request.query_string), you should get the
original browser URL (assuming no internal redirections).
If relative is None or not provided, request.app.relative_urls will
be used (if available, else False). If False, the output will be an
absolute URL (including the scheme, host, vhost, and script_name).
If True, the output will instead be a URL that is relative to the
current request path, perhaps including '..' atoms. If relative is
the string 'server', the output will instead be a URL that is
relative to the server root; i.e., it will start with a slash.
"""
if isinstance(qs, (tuple, list, dict)):
qs = _urlencode(qs)
if qs:
qs = '?' + qs
if request.app:
if not path.startswith("/"):
# Append/remove trailing slash from path_info as needed
# (this is to support mistyped URL's without redirecting;
# if you want to redirect, use tools.trailing_slash).
pi = request.path_info
if request.is_index is True:
if not pi.endswith('/'):
pi = pi + '/'
elif request.is_index is False:
if pi.endswith('/') and pi != '/':
pi = pi[:-1]
if path == "":
path = pi
else:
path = _urljoin(pi, path)
if script_name is None:
script_name = request.script_name
if base is None:
base = request.base
newurl = base + script_name + path + qs
else:
# No request.app (we're being called outside a request).
# We'll have to guess the base from server.* attributes.
# This will produce very different results from the above
# if you're using vhosts or tools.proxy.
if base is None:
base = server.base()
path = (script_name or "") + path
newurl = base + path + qs
if './' in newurl:
# Normalize the URL by removing ./ and ../
atoms = []
for atom in newurl.split('/'):
if atom == '.':
pass
elif atom == '..':
atoms.pop()
else:
atoms.append(atom)
newurl = '/'.join(atoms)
# At this point, we should have a fully-qualified absolute URL.
if relative is None:
relative = getattr(request.app, "relative_urls", False)
# See http://www.ietf.org/rfc/rfc2396.txt
if relative == 'server':
# "A relative reference beginning with a single slash character is
# termed an absolute-path reference, as defined by <abs_path>..."
# This is also sometimes called "server-relative".
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
elif relative:
# "A relative reference that does not begin with a scheme name
# or a slash character is termed a relative-path reference."
old = url(relative=False).split('/')[:-1]
new = newurl.split('/')
while old and new:
a, b = old[0], new[0]
if a != b:
break
old.pop(0)
new.pop(0)
new = (['..'] * len(old)) + new
newurl = '/'.join(new)
return newurl
from cherrypy._helper import expose, popargs, url # noqa
# import _cpconfig last so it can reference other top-level objects
from cherrypy import _cpconfig
from cherrypy import _cpconfig # noqa
# Use _global_conf_alias so quickstart can use 'config' as an arg
# without shadowing cherrypy.config.
config = _global_conf_alias = _cpconfig.Config()
@@ -613,12 +360,12 @@ config.defaults = {
'tools.log_headers.on': True,
'tools.trailing_slash.on': True,
'tools.encode.on': True
}
config.namespaces["log"] = lambda k, v: setattr(log, k, v)
config.namespaces["checker"] = lambda k, v: setattr(checker, k, v)
}
config.namespaces['log'] = lambda k, v: setattr(log, k, v)
config.namespaces['checker'] = lambda k, v: setattr(checker, k, v)
# Must reset to get our defaults applied.
config.reset()
from cherrypy import _cpchecker
from cherrypy import _cpchecker # noqa
checker = _cpchecker.Checker()
engine.subscribe('start', checker)

4
cherrypy/__main__.py Executable file
View File

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

View File

@@ -6,26 +6,26 @@ from cherrypy._cpcompat import iteritems, copykeys, builtins
class Checker(object):
"""A checker for CherryPy sites and their mounted applications.
When this object is called at engine startup, it executes each
of its own methods whose names start with ``check_``. If you wish
to disable selected checks, simply add a line in your global
config which sets the appropriate method to False::
[global]
checker.check_skipped_app_config = False
You may also dynamically add or replace ``check_*`` methods in this way.
"""
on = True
"""If True (the default), run all checks; if False, turn off all checks."""
def __init__(self):
self._populate_known_types()
def __call__(self):
"""Run all check_* methods."""
if self.on:
@@ -33,22 +33,23 @@ class Checker(object):
warnings.formatwarning = self.formatwarning
try:
for name in dir(self):
if name.startswith("check_"):
if name.startswith('check_'):
method = getattr(self, name)
if method and hasattr(method, '__call__'):
method()
finally:
warnings.formatwarning = oldformatwarning
def formatwarning(self, message, category, filename, lineno, line=None):
"""Function to format a warning."""
return "CherryPy Checker:\n%s\n\n" % message
return 'CherryPy Checker:\n%s\n\n' % message
# This value should be set inside _cpconfig.
global_config_contained_paths = False
def check_app_config_entries_dont_start_with_script_name(self):
"""Check for Application config with sections that repeat script_name."""
"""Check for Application config with sections that repeat script_name.
"""
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
@@ -56,66 +57,71 @@ class Checker(object):
continue
if sn == '':
continue
sn_atoms = sn.strip("/").split("/")
sn_atoms = sn.strip('/').split('/')
for key in app.config.keys():
key_atoms = key.strip("/").split("/")
key_atoms = key.strip('/').split('/')
if key_atoms[:len(sn_atoms)] == sn_atoms:
warnings.warn(
"The application mounted at %r has config " \
"entries that start with its script name: %r" % (sn, key))
'The application mounted at %r has config '
'entries that start with its script name: %r' % (sn,
key))
def check_site_config_entries_in_app_config(self):
"""Check for mounted Applications that have site-scoped config."""
for sn, app in iteritems(cherrypy.tree.apps):
if not isinstance(app, cherrypy.Application):
continue
msg = []
for section, entries in iteritems(app.config):
if section.startswith('/'):
for key, value in iteritems(entries):
for n in ("engine.", "server.", "tree.", "checker."):
for n in ('engine.', 'server.', 'tree.', 'checker.'):
if key.startswith(n):
msg.append("[%s] %s = %s" % (section, key, value))
msg.append('[%s] %s = %s' %
(section, key, value))
if msg:
msg.insert(0,
"The application mounted at %r contains the following "
"config entries, which are only allowed in site-wide "
"config. Move them to a [global] section and pass them "
"to cherrypy.config.update() instead of tree.mount()." % sn)
'The application mounted at %r contains the '
'following config entries, which are only allowed '
'in site-wide config. Move them to a [global] '
'section and pass them to cherrypy.config.update() '
'instead of tree.mount().' % sn)
warnings.warn(os.linesep.join(msg))
def check_skipped_app_config(self):
"""Check for mounted Applications that have no config."""
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
if not app.config:
msg = "The Application mounted at %r has an empty config." % sn
msg = 'The Application mounted at %r has an empty config.' % sn
if self.global_config_contained_paths:
msg += (" It looks like the config you passed to "
"cherrypy.config.update() contains application-"
"specific sections. You must explicitly pass "
"application config via "
"cherrypy.tree.mount(..., config=app_config)")
msg += (' It looks like the config you passed to '
'cherrypy.config.update() contains application-'
'specific sections. You must explicitly pass '
'application config via '
'cherrypy.tree.mount(..., config=app_config)')
warnings.warn(msg)
return
def check_app_config_brackets(self):
"""Check for Application config with extraneous brackets in section names."""
"""Check for Application config with extraneous brackets in section
names.
"""
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
if not app.config:
continue
for key in app.config.keys():
if key.startswith("[") or key.endswith("]"):
if key.startswith('[') or key.endswith(']'):
warnings.warn(
"The application mounted at %r has config " \
"section names with extraneous brackets: %r. "
"Config *files* need brackets; config *dicts* "
"(e.g. passed to tree.mount) do not." % (sn, key))
'The application mounted at %r has config '
'section names with extraneous brackets: %r. '
'Config *files* need brackets; config *dicts* '
'(e.g. passed to tree.mount) do not.' % (sn, key))
def check_static_paths(self):
"""Check Application config for incorrect static paths."""
# Use the dummy Request object in the main thread.
@@ -126,48 +132,50 @@ class Checker(object):
request.app = app
for section in app.config:
# get_resource will populate request.config
request.get_resource(section + "/dummy.html")
request.get_resource(section + '/dummy.html')
conf = request.config.get
if conf("tools.staticdir.on", False):
msg = ""
root = conf("tools.staticdir.root")
dir = conf("tools.staticdir.dir")
if conf('tools.staticdir.on', False):
msg = ''
root = conf('tools.staticdir.root')
dir = conf('tools.staticdir.dir')
if dir is None:
msg = "tools.staticdir.dir is not set."
msg = 'tools.staticdir.dir is not set.'
else:
fulldir = ""
fulldir = ''
if os.path.isabs(dir):
fulldir = dir
if root:
msg = ("dir is an absolute path, even "
"though a root is provided.")
msg = ('dir is an absolute path, even '
'though a root is provided.')
testdir = os.path.join(root, dir[1:])
if os.path.exists(testdir):
msg += ("\nIf you meant to serve the "
"filesystem folder at %r, remove "
"the leading slash from dir." % testdir)
msg += (
'\nIf you meant to serve the '
'filesystem folder at %r, remove the '
'leading slash from dir.' % (testdir,))
else:
if not root:
msg = "dir is a relative path and no root provided."
msg = (
'dir is a relative path and '
'no root provided.')
else:
fulldir = os.path.join(root, dir)
if not os.path.isabs(fulldir):
msg = "%r is not an absolute path." % fulldir
msg = ('%r is not an absolute path.' % (
fulldir,))
if fulldir and not os.path.exists(fulldir):
if msg:
msg += "\n"
msg += ("%r (root + dir) is not an existing "
"filesystem path." % fulldir)
msg += '\n'
msg += ('%r (root + dir) is not an existing '
'filesystem path.' % fulldir)
if msg:
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r'
% (msg, section, root, dir))
# -------------------------- Compatibility -------------------------- #
obsolete = {
'server.default_content_type': 'tools.response_headers.headers',
'log_access_file': 'log.access_file',
@@ -180,31 +188,31 @@ class Checker(object):
'throw_errors': 'request.throw_errors',
'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
'cherrypy.Application(Root())))'),
}
}
deprecated = {}
def _compat(self, config):
"""Process config and warn on each obsolete or deprecated entry."""
for section, conf in config.items():
if isinstance(conf, dict):
for k, v in conf.items():
if k in self.obsolete:
warnings.warn("%r is obsolete. Use %r instead.\n"
"section: [%s]" %
warnings.warn('%r is obsolete. Use %r instead.\n'
'section: [%s]' %
(k, self.obsolete[k], section))
elif k in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead.\n"
"section: [%s]" %
warnings.warn('%r is deprecated. Use %r instead.\n'
'section: [%s]' %
(k, self.deprecated[k], section))
else:
if section in self.obsolete:
warnings.warn("%r is obsolete. Use %r instead."
warnings.warn('%r is obsolete. Use %r instead.'
% (section, self.obsolete[section]))
elif section in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead."
warnings.warn('%r is deprecated. Use %r instead.'
% (section, self.deprecated[section]))
def check_compatibility(self):
"""Process config and warn on each obsolete or deprecated entry."""
self._compat(cherrypy.config)
@@ -212,45 +220,47 @@ class Checker(object):
if not isinstance(app, cherrypy.Application):
continue
self._compat(app.config)
# ------------------------ Known Namespaces ------------------------ #
extra_config_namespaces = []
def _known_ns(self, app):
ns = ["wsgi"]
ns = ['wsgi']
ns.extend(copykeys(app.toolboxes))
ns.extend(copykeys(app.namespaces))
ns.extend(copykeys(app.request_class.namespaces))
ns.extend(copykeys(cherrypy.config.namespaces))
ns += self.extra_config_namespaces
for section, conf in app.config.items():
is_path_section = section.startswith("/")
is_path_section = section.startswith('/')
if is_path_section and isinstance(conf, dict):
for k, v in conf.items():
atoms = k.split(".")
atoms = k.split('.')
if len(atoms) > 1:
if atoms[0] not in ns:
# Spit out a special warning if a known
# namespace is preceded by "cherrypy."
if (atoms[0] == "cherrypy" and atoms[1] in ns):
msg = ("The config entry %r is invalid; "
"try %r instead.\nsection: [%s]"
% (k, ".".join(atoms[1:]), section))
if atoms[0] == 'cherrypy' and atoms[1] in ns:
msg = (
'The config entry %r is invalid; '
'try %r instead.\nsection: [%s]'
% (k, '.'.join(atoms[1:]), section))
else:
msg = ("The config entry %r is invalid, because "
"the %r config namespace is unknown.\n"
"section: [%s]" % (k, atoms[0], section))
msg = (
'The config entry %r is invalid, '
'because the %r config namespace '
'is unknown.\n'
'section: [%s]' % (k, atoms[0], section))
warnings.warn(msg)
elif atoms[0] == "tools":
elif atoms[0] == 'tools':
if atoms[1] not in dir(cherrypy.tools):
msg = ("The config entry %r may be invalid, "
"because the %r tool was not found.\n"
"section: [%s]" % (k, atoms[1], section))
msg = (
'The config entry %r may be invalid, '
'because the %r tool was not found.\n'
'section: [%s]' % (k, atoms[1], section))
warnings.warn(msg)
def check_config_namespaces(self):
"""Process config and warn on each unknown config namespace."""
for sn, app in cherrypy.tree.apps.items():
@@ -258,17 +268,13 @@ class Checker(object):
continue
self._known_ns(app)
# -------------------------- Config Types -------------------------- #
known_config_types = {}
def _populate_known_types(self):
b = [x for x in vars(builtins).values()
if type(x) is type(str)]
def traverse(obj, namespace):
for name in dir(obj):
# Hack for 3.2's warning about body_params
@@ -276,18 +282,18 @@ class Checker(object):
continue
vtype = type(getattr(obj, name, None))
if vtype in b:
self.known_config_types[namespace + "." + name] = vtype
traverse(cherrypy.request, "request")
traverse(cherrypy.response, "response")
traverse(cherrypy.server, "server")
traverse(cherrypy.engine, "engine")
traverse(cherrypy.log, "log")
self.known_config_types[namespace + '.' + name] = vtype
traverse(cherrypy.request, 'request')
traverse(cherrypy.response, 'response')
traverse(cherrypy.server, 'server')
traverse(cherrypy.engine, 'engine')
traverse(cherrypy.log, 'log')
def _known_types(self, config):
msg = ("The config entry %r in section %r is of type %r, "
"which does not match the expected type %r.")
msg = ('The config entry %r in section %r is of type %r, '
'which does not match the expected type %r.')
for section, conf in config.items():
if isinstance(conf, dict):
for k, v in conf.items():
@@ -305,7 +311,7 @@ class Checker(object):
if expected_type and vtype != expected_type:
warnings.warn(msg % (k, section, vtype.__name__,
expected_type.__name__))
def check_config_types(self):
"""Assert that config values are of the same type as default values."""
self._known_types(cherrypy.config)
@@ -313,15 +319,14 @@ class Checker(object):
if not isinstance(app, cherrypy.Application):
continue
self._known_types(app.config)
# -------------------- Specific config warnings -------------------- #
def check_localhost(self):
"""Warn if any socket_host is 'localhost'. See #711."""
for k, v in cherrypy.config.items():
if k == 'server.socket_host' and v == 'localhost':
warnings.warn("The use of 'localhost' as a socket host can "
"cause problems on newer systems, since 'localhost' can "
"map to either an IPv4 or an IPv6 address. You should "
"use '127.0.0.1' or '[::1]' instead.")
'cause problems on newer systems, since '
"'localhost' can map to either an IPv4 or an "
"IPv6 address. You should use '127.0.0.1' "
"or '[::1]' instead.")

View File

@@ -1,13 +1,13 @@
"""Compatibility code for using CherryPy with various versions of Python.
CherryPy 3.2 is compatible with Python versions 2.3+. This module provides a
CherryPy 3.2 is compatible with Python versions 2.6+. This module provides a
useful abstraction over the differences between Python versions, sometimes by
preferring a newer idiom, sometimes an older one, and sometimes a custom one.
In particular, Python 2 uses str and '' for byte strings, while Python 3
uses str and '' for unicode strings. We will call each of these the 'native
string' type for each version. Because of this major difference, this module
provides new 'bytestr', 'unicodestr', and 'nativestr' attributes, as well as
provides
two functions: 'ntob', which translates native strings (of type 'str') into
byte strings regardless of Python version, and 'ntou', which translates native
strings to unicode strings. This also provides a 'BytesIO' name for dealing
@@ -15,81 +15,80 @@ specifically with bytes, and a 'StringIO' name for dealing with native strings.
It also provides a 'base64_decode' function with native strings as input and
output.
"""
import binascii
import os
import re
import sys
import threading
if sys.version_info >= (3, 0):
py3k = True
bytestr = bytes
unicodestr = str
nativestr = unicodestr
basestring = (bytes, str)
import six
if six.PY3:
def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given encoding."""
"""Return the given native string as a byte string in the given
encoding.
"""
assert_native(n)
# In Python 3, the native string type is unicode
return n.encode(encoding)
def ntou(n, encoding='ISO-8859-1'):
"""Return the given native string as a unicode string with the given encoding."""
"""Return the given native string as a unicode string with the given
encoding.
"""
assert_native(n)
# In Python 3, the native string type is unicode
return n
def tonative(n, encoding='ISO-8859-1'):
"""Return the given string as a native string in the given encoding."""
# In Python 3, the native string type is unicode
if isinstance(n, bytes):
return n.decode(encoding)
return n
# type("")
from io import StringIO
# bytes:
from io import BytesIO as BytesIO
else:
# Python 2
py3k = False
bytestr = str
unicodestr = unicode
nativestr = bytestr
basestring = basestring
def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given encoding."""
"""Return the given native string as a byte string in the given
encoding.
"""
assert_native(n)
# In Python 2, the native string type is bytes. Assume it's already
# in the given encoding, which for ISO-8859-1 is almost always what
# was intended.
return n
def ntou(n, encoding='ISO-8859-1'):
"""Return the given native string as a unicode string with the given encoding."""
"""Return the given native string as a unicode string with the given
encoding.
"""
assert_native(n)
# In Python 2, the native string type is bytes.
# First, check for the special encoding 'escape'. The test suite uses this
# to signal that it wants to pass a string with embedded \uXXXX escapes,
# but without having to prefix it with u'' for Python 2, but no prefix
# for Python 3.
# First, check for the special encoding 'escape'. The test suite uses
# this to signal that it wants to pass a string with embedded \uXXXX
# escapes, but without having to prefix it with u'' for Python 2,
# but no prefix for Python 3.
if encoding == 'escape':
return unicode(
re.sub(r'\\u([0-9a-zA-Z]{4})',
lambda m: unichr(int(m.group(1), 16)),
n.decode('ISO-8859-1')))
# Assume it's already in the given encoding, which for ISO-8859-1 is almost
# always what was intended.
# Assume it's already in the given encoding, which for ISO-8859-1
# is almost always what was intended.
return n.decode(encoding)
def tonative(n, encoding='ISO-8859-1'):
"""Return the given string as a native string in the given encoding."""
# In Python 2, the native string type is bytes.
if isinstance(n, unicode):
return n.encode(encoding)
return n
try:
# type("")
from cStringIO import StringIO
except ImportError:
# type("")
from StringIO import StringIO
# bytes:
BytesIO = StringIO
try:
set = set
except NameError:
from sets import Set as set
def assert_native(n):
if not isinstance(n, str):
raise TypeError('n must be a native str (got %s)' % type(n).__name__)
try:
# Python 3.1+
@@ -100,29 +99,19 @@ except ImportError:
# the legacy API of base64
from base64 import decodestring as _base64_decodebytes
def base64_decode(n, encoding='ISO-8859-1'):
"""Return the native string base64-decoded (as a native string)."""
if isinstance(n, unicodestr):
if isinstance(n, six.text_type):
b = n.encode(encoding)
else:
b = n
b = _base64_decodebytes(b)
if nativestr is unicodestr:
if str is six.text_type:
return b.decode(encoding)
else:
return b
try:
# Python 2.5+
from hashlib import md5
except ImportError:
from md5 import new as md5
try:
# Python 2.5+
from hashlib import sha1 as sha
except ImportError:
from sha import new as sha
try:
sorted = sorted
@@ -149,16 +138,11 @@ try:
from urllib.request import parse_http_list, parse_keqv_list
except ImportError:
# Python 2
from urlparse import urljoin
from urllib import urlencode, urlopen
from urllib import quote, quote_plus
from urllib import unquote
from urllib2 import parse_http_list, parse_keqv_list
try:
from threading import local as threadlocal
except ImportError:
from cherrypy._cpthreadinglocal import local as threadlocal
from urlparse import urljoin # noqa
from urllib import urlencode, urlopen # noqa
from urllib import quote, quote_plus # noqa
from urllib import unquote # noqa
from urllib2 import parse_http_list, parse_keqv_list # noqa
try:
dict.iteritems
@@ -195,31 +179,34 @@ try:
import builtins
except ImportError:
# Python 2
import __builtin__ as builtins
import __builtin__ as builtins # noqa
try:
# Python 2. We have to do it in this order so Python 2 builds
# Python 2. We try Python 2 first clients on Python 2
# don't try to import the 'http' module from cherrypy.lib
from Cookie import SimpleCookie, CookieError
from httplib import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
from httplib import BadStatusLine, HTTPConnection, IncompleteRead
from httplib import NotConnected
from BaseHTTPServer import BaseHTTPRequestHandler
except ImportError:
# Python 3
from http.cookies import SimpleCookie, CookieError
from http.client import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
from http.server import BaseHTTPRequestHandler
from http.cookies import SimpleCookie, CookieError # noqa
from http.client import BadStatusLine, HTTPConnection, IncompleteRead # noqa
from http.client import NotConnected # noqa
from http.server import BaseHTTPRequestHandler # noqa
try:
# Python 2. We have to do it in this order so Python 2 builds
# don't try to import the 'http' module from cherrypy.lib
from httplib import HTTPSConnection
except ImportError:
# Some platforms don't expose HTTPSConnection, so handle it separately
if six.PY3:
try:
# Python 3
from http.client import HTTPSConnection
except ImportError:
# Some platforms which don't have SSL don't expose HTTPSConnection
HTTPSConnection = None
else:
try:
from httplib import HTTPSConnection
except ImportError:
HTTPSConnection = None
try:
# Python 2
@@ -228,86 +215,68 @@ except NameError:
# Python 3
xrange = range
import threading
if hasattr(threading.Thread, "daemon"):
# Python 2.6+
def get_daemon(t):
return t.daemon
def set_daemon(t, val):
t.daemon = val
else:
def get_daemon(t):
return t.isDaemon()
def set_daemon(t, val):
t.setDaemon(val)
try:
from email.utils import formatdate
def HTTPDate(timeval=None):
return formatdate(timeval, usegmt=True)
except ImportError:
from rfc822 import formatdate as HTTPDate
try:
# Python 3
from urllib.parse import unquote as parse_unquote
def unquote_qs(atom, encoding, errors='strict'):
return parse_unquote(atom.replace('+', ' '), encoding=encoding, errors=errors)
return parse_unquote(
atom.replace('+', ' '),
encoding=encoding,
errors=errors)
except ImportError:
# Python 2
from urllib import unquote as parse_unquote
def unquote_qs(atom, encoding, errors='strict'):
return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors)
try:
# Prefer simplejson, which is usually more advanced than the builtin module.
# Prefer simplejson, which is usually more advanced than the builtin
# module.
import simplejson as json
json_decode = json.JSONDecoder().decode
json_encode = json.JSONEncoder().iterencode
_json_encode = json.JSONEncoder().iterencode
except ImportError:
if py3k:
# Python 3.0: json is part of the standard library,
# but outputs unicode. We need bytes.
if sys.version_info >= (2, 6):
# Python >=2.6 : json is part of the standard library
import json
json_decode = json.JSONDecoder().decode
_json_encode = json.JSONEncoder().iterencode
else:
json = None
def json_decode(s):
raise ValueError('No JSON library is available')
def _json_encode(s):
raise ValueError('No JSON library is available')
finally:
if json and six.PY3:
# The two Python 3 implementations (simplejson/json)
# outputs str. We need bytes.
def json_encode(value):
for chunk in _json_encode(value):
yield chunk.encode('utf8')
elif sys.version_info >= (2, 6):
# Python 2.6: json is part of the standard library
import json
json_decode = json.JSONDecoder().decode
json_encode = json.JSONEncoder().iterencode
else:
json = None
def json_decode(s):
raise ValueError('No JSON library is available')
def json_encode(s):
raise ValueError('No JSON library is available')
json_encode = _json_encode
text_or_bytes = six.text_type, six.binary_type
try:
import cPickle as pickle
except ImportError:
# In Python 2, pickle is a Python version.
# In Python 3, pickle is the sped-up C version.
import pickle
import pickle # noqa
try:
os.urandom(20)
import binascii
def random20():
return binascii.hexlify(os.urandom(20)).decode('ascii')
except (AttributeError, NotImplementedError):
import random
# os.urandom not available until Python 2.4. Fall back to random.random.
def random20():
return sha('%s' % random.random()).hexdigest()
def random20():
return binascii.hexlify(os.urandom(20)).decode('ascii')
try:
from _thread import get_ident as get_thread_ident
except ImportError:
from thread import get_ident as get_thread_ident
from thread import get_ident as get_thread_ident # noqa
try:
# Python 3
@@ -316,3 +285,50 @@ except NameError:
# Python 2
def next(i):
return i.next()
if sys.version_info >= (3, 3):
Timer = threading.Timer
Event = threading.Event
else:
# Python 3.2 and earlier
Timer = threading._Timer
Event = threading._Event
try:
# Python 2.7+
from subprocess import _args_from_interpreter_flags
except ImportError:
def _args_from_interpreter_flags():
"""Tries to reconstruct original interpreter args from sys.flags for Python 2.6
Backported from Python 3.5. Aims to return a list of
command-line arguments reproducing the current
settings in sys.flags and sys.warnoptions.
"""
flag_opt_map = {
'debug': 'd',
# 'inspect': 'i',
# 'interactive': 'i',
'optimize': 'O',
'dont_write_bytecode': 'B',
'no_user_site': 's',
'no_site': 'S',
'ignore_environment': 'E',
'verbose': 'v',
'bytes_warning': 'b',
'quiet': 'q',
'hash_randomization': 'R',
'py3k_warning': '3',
}
args = []
for flag, opt in flag_opt_map.items():
v = getattr(sys.flags, flag)
if v > 0:
if flag == 'hash_randomization':
v = 1 # Handle specification of an exact seed
args.append('-' + opt * v)
for opt in sys.warnoptions:
args.append('-W' + opt)
return args

View File

@@ -46,21 +46,21 @@ To declare global configuration entries, place them in a [global] section.
You may also declare config entries directly on the classes and methods
(page handlers) that make up your CherryPy application via the ``_cp_config``
attribute. For example::
attribute, set with the ``cherrypy.config`` decorator. For example::
@cherrypy.config(**{'tools.gzip.on': True})
class Demo:
_cp_config = {'tools.gzip.on': True}
@cherrypy.expose
@cherrypy.config(**{'request.show_tracebacks': False})
def index(self):
return "Hello world"
index.exposed = True
index._cp_config = {'request.show_tracebacks': False}
.. note::
This behavior is only guaranteed for the default dispatcher.
Other dispatchers may have different restrictions on where
you can attach _cp_config attributes.
you can attach config attributes.
Namespaces
@@ -119,86 +119,102 @@ style) context manager.
"""
import cherrypy
from cherrypy._cpcompat import set, basestring
from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import reprconf
# Deprecated in CherryPy 3.2--remove in 3.3
NamespaceSet = reprconf.NamespaceSet
def merge(base, other):
"""Merge one app config (from a dict, file, or filename) into another.
If the given config is a filename, it will be appended to
the list of files to monitor for "autoreload" changes.
"""
if isinstance(other, basestring):
if isinstance(other, text_or_bytes):
cherrypy.engine.autoreload.files.add(other)
# Load other into base
for section, value_map in reprconf.as_dict(other).items():
if not isinstance(value_map, dict):
raise ValueError(
"Application config must include section headers, but the "
'Application config must include section headers, but the '
"config you tried to merge doesn't have any sections. "
"Wrap your config in another dict with paths as section "
'Wrap your config in another dict with paths as section '
"headers, for example: {'/': config}.")
base.setdefault(section, {}).update(value_map)
class Config(reprconf.Config):
"""The 'global' configuration data for the entire CherryPy process."""
def update(self, config):
"""Update self from a dict, file or filename."""
if isinstance(config, basestring):
if isinstance(config, text_or_bytes):
# Filename
cherrypy.engine.autoreload.files.add(config)
reprconf.Config.update(self, config)
def _apply(self, config):
"""Update self from a dict."""
if isinstance(config.get("global", None), dict):
if isinstance(config.get('global'), dict):
if len(config) > 1:
cherrypy.checker.global_config_contained_paths = True
config = config["global"]
config = config['global']
if 'tools.staticdir.dir' in config:
config['tools.staticdir.section'] = "global"
config['tools.staticdir.section'] = 'global'
reprconf.Config._apply(self, config)
def __call__(self, *args, **kwargs):
@staticmethod
def __call__(*args, **kwargs):
"""Decorator for page handlers to set _cp_config."""
if args:
raise TypeError(
"The cherrypy.config decorator does not accept positional "
"arguments; you must use keyword arguments.")
'The cherrypy.config decorator does not accept positional '
'arguments; you must use keyword arguments.')
def tool_decorator(f):
if not hasattr(f, "_cp_config"):
f._cp_config = {}
for k, v in kwargs.items():
f._cp_config[k] = v
_Vars(f).setdefault('_cp_config', {}).update(kwargs)
return f
return tool_decorator
class _Vars(object):
"""
Adapter that allows setting a default attribute on a function
or class.
"""
def __init__(self, target):
self.target = target
def setdefault(self, key, default):
if not hasattr(self.target, key):
setattr(self.target, key, default)
return getattr(self.target, key)
# Sphinx begin config.environments
Config.environments = environments = {
"staging": {
'engine.autoreload_on': False,
'staging': {
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': False,
'request.show_mismatched_params': False,
},
"production": {
'engine.autoreload_on': False,
},
'production': {
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': False,
'request.show_mismatched_params': False,
'log.screen': False,
},
"embedded": {
},
'embedded': {
# For use with CherryPy embedded in another deployment stack.
'engine.autoreload_on': False,
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': False,
@@ -206,34 +222,35 @@ Config.environments = environments = {
'log.screen': False,
'engine.SIGHUP': None,
'engine.SIGTERM': None,
},
"test_suite": {
'engine.autoreload_on': False,
},
'test_suite': {
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': True,
'request.show_mismatched_params': True,
'log.screen': False,
},
}
},
}
# Sphinx end config.environments
def _server_namespace_handler(k, v):
"""Config handler for the "server" namespace."""
atoms = k.split(".", 1)
atoms = k.split('.', 1)
if len(atoms) > 1:
# Special-case config keys of the form 'server.servername.socket_port'
# to configure additional HTTP servers.
if not hasattr(cherrypy, "servers"):
if not hasattr(cherrypy, 'servers'):
cherrypy.servers = {}
servername, k = atoms
if servername not in cherrypy.servers:
from cherrypy import _cpserver
cherrypy.servers[servername] = _cpserver.Server()
# On by default, but 'on = False' can unsubscribe it (see below).
cherrypy.servers[servername].subscribe()
if k == 'on':
if v:
cherrypy.servers[servername].subscribe()
@@ -243,42 +260,34 @@ def _server_namespace_handler(k, v):
setattr(cherrypy.servers[servername], k, v)
else:
setattr(cherrypy.server, k, v)
Config.namespaces["server"] = _server_namespace_handler
Config.namespaces['server'] = _server_namespace_handler
def _engine_namespace_handler(k, v):
"""Backward compatibility handler for the "engine" namespace."""
"""Config handler for the "engine" namespace."""
engine = cherrypy.engine
if k == 'autoreload_on':
if v:
engine.autoreload.subscribe()
else:
engine.autoreload.unsubscribe()
elif k == 'autoreload_frequency':
engine.autoreload.frequency = v
elif k == 'autoreload_match':
engine.autoreload.match = v
elif k == 'reload_files':
engine.autoreload.files = set(v)
elif k == 'deadlock_poll_freq':
engine.timeout_monitor.frequency = v
elif k == 'SIGHUP':
engine.listeners['SIGHUP'] = set([v])
if k == 'SIGHUP':
engine.subscribe('SIGHUP', v)
elif k == 'SIGTERM':
engine.listeners['SIGTERM'] = set([v])
elif "." in k:
plugin, attrname = k.split(".", 1)
engine.subscribe('SIGTERM', v)
elif '.' in k:
plugin, attrname = k.split('.', 1)
plugin = getattr(engine, plugin)
if attrname == 'on':
if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
plugin.subscribe()
return
elif (not v) and hasattr(getattr(plugin, 'unsubscribe', None), '__call__'):
elif (
(not v) and
hasattr(getattr(plugin, 'unsubscribe', None), '__call__')
):
plugin.unsubscribe()
return
setattr(plugin, attrname, v)
else:
setattr(engine, k, v)
Config.namespaces["engine"] = _engine_namespace_handler
Config.namespaces['engine'] = _engine_namespace_handler
def _tree_namespace_handler(k, v):
@@ -286,10 +295,9 @@ def _tree_namespace_handler(k, v):
if isinstance(v, dict):
for script_name, app in v.items():
cherrypy.tree.graft(app, script_name)
cherrypy.engine.log("Mounted: %s on %s" % (app, script_name or "/"))
msg = 'Mounted: %s on %s' % (app, script_name or '/')
cherrypy.engine.log(msg)
else:
cherrypy.tree.graft(v, v.script_name)
cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/"))
Config.namespaces["tree"] = _tree_namespace_handler
cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/'))
Config.namespaces['tree'] = _tree_namespace_handler

View File

@@ -18,17 +18,43 @@ except AttributeError:
classtype = type
import cherrypy
from cherrypy._cpcompat import set
class PageHandler(object):
"""Callable which sets response.body."""
def __init__(self, callable, *args, **kwargs):
self.callable = callable
self.args = args
self.kwargs = kwargs
def get_args(self):
return cherrypy.serving.request.args
def set_args(self, args):
cherrypy.serving.request.args = args
return cherrypy.serving.request.args
args = property(
get_args,
set_args,
doc='The ordered args should be accessible from post dispatch hooks'
)
def get_kwargs(self):
return cherrypy.serving.request.kwargs
def set_kwargs(self, kwargs):
cherrypy.serving.request.kwargs = kwargs
return cherrypy.serving.request.kwargs
kwargs = property(
get_kwargs,
set_kwargs,
doc='The named kwargs should be accessible from post dispatch hooks'
)
def __call__(self):
try:
return self.callable(*self.args, **self.kwargs)
@@ -54,7 +80,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
2. Too little parameters are passed to the function.
There are 3 sources of parameters to a cherrypy handler.
1. query string parameters are passed as keyword parameters to the handler.
1. query string parameters are passed as keyword parameters to the
handler.
2. body parameters are also passed as keyword parameters.
3. when partial matching occurs, the final path atoms are passed as
positional args.
@@ -65,12 +92,13 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
show_mismatched_params = getattr(
cherrypy.serving.request, 'show_mismatched_params', False)
try:
(args, varargs, varkw, defaults) = inspect.getargspec(callable)
(args, varargs, varkw, defaults) = getargspec(callable)
except TypeError:
if isinstance(callable, object) and hasattr(callable, '__call__'):
(args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
(args, varargs, varkw,
defaults) = getargspec(callable.__call__)
else:
# If it wasn't one of our own types, re-raise
# If it wasn't one of our own types, re-raise
# the original error
raise
@@ -117,15 +145,15 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
# 2. not enough body parameters -> 400
# 3. not enough path parts (partial matches) -> 404
#
# We can't actually tell which case it is,
# We can't actually tell which case it is,
# so I'm raising a 404 because that covers 2/3 of the
# possibilities
#
#
# In the case where the method does not allow body
# arguments it's definitely a 404.
message = None
if show_mismatched_params:
message="Missing parameters: %s" % ",".join(missing_args)
message = 'Missing parameters: %s' % ','.join(missing_args)
raise cherrypy.HTTPError(404, message=message)
# the extra positional arguments come from the path - 404 Not Found
@@ -147,8 +175,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
message = None
if show_mismatched_params:
message="Multiple values for parameters: "\
"%s" % ",".join(multiple_args)
message = 'Multiple values for parameters: '\
'%s' % ','.join(multiple_args)
raise cherrypy.HTTPError(error, message=message)
if not varkw and varkw_usage > 0:
@@ -158,8 +186,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_qs_params:
message = None
if show_mismatched_params:
message="Unexpected query string "\
"parameters: %s" % ", ".join(extra_qs_params)
message = 'Unexpected query string '\
'parameters: %s' % ', '.join(extra_qs_params)
raise cherrypy.HTTPError(404, message=message)
# If there were any extra body parameters, it's a 400 Not Found
@@ -167,8 +195,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_body_params:
message = None
if show_mismatched_params:
message="Unexpected body parameters: "\
"%s" % ", ".join(extra_body_params)
message = 'Unexpected body parameters: '\
'%s' % ', '.join(extra_body_params)
raise cherrypy.HTTPError(400, message=message)
@@ -176,10 +204,16 @@ try:
import inspect
except ImportError:
test_callable_spec = lambda callable, args, kwargs: None
else:
getargspec = inspect.getargspec
# Python 3 requires using getfullargspec if keyword-only arguments are present
if hasattr(inspect, 'getfullargspec'):
def getargspec(callable):
return inspect.getfullargspec(callable)[:4]
class LateParamPageHandler(PageHandler):
"""When passing cherrypy.request.params to the page handler, we do not
want to capture that dict too early; we want to give tools like the
decoding tool a chance to modify the params dict in-between the lookup
@@ -187,16 +221,17 @@ class LateParamPageHandler(PageHandler):
takes that into account, and allows request.params to be 'bound late'
(it's more complicated than that, but that's the effect).
"""
def _get_kwargs(self):
kwargs = cherrypy.serving.request.params.copy()
if self._kwargs:
kwargs.update(self._kwargs)
return kwargs
def _set_kwargs(self, kwargs):
cherrypy.serving.request.kwargs = kwargs
self._kwargs = kwargs
kwargs = property(_get_kwargs, _set_kwargs,
doc='page handler kwargs (with '
'cherrypy.request.params copied in)')
@@ -205,19 +240,24 @@ class LateParamPageHandler(PageHandler):
if sys.version_info < (3, 0):
punctuation_to_underscores = string.maketrans(
string.punctuation, '_' * len(string.punctuation))
def validate_translator(t):
if not isinstance(t, str) or len(t) != 256:
raise ValueError("The translate argument must be a str of len 256.")
raise ValueError(
'The translate argument must be a str of len 256.')
else:
punctuation_to_underscores = str.maketrans(
string.punctuation, '_' * len(string.punctuation))
def validate_translator(t):
if not isinstance(t, dict):
raise ValueError("The translate argument must be a dict.")
raise ValueError('The translate argument must be a dict.')
class Dispatcher(object):
"""CherryPy Dispatcher which walks a tree of objects to find a handler.
The tree is rooted at cherrypy.request.app.root, and each hierarchical
component in the path_info argument is matched to a corresponding nested
attribute of the root object. Matching handlers must have an 'exposed'
@@ -225,16 +265,16 @@ class Dispatcher(object):
matches a URI which ends in a slash ("/"). The special method name
"default" may match a portion of the path_info (but only when no longer
substring of the path_info matches some other object).
This is the default, built-in dispatcher for CherryPy.
"""
dispatch_method_name = '_cp_dispatch'
"""
The name of the dispatch method that nodes may optionally implement
to provide their own dynamic dispatch algorithm.
"""
def __init__(self, dispatch_method_name=None,
translate=punctuation_to_underscores):
validate_translator(translate)
@@ -246,27 +286,27 @@ class Dispatcher(object):
"""Set handler and config for the current request."""
request = cherrypy.serving.request
func, vpath = self.find_handler(path_info)
if func:
# Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath]
vpath = [x.replace('%2F', '/') for x in vpath]
request.handler = LateParamPageHandler(func, *vpath)
else:
request.handler = cherrypy.NotFound()
def find_handler(self, path):
"""Return the appropriate page handler, plus any virtual path.
This will return two objects. The first will be a callable,
which can be used to generate page output. Any parameters from
the query string or request body will be sent to that callable
as keyword arguments.
The callable is found by traversing the application's tree,
starting from cherrypy.request.app.root, and matching path
components to successive objects in the tree. For example, the
URL "/path/to/handler" might return root.path.to.handler.
The second object returned will be a list of names which are
'virtual path' components: parts of the URL which are dynamic,
and were not used when looking up the handler.
@@ -277,25 +317,25 @@ class Dispatcher(object):
app = request.app
root = app.root
dispatch_name = self.dispatch_method_name
# Get config for the root object/path.
fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
fullpath_len = len(fullpath)
segleft = fullpath_len
nodeconf = {}
if hasattr(root, "_cp_config"):
if hasattr(root, '_cp_config'):
nodeconf.update(root._cp_config)
if "/" in app.config:
nodeconf.update(app.config["/"])
if '/' in app.config:
nodeconf.update(app.config['/'])
object_trail = [['root', root, nodeconf, segleft]]
node = root
iternames = fullpath[:]
while iternames:
name = iternames[0]
# map to legal Python identifiers (e.g. replace '.' with '_')
objname = name.translate(self.translate)
nodeconf = {}
subnode = getattr(node, objname, None)
pre_len = len(iternames)
@@ -304,40 +344,40 @@ class Dispatcher(object):
if dispatch and hasattr(dispatch, '__call__') and not \
getattr(dispatch, 'exposed', False) and \
pre_len > 1:
#Don't expose the hidden 'index' token to _cp_dispatch
#We skip this if pre_len == 1 since it makes no sense
#to call a dispatcher when we have no tokens left.
# Don't expose the hidden 'index' token to _cp_dispatch
# We skip this if pre_len == 1 since it makes no sense
# to call a dispatcher when we have no tokens left.
index_name = iternames.pop()
subnode = dispatch(vpath=iternames)
iternames.append(index_name)
else:
#We didn't find a path, but keep processing in case there
#is a default() handler.
# We didn't find a path, but keep processing in case there
# is a default() handler.
iternames.pop(0)
else:
#We found the path, remove the vpath entry
# We found the path, remove the vpath entry
iternames.pop(0)
segleft = len(iternames)
if segleft > pre_len:
#No path segment was removed. Raise an error.
# No path segment was removed. Raise an error.
raise cherrypy.CherryPyException(
"A vpath segment was added. Custom dispatchers may only "
+ "remove elements. While trying to process "
+ "{0} in {1}".format(name, fullpath)
)
'A vpath segment was added. Custom dispatchers may only '
+ 'remove elements. While trying to process '
+ '{0} in {1}'.format(name, fullpath)
)
elif segleft == pre_len:
#Assume that the handler used the current path segment, but
#did not pop it. This allows things like
#return getattr(self, vpath[0], None)
# Assume that the handler used the current path segment, but
# did not pop it. This allows things like
# return getattr(self, vpath[0], None)
iternames.pop(0)
segleft -= 1
node = subnode
if node is not None:
# Get _cp_config attached to this node.
if hasattr(node, "_cp_config"):
if hasattr(node, '_cp_config'):
nodeconf.update(node._cp_config)
# Mix in values from app.config for this path.
existing_len = fullpath_len - pre_len
if existing_len != 0:
@@ -349,43 +389,47 @@ class Dispatcher(object):
curpath += '/' + seg
if curpath in app.config:
nodeconf.update(app.config[curpath])
object_trail.append([name, node, nodeconf, segleft])
def set_conf():
"""Collapse all object_trail config into cherrypy.request.config."""
"""Collapse all object_trail config into cherrypy.request.config.
"""
base = cherrypy.config.copy()
# Note that we merge the config from each node
# even if that node was None.
for name, obj, conf, segleft in object_trail:
base.update(conf)
if 'tools.staticdir.dir' in conf:
base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
base['tools.staticdir.section'] = '/' + \
'/'.join(fullpath[0:fullpath_len - segleft])
return base
# Try successive objects (reverse order)
num_candidates = len(object_trail) - 1
for i in range(num_candidates, -1, -1):
name, candidate, nodeconf, segleft = object_trail[i]
if candidate is None:
continue
# Try a "default" method on the current leaf.
if hasattr(candidate, "default"):
if hasattr(candidate, 'default'):
defhandler = candidate.default
if getattr(defhandler, 'exposed', False):
# Insert any extra _cp_config from the default handler.
conf = getattr(defhandler, "_cp_config", {})
object_trail.insert(i+1, ["default", defhandler, conf, segleft])
conf = getattr(defhandler, '_cp_config', {})
object_trail.insert(
i + 1, ['default', defhandler, conf, segleft])
request.config = set_conf()
# See http://www.cherrypy.org/ticket/613
request.is_index = path.endswith("/")
# See https://github.com/cherrypy/cherrypy/issues/613
request.is_index = path.endswith('/')
return defhandler, fullpath[fullpath_len - segleft:-1]
# Uncomment the next line to restrict positional params to "default".
# Uncomment the next line to restrict positional params to
# "default".
# if i < num_candidates - 2: continue
# Try the current leaf.
if getattr(candidate, 'exposed', False):
request.config = set_conf()
@@ -400,48 +444,49 @@ class Dispatcher(object):
# positional parameters (virtual paths).
request.is_index = False
return candidate, fullpath[fullpath_len - segleft:-1]
# We didn't find anything
request.config = set_conf()
return None, []
class MethodDispatcher(Dispatcher):
"""Additional dispatch based on cherrypy.request.method.upper().
Methods named GET, POST, etc will be called on an exposed class.
The method names must be all caps; the appropriate Allow header
will be output showing all capitalized method names as allowable
HTTP verbs.
Note that the containing class must be exposed, not the methods.
"""
def __call__(self, path_info):
"""Set handler and config for the current request."""
request = cherrypy.serving.request
resource, vpath = self.find_handler(path_info)
if resource:
# Set Allow header
avail = [m for m in dir(resource) if m.isupper()]
if "GET" in avail and "HEAD" not in avail:
avail.append("HEAD")
if 'GET' in avail and 'HEAD' not in avail:
avail.append('HEAD')
avail.sort()
cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
cherrypy.serving.response.headers['Allow'] = ', '.join(avail)
# Find the subhandler
meth = request.method.upper()
func = getattr(resource, meth, None)
if func is None and meth == "HEAD":
func = getattr(resource, "GET", None)
if func is None and meth == 'HEAD':
func = getattr(resource, 'GET', None)
if func:
# Grab any _cp_config on the subhandler.
if hasattr(func, "_cp_config"):
if hasattr(func, '_cp_config'):
request.config.update(func._cp_config)
# Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath]
vpath = [x.replace('%2F', '/') for x in vpath]
request.handler = LateParamPageHandler(func, *vpath)
else:
request.handler = cherrypy.HTTPError(405)
@@ -450,9 +495,10 @@ class MethodDispatcher(Dispatcher):
class RoutesDispatcher(object):
"""A Routes based dispatcher for CherryPy."""
def __init__(self, full_result=False):
def __init__(self, full_result=False, **mapper_options):
"""
Routes dispatcher
@@ -463,16 +509,16 @@ class RoutesDispatcher(object):
import routes
self.full_result = full_result
self.controllers = {}
self.mapper = routes.Mapper()
self.mapper = routes.Mapper(**mapper_options)
self.mapper.controller_scan = self.controllers.keys
def connect(self, name, route, controller, **kwargs):
self.controllers[name] = controller
self.mapper.connect(name, route, controller=name, **kwargs)
def redirect(self, url):
raise cherrypy.HTTPRedirect(url)
def __call__(self, path_info):
"""Set handler and config for the current request."""
func = self.find_handler(path_info)
@@ -480,13 +526,13 @@ class RoutesDispatcher(object):
cherrypy.serving.request.handler = LateParamPageHandler(func)
else:
cherrypy.serving.request.handler = cherrypy.NotFound()
def find_handler(self, path_info):
"""Find the right page handler, and set request.config."""
import routes
request = cherrypy.serving.request
config = routes.request_config()
config.mapper = self.mapper
if hasattr(request, 'wsgi_environ'):
@@ -494,9 +540,9 @@ class RoutesDispatcher(object):
config.host = request.headers.get('Host', None)
config.protocol = request.scheme
config.redirect = self.redirect
result = self.mapper.match(path_info)
config.mapper_dict = result
params = {}
if result:
@@ -505,34 +551,34 @@ class RoutesDispatcher(object):
params.pop('controller', None)
params.pop('action', None)
request.params.update(params)
# Get config for the root object/path.
request.config = base = cherrypy.config.copy()
curpath = ""
curpath = ''
def merge(nodeconf):
if 'tools.staticdir.dir' in nodeconf:
nodeconf['tools.staticdir.section'] = curpath or "/"
nodeconf['tools.staticdir.section'] = curpath or '/'
base.update(nodeconf)
app = request.app
root = app.root
if hasattr(root, "_cp_config"):
if hasattr(root, '_cp_config'):
merge(root._cp_config)
if "/" in app.config:
merge(app.config["/"])
if '/' in app.config:
merge(app.config['/'])
# Mix in values from app.config.
atoms = [x for x in path_info.split("/") if x]
atoms = [x for x in path_info.split('/') if x]
if atoms:
last = atoms.pop()
else:
last = None
for atom in atoms:
curpath = "/".join((curpath, atom))
curpath = '/'.join((curpath, atom))
if curpath in app.config:
merge(app.config[curpath])
handler = None
if result:
controller = result.get('controller')
@@ -541,66 +587,68 @@ class RoutesDispatcher(object):
if isinstance(controller, classtype):
controller = controller()
# Get config from the controller.
if hasattr(controller, "_cp_config"):
if hasattr(controller, '_cp_config'):
merge(controller._cp_config)
action = result.get('action')
if action is not None:
handler = getattr(controller, action, None)
# Get config from the handler
if hasattr(handler, "_cp_config"):
# Get config from the handler
if hasattr(handler, '_cp_config'):
merge(handler._cp_config)
else:
handler = controller
# Do the last path atom here so it can
# override the controller's _cp_config.
if last:
curpath = "/".join((curpath, last))
curpath = '/'.join((curpath, last))
if curpath in app.config:
merge(app.config[curpath])
return handler
def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
from cherrypy.lib import xmlrpcutil
def xmlrpc_dispatch(path_info):
path_info = xmlrpcutil.patched_path(path_info)
return next_dispatcher(path_info)
return xmlrpc_dispatch
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
**domains):
"""
Select a different handler based on the Host header.
This can be useful when running multiple sites within one CP server.
It allows several domains to point to different parts of a single
website structure. For example::
http://www.domain.example -> root
http://www.domain2.example -> root/domain2/
http://www.domain2.example:443 -> root/secure
can be accomplished via the following config::
[/]
request.dispatch = cherrypy.dispatch.VirtualHost(
**{'www.domain2.example': '/domain2',
'www.domain2.example:443': '/secure',
})
next_dispatcher
The next dispatcher object in the dispatch chain.
The VirtualHost dispatcher adds a prefix to the URL and calls
another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
use_x_forwarded_host
If True (the default), any "X-Forwarded-Host"
request header will be used instead of the "Host" header. This
is commonly added by HTTP servers (such as Apache) when proxying.
``**domains``
A dict of {host header value: virtual prefix} pairs.
The incoming "Host" request header is looked up in this dict,
@@ -611,26 +659,27 @@ def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domai
headers may contain the port number.
"""
from cherrypy.lib import httputil
def vhost_dispatch(path_info):
request = cherrypy.serving.request
header = request.headers.get
domain = header('Host', '')
if use_x_forwarded_host:
domain = header("X-Forwarded-Host", domain)
prefix = domains.get(domain, "")
domain = header('X-Forwarded-Host', domain)
prefix = domains.get(domain, '')
if prefix:
path_info = httputil.urljoin(prefix, path_info)
result = next_dispatcher(path_info)
# Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
# Touch up staticdir config. See
# https://github.com/cherrypy/cherrypy/issues/614.
section = request.config.get('tools.staticdir.section')
if section:
section = section[len(prefix):]
request.config['tools.staticdir.section'] = section
return result
return vhost_dispatch

View File

@@ -2,8 +2,9 @@
CherryPy provides (and uses) exceptions for declaring that the HTTP response
should be a status other than the default "200 OK". You can ``raise`` them like
normal Python exceptions. You can also call them and they will raise themselves;
this means you can set an :class:`HTTPError<cherrypy._cperror.HTTPError>`
normal Python exceptions. You can also call them and they will raise
themselves; this means you can set an
:class:`HTTPError<cherrypy._cperror.HTTPError>`
or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the
:attr:`request.handler<cherrypy._cprequest.Request.handler>`.
@@ -21,7 +22,8 @@ POST, however, is neither safe nor idempotent--if you
charge a credit card, you don't want to be charged twice by a redirect!
For this reason, *none* of the 3xx responses permit a user-agent (browser) to
resubmit a POST on redirection without first confirming the action with the user:
resubmit a POST on redirection without first confirming the action with the
user:
===== ================================= ===========
300 Multiple Choices Confirm with the user
@@ -53,14 +55,16 @@ Anticipated HTTP responses
--------------------------
The 'error_page' config namespace can be used to provide custom HTML output for
expected responses (like 404 Not Found). Supply a filename from which the output
will be read. The contents will be interpolated with the values %(status)s,
%(message)s, %(traceback)s, and %(version)s using plain old Python
`string formatting <http://www.python.org/doc/2.6.4/library/stdtypes.html#string-formatting-operations>`_.
expected responses (like 404 Not Found). Supply a filename from which the
output will be read. The contents will be interpolated with the values
%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python
`string formatting <http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_.
::
_cp_config = {'error_page.404': os.path.join(localDir, "static/index.html")}
_cp_config = {
'error_page.404': os.path.join(localDir, "static/index.html")
}
Beginning in version 3.1, you may also provide a function or other callable as
@@ -72,7 +76,8 @@ version arguments that are interpolated into templates::
cherrypy.config.update({'error_page.402': error_page_402})
Also in 3.1, in addition to the numbered error codes, you may also supply
"error_page.default" to handle all codes which do not have their own error_page entry.
"error_page.default" to handle all codes which do not have their own error_page
entry.
@@ -81,8 +86,9 @@ Unanticipated errors
CherryPy also has a generic error handling mechanism: whenever an unanticipated
error occurs in your code, it will call
:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to set
the response status, headers, and body. By default, this is the same output as
:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to
set the response status, headers, and body. By default, this is the same
output as
:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide
some other behavior, you generally replace "request.error_response".
@@ -93,67 +99,83 @@ send an e-mail containing the error::
def handle_error():
cherrypy.response.status = 500
cherrypy.response.body = ["<html><body>Sorry, an error occured</body></html>"]
sendMail('error@domain.com', 'Error in your web app', _cperror.format_exc())
cherrypy.response.body = [
"<html><body>Sorry, an error occured</body></html>"
]
sendMail('error@domain.com',
'Error in your web app',
_cperror.format_exc())
@cherrypy.config(**{'request.error_response': handle_error})
class Root:
_cp_config = {'request.error_response': handle_error}
pass
Note that you have to explicitly set :attr:`response.body <cherrypy._cprequest.Response.body>`
Note that you have to explicitly set
:attr:`response.body <cherrypy._cprequest.Response.body>`
and not simply return an error message as a result.
"""
import contextlib
from cgi import escape as _escape
from sys import exc_info as _exc_info
from traceback import format_exception as _format_exception
from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob, tonative, urljoin as _urljoin
from xml.sax import saxutils
import six
from cherrypy._cpcompat import text_or_bytes, iteritems, ntob
from cherrypy._cpcompat import tonative, urljoin as _urljoin
from cherrypy.lib import httputil as _httputil
class CherryPyException(Exception):
"""A base class for CherryPy exceptions."""
pass
class TimeoutError(CherryPyException):
"""Exception raised when Response.timed_out is detected."""
pass
class InternalRedirect(CherryPyException):
"""Exception raised to switch to the handler for a different URL.
This exception will redirect processing to another path within the site
(without informing the client). Provide the new path as an argument when
raising the exception. Provide any params in the querystring for the new URL.
raising the exception. Provide any params in the querystring for the new
URL.
"""
def __init__(self, path, query_string=""):
def __init__(self, path, query_string=''):
import cherrypy
self.request = cherrypy.serving.request
self.query_string = query_string
if "?" in path:
if '?' in path:
# Separate any params included in the path
path, self.query_string = path.split("?", 1)
path, self.query_string = path.split('?', 1)
# Note that urljoin will "do the right thing" whether url is:
# 1. a URL relative to root (e.g. "/dummy")
# 2. a URL relative to the current path
# Note that any query string will be discarded.
path = _urljoin(self.request.path_info, path)
# Set a 'path' member attribute so that code which traps this
# error can have access to it.
self.path = path
CherryPyException.__init__(self, path, self.query_string)
class HTTPRedirect(CherryPyException):
"""Exception raised when the request should be redirected.
This exception will force a HTTP redirect to the URL or URL's you give it.
The new URL must be passed as the first argument to the Exception,
e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
@@ -162,49 +184,38 @@ class HTTPRedirect(CherryPyException):
If one of the provided URL is a unicode object, it will be encoded
using the default encoding or the one passed in parameter.
There are multiple types of redirect, from which you can select via the
``status`` argument. If you do not provide a ``status`` arg, it defaults to
303 (or 302 if responding with HTTP/1.0).
Examples::
raise cherrypy.HTTPRedirect("")
raise cherrypy.HTTPRedirect("/abs/path", 307)
raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
See :ref:`redirectingpost` for additional caveats.
"""
status = None
"""The integer HTTP status code to emit."""
urls = None
"""The list of URL's to emit."""
encoding = 'utf-8'
"""The encoding when passed urls are not native strings"""
def __init__(self, urls, status=None, encoding=None):
import cherrypy
request = cherrypy.serving.request
if isinstance(urls, basestring):
if isinstance(urls, text_or_bytes):
urls = [urls]
abs_urls = []
for url in urls:
url = tonative(url, encoding or self.encoding)
# Note that urljoin will "do the right thing" whether url is:
# 1. a complete URL with host (e.g. "http://www.example.com/test")
# 2. a URL relative to root (e.g. "/dummy")
# 3. a URL relative to the current path
# Note that any query string in cherrypy.request is discarded.
url = _urljoin(cherrypy.url(), url)
abs_urls.append(url)
self.urls = abs_urls
self.urls = [tonative(url, encoding or self.encoding) for url in urls]
# RFC 2616 indicates a 301 response code fits our goal; however,
# browser support for 301 is quite messy. Do 302/303 instead. See
# http://www.alanflavell.org.uk/www/post-redirect.html
@@ -216,38 +227,41 @@ class HTTPRedirect(CherryPyException):
else:
status = int(status)
if status < 300 or status > 399:
raise ValueError("status must be between 300 and 399.")
raise ValueError('status must be between 300 and 399.')
self.status = status
CherryPyException.__init__(self, abs_urls, status)
CherryPyException.__init__(self, self.urls, status)
def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent self.
"""Modify cherrypy.response status, headers, and body to represent
self.
CherryPy uses this internally, but you can also use it to create an
HTTPRedirect object and set its output without *raising* the exception.
"""
import cherrypy
response = cherrypy.serving.response
response.status = status = self.status
if status in (300, 301, 302, 303, 307):
response.headers['Content-Type'] = "text/html;charset=utf-8"
response.headers['Content-Type'] = 'text/html;charset=utf-8'
# "The ... URI SHOULD be given by the Location field
# in the response."
response.headers['Location'] = self.urls[0]
# "Unless the request method was HEAD, the entity of the response
# SHOULD contain a short hypertext note with a hyperlink to the
# new URI(s)."
msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
301: "This resource has permanently moved to <a href='%s'>%s</a>.",
302: "This resource resides temporarily at <a href='%s'>%s</a>.",
303: "This resource can be found at <a href='%s'>%s</a>.",
307: "This resource has moved temporarily to <a href='%s'>%s</a>.",
}[status]
msgs = [msg % (u, u) for u in self.urls]
response.body = ntob("<br />\n".join(msgs), 'utf-8')
msg = {
300: 'This resource can be found at ',
301: 'This resource has permanently moved to ',
302: 'This resource resides temporarily at ',
303: 'This resource can be found at ',
307: 'This resource has moved temporarily to ',
}[status]
msg += '<a href=%s>%s</a>.'
msgs = [msg % (saxutils.quoteattr(u), u) for u in self.urls]
response.body = ntob('<br />\n'.join(msgs), 'utf-8')
# Previous code may have set C-L, so we have to reset it
# (allow finalize to set it).
response.headers.pop('Content-Length', None)
@@ -256,7 +270,7 @@ class HTTPRedirect(CherryPyException):
# "The response MUST include the following header fields:
# Date, unless its omission is required by section 14.18.1"
# The "Date" header should have been set in Response.__init__
# "...the response SHOULD NOT include other entity-headers."
for key in ('Allow', 'Content-Encoding', 'Content-Language',
'Content-Length', 'Content-Location', 'Content-MD5',
@@ -264,7 +278,7 @@ class HTTPRedirect(CherryPyException):
'Last-Modified'):
if key in response.headers:
del response.headers[key]
# "The 304 response MUST NOT contain a message-body."
response.body = None
# Previous code may have set C-L, so we have to reset it.
@@ -272,13 +286,13 @@ class HTTPRedirect(CherryPyException):
elif status == 305:
# Use Proxy.
# self.urls[0] should be the URI of the proxy.
response.headers['Location'] = self.urls[0]
response.headers['Location'] = ntob(self.urls[0], 'utf-8')
response.body = None
# Previous code may have set C-L, so we have to reset it.
response.headers.pop('Content-Length', None)
else:
raise ValueError("The %s status code is unknown." % status)
raise ValueError('The %s status code is unknown.' % status)
def __call__(self):
"""Use this exception as a request.handler (raise self)."""
raise self
@@ -287,18 +301,18 @@ class HTTPRedirect(CherryPyException):
def clean_headers(status):
"""Remove any headers which should not apply to an error response."""
import cherrypy
response = cherrypy.serving.response
# Remove headers which applied to the original content,
# but do not apply to the error page.
respheaders = response.headers
for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
"Vary", "Content-Encoding", "Content-Length", "Expires",
"Content-Location", "Content-MD5", "Last-Modified"]:
for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After',
'Vary', 'Content-Encoding', 'Content-Length', 'Expires',
'Content-Location', 'Content-MD5', 'Last-Modified']:
if key in respheaders:
del respheaders[key]
if status != 416:
# A server sending a response with status code 416 (Requested
# range not satisfiable) SHOULD include a Content-Range field
@@ -306,93 +320,107 @@ def clean_headers(status):
# specifies the current length of the selected resource.
# A response with status code 206 (Partial Content) MUST NOT
# include a Content-Range field with a byte-range- resp-spec of "*".
if "Content-Range" in respheaders:
del respheaders["Content-Range"]
if 'Content-Range' in respheaders:
del respheaders['Content-Range']
class HTTPError(CherryPyException):
"""Exception used to return an HTTP error code (4xx-5xx) to the client.
This exception can be used to automatically send a response using a http status
code, with an appropriate error page. It takes an optional
This exception can be used to automatically send a response using a
http status code, with an appropriate error page. It takes an optional
``status`` argument (which must be between 400 and 599); it defaults to 500
("Internal Server Error"). It also takes an optional ``message`` argument,
which will be returned in the response body. See
`RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
`RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
for a complete list of available error codes and when to use them.
Examples::
raise cherrypy.HTTPError(403)
raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.")
raise cherrypy.HTTPError(
"403 Forbidden", "You are not allowed to access this resource.")
"""
status = None
"""The HTTP status code. May be of type int or str (with a Reason-Phrase)."""
"""The HTTP status code. May be of type int or str (with a Reason-Phrase).
"""
code = None
"""The integer HTTP status code."""
reason = None
"""The HTTP Reason-Phrase string."""
def __init__(self, status=500, message=None):
self.status = status
try:
self.code, self.reason, defaultmsg = _httputil.valid_status(status)
except ValueError:
raise self.__class__(500, _exc_info()[1].args[0])
if self.code < 400 or self.code > 599:
raise ValueError("status must be between 400 and 599.")
raise ValueError('status must be between 400 and 599.')
# See http://www.python.org/dev/peps/pep-0352/
# self.message = message
self._message = message or defaultmsg
CherryPyException.__init__(self, status, message)
def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent self.
"""Modify cherrypy.response status, headers, and body to represent
self.
CherryPy uses this internally, but you can also use it to create an
HTTPError object and set its output without *raising* the exception.
"""
import cherrypy
response = cherrypy.serving.response
clean_headers(self.code)
# In all cases, finalize will be called after this method,
# so don't bother cleaning up response values here.
response.status = self.status
tb = None
if cherrypy.serving.request.show_tracebacks:
tb = format_exc()
response.headers['Content-Type'] = "text/html;charset=utf-8"
response.headers.pop('Content-Length', None)
content = ntob(self.get_error_page(self.status, traceback=tb,
message=self._message), 'utf-8')
content = self.get_error_page(self.status, traceback=tb,
message=self._message)
response.body = content
_be_ie_unfriendly(self.code)
def get_error_page(self, *args, **kwargs):
return get_error_page(*args, **kwargs)
def __call__(self):
"""Use this exception as a request.handler (raise self)."""
raise self
@classmethod
@contextlib.contextmanager
def handle(cls, exception, status=500, message=''):
"""Translate exception into an HTTPError."""
try:
yield
except exception as exc:
raise cls(status, message or str(exc))
class NotFound(HTTPError):
"""Exception raised when a URL could not be mapped to any handler (404).
This is equivalent to raising
:class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
"""
def __init__(self, path=None):
if path is None:
import cherrypy
@@ -402,7 +430,8 @@ class NotFound(HTTPError):
HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
@@ -425,74 +454,101 @@ _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitiona
<p>%(message)s</p>
<pre id="traceback">%(traceback)s</pre>
<div id="powered_by">
<span>Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
<span>
Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a>
</span>
</div>
</body>
</html>
'''
def get_error_page(status, **kwargs):
"""Return an HTML page, containing a pretty error response.
status should be an int or a str.
kwargs will be interpolated into the page template.
"""
import cherrypy
try:
code, reason, message = _httputil.valid_status(status)
except ValueError:
raise cherrypy.HTTPError(500, _exc_info()[1].args[0])
# We can't use setdefault here, because some
# callers send None for kwarg values.
if kwargs.get('status') is None:
kwargs['status'] = "%s %s" % (code, reason)
kwargs['status'] = '%s %s' % (code, reason)
if kwargs.get('message') is None:
kwargs['message'] = message
if kwargs.get('traceback') is None:
kwargs['traceback'] = ''
if kwargs.get('version') is None:
kwargs['version'] = cherrypy.__version__
for k, v in iteritems(kwargs):
if v is None:
kwargs[k] = ""
kwargs[k] = ''
else:
kwargs[k] = _escape(kwargs[k])
# Use a custom template or callable for the error page?
pages = cherrypy.serving.request.error_page
error_page = pages.get(code) or pages.get('default')
# Default template, can be overridden below.
template = _HTTPErrorTemplate
if error_page:
try:
if hasattr(error_page, '__call__'):
return error_page(**kwargs)
# The caller function may be setting headers manually,
# so we delegate to it completely. We may be returning
# an iterator as well as a string here.
#
# We *must* make sure any content is not unicode.
result = error_page(**kwargs)
if cherrypy.lib.is_iterator(result):
from cherrypy.lib.encoding import UTF8StreamEncoder
return UTF8StreamEncoder(result)
elif isinstance(result, six.text_type):
return result.encode('utf-8')
else:
if not isinstance(result, bytes):
raise ValueError('error page function did not '
'return a bytestring, six.text_typeing or an '
'iterator - returned object of type %s.'
% (type(result).__name__))
return result
else:
data = open(error_page, 'rb').read()
return tonative(data) % kwargs
# Load the template from this path.
template = tonative(open(error_page, 'rb').read())
except:
e = _format_exception(*_exc_info())[-1]
m = kwargs['message']
if m:
m += "<br />"
m += "In addition, the custom error page failed:\n<br />%s" % e
m += '<br />'
m += 'In addition, the custom error page failed:\n<br />%s' % e
kwargs['message'] = m
return _HTTPErrorTemplate % kwargs
response = cherrypy.serving.response
response.headers['Content-Type'] = 'text/html;charset=utf-8'
result = template % kwargs
return result.encode('utf-8')
_ie_friendly_error_sizes = {
400: 512, 403: 256, 404: 512, 405: 256,
406: 512, 408: 512, 409: 512, 410: 256,
500: 512, 501: 512, 505: 512,
}
}
def _be_ie_unfriendly(status):
import cherrypy
response = cherrypy.serving.response
# For some statuses, Internet Explorer 5+ shows "friendly error
# messages" instead of our response.body if the body is smaller
# than a given size. Fix this by returning a body over that size
@@ -508,7 +564,7 @@ def _be_ie_unfriendly(status):
if l and l < s:
# IN ADDITION: the response must be written to IE
# in one chunk or it will still get replaced! Bah.
content = content + (ntob(" ") * (s - l))
content = content + (ntob(' ') * (s - l))
response.body = content
response.headers['Content-Length'] = str(len(content))
@@ -519,38 +575,37 @@ def format_exc(exc=None):
if exc is None:
exc = _exc_info()
if exc == (None, None, None):
return ""
return ''
import traceback
return "".join(traceback.format_exception(*exc))
return ''.join(traceback.format_exception(*exc))
finally:
del exc
def bare_error(extrabody=None):
"""Produce status, headers, body for a critical error.
Returns a triple without calling any other questionable functions,
so it should be as error-free as possible. Call it from an HTTP server
if you get errors outside of the request.
If extrabody is None, a friendly but rather unhelpful error message
is set in the body. If extrabody is a string, it will be appended
as-is to the body.
"""
# The whole point of this function is to be a last line-of-defense
# in handling errors. That is, it must not raise any errors itself;
# it cannot be allowed to fail. Therefore, don't add to it!
# In particular, don't call any other CP functions.
body = ntob("Unrecoverable error in the server.")
body = ntob('Unrecoverable error in the server.')
if extrabody is not None:
if not isinstance(extrabody, bytestr):
if not isinstance(extrabody, bytes):
extrabody = extrabody.encode('utf-8')
body += ntob("\n") + extrabody
return (ntob("500 Internal Server Error"),
body += ntob('\n') + extrabody
return (ntob('500 Internal Server Error'),
[(ntob('Content-Type'), ntob('text/plain')),
(ntob('Content-Length'), ntob(str(len(body)),'ISO-8859-1'))],
(ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))],
[body])

View File

@@ -34,10 +34,11 @@ and another set of rules specific to each application. The global log
manager is found at :func:`cherrypy.log`, and the log manager for each
application is found at :attr:`app.log<cherrypy._cptree.Application.log>`.
If you're inside a request, the latter is reachable from
``cherrypy.request.app.log``; if you're outside a request, you'll have to obtain
a reference to the ``app``: either the return value of
``cherrypy.request.app.log``; if you're outside a request, you'll have to
obtain a reference to the ``app``: either the return value of
:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used
:func:`quickstart()<cherrypy.quickstart>` instead, via ``cherrypy.tree.apps['/']``.
:func:`quickstart()<cherrypy.quickstart>` instead, via
``cherrypy.tree.apps['/']``.
By default, the global logs are named "cherrypy.error" and "cherrypy.access",
and the application logs are named "cherrypy.error.2378745" and
@@ -55,6 +56,13 @@ errors! The format of access messages is highly formalized, but the error log
isn't--it receives messages from a variety of sources (including full error
tracebacks, if enabled).
If you are logging the access log and error log to the same source, then there
is a possibility that a specially crafted error message may replicate an access
log message as described in CWE-117. In this case it is the application
developer's responsibility to manually escape data before using CherryPy's log()
functionality, or they may create an application that is vulnerable to CWE-117.
This would be achieved by using a custom handler escape any special characters,
and attached as described below.
Custom Handlers
===============
@@ -69,21 +77,21 @@ and uses a RotatingFileHandler instead:
#python
log = app.log
# Remove the default FileHandlers if present.
log.error_file = ""
log.access_file = ""
maxBytes = getattr(log, "rot_maxBytes", 10000000)
backupCount = getattr(log, "rot_backupCount", 1000)
# Make a new RotatingFileHandler for the error log.
fname = getattr(log, "rot_error_file", "error.log")
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
h.setLevel(DEBUG)
h.setFormatter(_cplogging.logfmt)
log.error_log.addHandler(h)
# Make a new RotatingFileHandler for the access log.
fname = getattr(log, "rot_access_file", "access.log")
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
@@ -101,18 +109,23 @@ the "log.error_file" config entry, for example).
import datetime
import logging
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
logging.Logger.manager.emittedNoHandlerWarning = 1
logfmt = logging.Formatter("%(message)s")
import os
import sys
import six
import cherrypy
from cherrypy import _cperror
from cherrypy._cpcompat import ntob, py3k
from cherrypy._cpcompat import ntob
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
logging.Logger.manager.emittedNoHandlerWarning = 1
logfmt = logging.Formatter('%(message)s')
class NullHandler(logging.Handler):
"""A no-op logging handler to silence the logging.lastResort handler."""
def handle(self, record):
@@ -126,48 +139,50 @@ class NullHandler(logging.Handler):
class LogManager(object):
"""An object to assist both simple and advanced logging.
``cherrypy.log`` is an instance of this class.
"""
appid = None
"""The id() of the Application object which owns this log manager. If this
is a global log manager, appid is None."""
error_log = None
"""The actual :class:`logging.Logger` instance for error messages."""
access_log = None
"""The actual :class:`logging.Logger` instance for access messages."""
if py3k:
access_log_format = \
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
else:
access_log_format = \
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
access_log_format = (
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
if six.PY3 else
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
)
logger_root = None
"""The "top-level" logger name.
This string will be used as the first segment in the Logger names.
The default is "cherrypy", for example, in which case the Logger names
will be of the form::
cherrypy.error.<appid>
cherrypy.access.<appid>
"""
def __init__(self, appid=None, logger_root="cherrypy"):
def __init__(self, appid=None, logger_root='cherrypy'):
self.logger_root = logger_root
self.appid = appid
if appid is None:
self.error_log = logging.getLogger("%s.error" % logger_root)
self.access_log = logging.getLogger("%s.access" % logger_root)
self.error_log = logging.getLogger('%s.error' % logger_root)
self.access_log = logging.getLogger('%s.access' % logger_root)
else:
self.error_log = logging.getLogger("%s.error.%s" % (logger_root, appid))
self.access_log = logging.getLogger("%s.access.%s" % (logger_root, appid))
self.error_log = logging.getLogger(
'%s.error.%s' % (logger_root, appid))
self.access_log = logging.getLogger(
'%s.access.%s' % (logger_root, appid))
self.error_log.setLevel(logging.INFO)
self.access_log.setLevel(logging.INFO)
@@ -186,34 +201,38 @@ class LogManager(object):
h.stream.close()
h.stream = open(h.baseFilename, h.mode)
h.release()
def error(self, msg='', context='', severity=logging.INFO, traceback=False):
def error(self, msg='', context='', severity=logging.INFO,
traceback=False):
"""Write the given ``msg`` to the error log.
This is not just for errors! Applications may call this at any time
to log application-specific information.
If ``traceback`` is True, the traceback of the current exception
(if any) will be appended to ``msg``.
"""
exc_info = None
if traceback:
msg += _cperror.format_exc()
self.error_log.log(severity, ' '.join((self.time(), context, msg)))
exc_info = _cperror._exc_info()
self.error_log.log(severity, ' '.join((self.time(), context, msg)), exc_info=exc_info)
def __call__(self, *args, **kwargs):
"""An alias for ``error``."""
return self.error(*args, **kwargs)
def access(self):
"""Write to the access log (in Apache/NCSA Combined Log format).
See http://httpd.apache.org/docs/2.0/logs.html#combined for format
details.
See the
`apache documentation <http://httpd.apache.org/docs/current/logs.html#combined>`_
for format details.
CherryPy calls this automatically for you. Note there are no arguments;
it collects the data itself from
:class:`cherrypy.request<cherrypy._cprequest.Request>`.
Like Apache started doing in 2.0.46, non-printable and other special
characters in %r (and we expand that to all parts) are escaped using
\\xhh sequences, where hh stands for the hexadecimal representation
@@ -227,23 +246,24 @@ class LogManager(object):
outheaders = response.headers
inheaders = request.headers
if response.output_status is None:
status = "-"
status = '-'
else:
status = response.output_status.split(ntob(" "), 1)[0]
if py3k:
status = response.output_status.split(ntob(' '), 1)[0]
if six.PY3:
status = status.decode('ISO-8859-1')
atoms = {'h': remote.name or remote.ip,
'l': '-',
'u': getattr(request, "login", None) or "-",
'u': getattr(request, 'login', None) or '-',
't': self.time(),
'r': request.request_line,
's': status,
'b': dict.get(outheaders, 'Content-Length', '') or "-",
'b': dict.get(outheaders, 'Content-Length', '') or '-',
'f': dict.get(inheaders, 'Referer', ''),
'a': dict.get(inheaders, 'User-Agent', ''),
'o': dict.get(inheaders, 'Host', '-'),
}
if py3k:
if six.PY3:
for k, v in atoms.items():
if not isinstance(v, str):
v = str(v)
@@ -251,22 +271,23 @@ class LogManager(object):
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
# and backslash for us. All we have to do is strip the quotes.
v = repr(v)[2:-1]
# in python 3.0 the repr of bytes (as returned by encode)
# in python 3.0 the repr of bytes (as returned by encode)
# uses double \'s. But then the logger escapes them yet, again
# resulting in quadruple slashes. Remove the extra one here.
v = v.replace('\\\\', '\\')
# Escape double-quote.
atoms[k] = v
try:
self.access_log.log(logging.INFO, self.access_log_format.format(**atoms))
self.access_log.log(
logging.INFO, self.access_log_format.format(**atoms))
except:
self(traceback=True)
else:
for k, v in atoms.items():
if isinstance(v, unicode):
if isinstance(v, six.text_type):
v = v.encode('utf8')
elif not isinstance(v, str):
v = str(v)
@@ -275,12 +296,13 @@ class LogManager(object):
v = repr(v)[1:-1]
# Escape double-quote.
atoms[k] = v.replace('"', '\\"')
try:
self.access_log.log(logging.INFO, self.access_log_format % atoms)
self.access_log.log(
logging.INFO, self.access_log_format % atoms)
except:
self(traceback=True)
def time(self):
"""Return now() in Apache Common Log Format (no timezone)."""
now = datetime.datetime.now()
@@ -289,53 +311,51 @@ class LogManager(object):
month = monthnames[now.month - 1].capitalize()
return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
(now.day, month, now.year, now.hour, now.minute, now.second))
def _get_builtin_handler(self, log, key):
for h in log.handlers:
if getattr(h, "_cpbuiltin", None) == key:
if getattr(h, '_cpbuiltin', None) == key:
return h
# ------------------------- Screen handlers ------------------------- #
def _set_screen_handler(self, log, enable, stream=None):
h = self._get_builtin_handler(log, "screen")
h = self._get_builtin_handler(log, 'screen')
if enable:
if not h:
if stream is None:
stream=sys.stderr
stream = sys.stderr
h = logging.StreamHandler(stream)
h.setFormatter(logfmt)
h._cpbuiltin = "screen"
h._cpbuiltin = 'screen'
log.addHandler(h)
elif h:
log.handlers.remove(h)
def _get_screen(self):
h = self._get_builtin_handler
has_h = h(self.error_log, "screen") or h(self.access_log, "screen")
has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen')
return bool(has_h)
def _set_screen(self, newvalue):
self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
screen = property(_get_screen, _set_screen,
doc="""Turn stderr/stdout logging on or off.
doc="""Turn stderr/stdout logging on or off.
If you set this to True, it'll add the appropriate StreamHandler for
you. If you set it to False, it will remove the handler.
""")
# -------------------------- File handlers -------------------------- #
def _add_builtin_file_handler(self, log, fname):
h = logging.FileHandler(fname)
h.setFormatter(logfmt)
h._cpbuiltin = "file"
h._cpbuiltin = 'file'
log.addHandler(h)
def _set_file_handler(self, log, filename):
h = self._get_builtin_handler(log, "file")
h = self._get_builtin_handler(log, 'file')
if filename:
if h:
if h.baseFilename != os.path.abspath(filename):
@@ -348,56 +368,58 @@ class LogManager(object):
if h:
h.close()
log.handlers.remove(h)
def _get_error_file(self):
h = self._get_builtin_handler(self.error_log, "file")
h = self._get_builtin_handler(self.error_log, 'file')
if h:
return h.baseFilename
return ''
def _set_error_file(self, newvalue):
self._set_file_handler(self.error_log, newvalue)
error_file = property(_get_error_file, _set_error_file,
doc="""The filename for self.error_log.
doc="""The filename for self.error_log.
If you set this to a string, it'll add the appropriate FileHandler for
you. If you set it to ``None`` or ``''``, it will remove the handler.
""")
def _get_access_file(self):
h = self._get_builtin_handler(self.access_log, "file")
h = self._get_builtin_handler(self.access_log, 'file')
if h:
return h.baseFilename
return ''
def _set_access_file(self, newvalue):
self._set_file_handler(self.access_log, newvalue)
access_file = property(_get_access_file, _set_access_file,
doc="""The filename for self.access_log.
doc="""The filename for self.access_log.
If you set this to a string, it'll add the appropriate FileHandler for
you. If you set it to ``None`` or ``''``, it will remove the handler.
""")
# ------------------------- WSGI handlers ------------------------- #
def _set_wsgi_handler(self, log, enable):
h = self._get_builtin_handler(log, "wsgi")
h = self._get_builtin_handler(log, 'wsgi')
if enable:
if not h:
h = WSGIErrorHandler()
h.setFormatter(logfmt)
h._cpbuiltin = "wsgi"
h._cpbuiltin = 'wsgi'
log.addHandler(h)
elif h:
log.handlers.remove(h)
def _get_wsgi(self):
return bool(self._get_builtin_handler(self.error_log, "wsgi"))
return bool(self._get_builtin_handler(self.error_log, 'wsgi'))
def _set_wsgi(self, newvalue):
self._set_wsgi_handler(self.error_log, newvalue)
wsgi = property(_get_wsgi, _set_wsgi,
doc="""Write errors to wsgi.errors.
doc="""Write errors to wsgi.errors.
If you set this to True, it'll add the appropriate
:class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
(which writes errors to ``wsgi.errors``).
@@ -406,8 +428,9 @@ class LogManager(object):
class WSGIErrorHandler(logging.Handler):
"A handler class which writes logging records to environ['wsgi.errors']."
def flush(self):
"""Flushes the stream."""
try:
@@ -416,7 +439,7 @@ class WSGIErrorHandler(logging.Handler):
pass
else:
stream.flush()
def emit(self, record):
"""Emit a record."""
try:
@@ -426,15 +449,16 @@ class WSGIErrorHandler(logging.Handler):
else:
try:
msg = self.format(record)
fs = "%s\n"
fs = '%s\n'
import types
if not hasattr(types, "UnicodeType"): #if no unicode support...
# if no unicode support...
if not hasattr(types, 'UnicodeType'):
stream.write(fs % msg)
else:
try:
stream.write(fs % msg)
except UnicodeError:
stream.write(fs % msg.encode("UTF-8"))
stream.write(fs % msg.encode('UTF-8'))
self.flush()
except:
self.handleError(record)

View File

@@ -35,12 +35,12 @@ Listen 8080
LoadModule python_module /usr/lib/apache2/modules/mod_python.so
<Location "/">
PythonPath "sys.path+['/path/to/my/application']"
SetHandler python-program
PythonHandler cherrypy._cpmodpy::handler
PythonOption cherrypy.setup myapp::setup_server
PythonDebug On
</Location>
PythonPath "sys.path+['/path/to/my/application']"
SetHandler python-program
PythonHandler cherrypy._cpmodpy::handler
PythonOption cherrypy.setup myapp::setup_server
PythonDebug On
</Location>
# End
The actual path to your mod_python.so is dependent on your
@@ -55,11 +55,14 @@ resides in the global site-package this won't be needed.
Then restart apache2 and access http://127.0.0.1:8080
"""
import io
import logging
import os
import re
import sys
import cherrypy
from cherrypy._cpcompat import BytesIO, copyitems, ntob
from cherrypy._cpcompat import copyitems, ntob
from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import httputil
@@ -67,11 +70,11 @@ from cherrypy.lib import httputil
# ------------------------------ Request-handling
def setup(req):
from mod_python import apache
# Run any setup functions defined by a "PythonOption cherrypy.setup" directive.
# Run any setup functions defined by a "PythonOption cherrypy.setup"
# directive.
options = req.get_options()
if 'cherrypy.setup' in options:
for function in options['cherrypy.setup'].split():
@@ -83,20 +86,20 @@ def setup(req):
mod = __import__(modname, globals(), locals(), [fname])
func = getattr(mod, fname)
func()
cherrypy.config.update({'log.screen': False,
"tools.ignore_headers.on": True,
"tools.ignore_headers.headers": ['Range'],
'tools.ignore_headers.on': True,
'tools.ignore_headers.headers': ['Range'],
})
engine = cherrypy.engine
if hasattr(engine, "signal_handler"):
if hasattr(engine, 'signal_handler'):
engine.signal_handler.unsubscribe()
if hasattr(engine, "console_control_handler"):
if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.unsubscribe()
engine.autoreload.unsubscribe()
cherrypy.server.unsubscribe()
def _log(msg, level):
newlevel = apache.APLOG_ERR
if logging.DEBUG >= level:
@@ -106,13 +109,13 @@ def setup(req):
elif logging.WARNING >= level:
newlevel = apache.APLOG_WARNING
# On Windows, req.server is required or the msg will vanish. See
# http://www.modpython.org/pipermail/mod_python/2003-October/014291.html.
# http://www.modpython.org/pipermail/mod_python/2003-October/014291.html
# Also, "When server is not specified...LogLevel does not apply..."
apache.log_error(msg, newlevel, req.server)
engine.subscribe('log', _log)
engine.start()
def cherrypy_cleanup(data):
engine.exit()
try:
@@ -124,6 +127,7 @@ def setup(req):
class _ReadOnlyRequest:
expose = ('read', 'readline', 'readlines')
def __init__(self, req):
for method in self.expose:
self.__dict__[method] = getattr(req, method)
@@ -132,6 +136,8 @@ class _ReadOnlyRequest:
recursive = False
_isSetUp = False
def handler(req):
from mod_python import apache
try:
@@ -139,16 +145,18 @@ def handler(req):
if not _isSetUp:
setup(req)
_isSetUp = True
# Obtain a Request object from CherryPy
local = req.connection.local_addr
local = httputil.Host(local[0], local[1], req.connection.local_host or "")
local = httputil.Host(
local[0], local[1], req.connection.local_host or '')
remote = req.connection.remote_addr
remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "")
remote = httputil.Host(
remote[0], remote[1], req.connection.remote_host or '')
scheme = req.parsed_uri[0] or 'http'
req.get_basic_auth_pw()
try:
# apache.mpm_query only became available in mod_python 3.1
q = apache.mpm_query
@@ -157,48 +165,48 @@ def handler(req):
except AttributeError:
bad_value = ("You must provide a PythonOption '%s', "
"either 'on' or 'off', when running a version "
"of mod_python < 3.1")
'of mod_python < 3.1')
threaded = options.get('multithread', '').lower()
if threaded == 'on':
threaded = True
elif threaded == 'off':
threaded = False
else:
raise ValueError(bad_value % "multithread")
raise ValueError(bad_value % 'multithread')
forked = options.get('multiprocess', '').lower()
if forked == 'on':
forked = True
elif forked == 'off':
forked = False
else:
raise ValueError(bad_value % "multiprocess")
sn = cherrypy.tree.script_name(req.uri or "/")
raise ValueError(bad_value % 'multiprocess')
sn = cherrypy.tree.script_name(req.uri or '/')
if sn is None:
send_response(req, '404 Not Found', [], '')
else:
app = cherrypy.tree.apps[sn]
method = req.method
path = req.uri
qs = req.args or ""
qs = req.args or ''
reqproto = req.protocol
headers = copyitems(req.headers_in)
rfile = _ReadOnlyRequest(req)
prev = None
try:
redirections = []
while True:
request, response = app.get_serving(local, remote, scheme,
"HTTP/1.1")
'HTTP/1.1')
request.login = req.user
request.multithread = bool(threaded)
request.multiprocess = bool(forked)
request.app = app
request.prev = prev
# Run the CherryPy Request object and obtain the response
try:
request.run(method, path, qs, reqproto, headers, rfile)
@@ -207,25 +215,28 @@ def handler(req):
ir = sys.exc_info()[1]
app.release_serving()
prev = request
if not recursive:
if ir.path in redirections:
raise RuntimeError("InternalRedirector visited the "
"same URL twice: %r" % ir.path)
raise RuntimeError(
'InternalRedirector visited the same URL '
'twice: %r' % ir.path)
else:
# Add the *previous* path_info + qs to redirections.
# Add the *previous* path_info + qs to
# redirections.
if qs:
qs = "?" + qs
qs = '?' + qs
redirections.append(sn + path + qs)
# Munge environment and try again.
method = "GET"
method = 'GET'
path = ir.path
qs = ir.query_string
rfile = BytesIO()
send_response(req, response.output_status, response.header_list,
response.body, response.stream)
rfile = io.BytesIO()
send_response(
req, response.output_status, response.header_list,
response.body, response.stream)
finally:
app.release_serving()
except:
@@ -239,35 +250,31 @@ def handler(req):
def send_response(req, status, headers, body, stream=False):
# Set response status
req.status = int(status[:3])
# Set response headers
req.content_type = "text/plain"
req.content_type = 'text/plain'
for header, value in headers:
if header.lower() == 'content-type':
req.content_type = value
continue
req.headers_out.add(header, value)
if stream:
# Flush now so the status and headers are sent immediately.
req.flush()
# Set response body
if isinstance(body, basestring):
if isinstance(body, text_or_bytes):
req.write(body)
else:
for seg in body:
req.write(seg)
# --------------- Startup tools for CherryPy + mod_python --------------- #
import os
import re
try:
import subprocess
def popen(fullcmd):
p = subprocess.Popen(fullcmd, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
@@ -279,13 +286,17 @@ except ImportError:
return pipeout
def read_process(cmd, args=""):
fullcmd = "%s %s" % (cmd, args)
def read_process(cmd, args=''):
fullcmd = '%s %s' % (cmd, args)
pipeout = popen(fullcmd)
try:
firstline = pipeout.readline()
if (re.search(ntob("(not recognized|No such file|not found)"), firstline,
re.IGNORECASE)):
cmd_not_found = re.search(
ntob('(not recognized|No such file|not found)'),
firstline,
re.IGNORECASE
)
if cmd_not_found:
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
@@ -294,7 +305,7 @@ def read_process(cmd, args=""):
class ModPythonServer(object):
template = """
# Apache2 server configuration file for running CherryPy with mod_python.
@@ -309,36 +320,35 @@ LoadModule python_module modules/mod_python.so
%(opts)s
</Location>
"""
def __init__(self, loc="/", port=80, opts=None, apache_path="apache",
handler="cherrypy._cpmodpy::handler"):
def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
handler='cherrypy._cpmodpy::handler'):
self.loc = loc
self.port = port
self.opts = opts
self.apache_path = apache_path
self.handler = handler
def start(self):
opts = "".join([" PythonOption %s %s\n" % (k, v)
opts = ''.join([' PythonOption %s %s\n' % (k, v)
for k, v in self.opts])
conf_data = self.template % {"port": self.port,
"loc": self.loc,
"opts": opts,
"handler": self.handler,
conf_data = self.template % {'port': self.port,
'loc': self.loc,
'opts': opts,
'handler': self.handler,
}
mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf')
f = open(mpconf, 'wb')
try:
f.write(conf_data)
finally:
f.close()
response = read_process(self.apache_path, "-k start -f %s" % mpconf)
response = read_process(self.apache_path, '-k start -f %s' % mpconf)
self.ready = True
return response
def stop(self):
os.popen("apache -k stop")
self.ready = False
def stop(self):
os.popen('apache -k stop')
self.ready = False

View File

@@ -2,75 +2,79 @@
import logging
import sys
import io
import cherrypy
from cherrypy._cpcompat import BytesIO
from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import httputil
from cherrypy import wsgiserver
class NativeGateway(wsgiserver.Gateway):
recursive = False
def respond(self):
req = self.req
try:
# Obtain a Request object from CherryPy
local = req.server.bind_addr
local = httputil.Host(local[0], local[1], "")
local = httputil.Host(local[0], local[1], '')
remote = req.conn.remote_addr, req.conn.remote_port
remote = httputil.Host(remote[0], remote[1], "")
remote = httputil.Host(remote[0], remote[1], '')
scheme = req.scheme
sn = cherrypy.tree.script_name(req.uri or "/")
sn = cherrypy.tree.script_name(req.uri or '/')
if sn is None:
self.send_response('404 Not Found', [], [''])
else:
app = cherrypy.tree.apps[sn]
method = req.method
path = req.path
qs = req.qs or ""
qs = req.qs or ''
headers = req.inheaders.items()
rfile = req.rfile
prev = None
try:
redirections = []
while True:
request, response = app.get_serving(
local, remote, scheme, "HTTP/1.1")
local, remote, scheme, 'HTTP/1.1')
request.multithread = True
request.multiprocess = False
request.app = app
request.prev = prev
# Run the CherryPy Request object and obtain the response
# Run the CherryPy Request object and obtain the
# response
try:
request.run(method, path, qs, req.request_protocol, headers, rfile)
request.run(method, path, qs,
req.request_protocol, headers, rfile)
break
except cherrypy.InternalRedirect:
ir = sys.exc_info()[1]
app.release_serving()
prev = request
if not self.recursive:
if ir.path in redirections:
raise RuntimeError("InternalRedirector visited the "
"same URL twice: %r" % ir.path)
raise RuntimeError(
'InternalRedirector visited the same '
'URL twice: %r' % ir.path)
else:
# Add the *previous* path_info + qs to redirections.
# Add the *previous* path_info + qs to
# redirections.
if qs:
qs = "?" + qs
qs = '?' + qs
redirections.append(sn + path + qs)
# Munge environment and try again.
method = "GET"
method = 'GET'
path = ir.path
qs = ir.query_string
rfile = BytesIO()
rfile = io.BytesIO()
self.send_response(
response.output_status, response.header_list,
response.body)
@@ -78,59 +82,62 @@ class NativeGateway(wsgiserver.Gateway):
app.release_serving()
except:
tb = format_exc()
#print tb
# print tb
cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
s, h, b = bare_error()
self.send_response(s, h, b)
def send_response(self, status, headers, body):
req = self.req
# Set response status
req.status = str(status or "500 Server Error")
req.status = str(status or '500 Server Error')
# Set response headers
for header, value in headers:
req.outheaders.append((header, value))
if (req.ready and not req.sent_headers):
req.sent_headers = True
req.send_headers()
# Set response body
for seg in body:
req.write(seg)
class CPHTTPServer(wsgiserver.HTTPServer):
"""Wrapper for wsgiserver.HTTPServer.
wsgiserver has been designed to not reference CherryPy in any way,
so that it can be used in other frameworks and applications.
Therefore, we wrap it here, so we can apply some attributes
from config -> cherrypy.server -> HTTPServer.
"""
def __init__(self, server_adapter=cherrypy.server):
self.server_adapter = server_adapter
server_name = (self.server_adapter.socket_host or
self.server_adapter.socket_file or
None)
wsgiserver.HTTPServer.__init__(
self, server_adapter.bind_addr, NativeGateway,
minthreads=server_adapter.thread_pool,
maxthreads=server_adapter.thread_pool_max,
server_name=server_name)
self.max_request_header_size = self.server_adapter.max_request_header_size or 0
self.max_request_body_size = self.server_adapter.max_request_body_size or 0
self.max_request_header_size = (
self.server_adapter.max_request_header_size or 0)
self.max_request_body_size = (
self.server_adapter.max_request_body_size or 0)
self.request_queue_size = self.server_adapter.socket_queue_size
self.timeout = self.server_adapter.socket_timeout
self.shutdown_timeout = self.server_adapter.shutdown_timeout
self.protocol = self.server_adapter.protocol_version
self.nodelay = self.server_adapter.nodelay
ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
if self.server_adapter.ssl_context:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
@@ -145,5 +152,3 @@ class CPHTTPServer(wsgiserver.HTTPServer):
self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain)

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
"""Manage HTTP servers with CherryPy."""
import warnings
import six
import cherrypy
from cherrypy.lib import attributes
from cherrypy._cpcompat import basestring, py3k
from cherrypy.lib.reprconf import attributes
from cherrypy._cpcompat import text_or_bytes
# We import * because we want to export check_port
# et al as attributes of this module.
@@ -12,112 +12,129 @@ from cherrypy.process.servers import *
class Server(ServerAdapter):
"""An adapter for an HTTP server.
You can set attributes (like socket_host and socket_port)
on *this* object (which is probably cherrypy.server), and call
quickstart. For example::
cherrypy.server.socket_port = 80
cherrypy.quickstart()
"""
socket_port = 8080
"""The TCP port on which to listen for connections."""
_socket_host = '127.0.0.1'
def _get_socket_host(self):
return self._socket_host
def _set_socket_host(self, value):
if value == '':
raise ValueError("The empty string ('') is not an allowed value. "
"Use '0.0.0.0' instead to listen on all active "
"interfaces (INADDR_ANY).")
'interfaces (INADDR_ANY).')
self._socket_host = value
socket_host = property(_get_socket_host, _set_socket_host,
socket_host = property(
_get_socket_host,
_set_socket_host,
doc="""The hostname or IP address on which to listen for connections.
Host values may be any IPv4 or IPv6 address, or any valid hostname.
The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
your hosts file prefers IPv6). The string '0.0.0.0' is a special
IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
is the similar IN6ADDR_ANY for IPv6. The empty string or None are
not allowed.""")
socket_file = None
"""If given, the name of the UNIX socket to use instead of TCP/IP.
When this option is not None, the `socket_host` and `socket_port` options
are ignored."""
socket_queue_size = 5
"""The 'backlog' argument to socket.listen(); specifies the maximum number
of queued connections (default 5)."""
socket_timeout = 10
"""The timeout in seconds for accepted connections (default 10)."""
accepted_queue_size = -1
"""The maximum number of requests which will be queued up before
the server refuses to accept it (default -1, meaning no limit)."""
accepted_queue_timeout = 10
"""The timeout in seconds for attempting to add a request to the
queue when the queue is full (default 10)."""
shutdown_timeout = 5
"""The time to wait for HTTP worker threads to clean up."""
protocol_version = 'HTTP/1.1'
"""The version string to write in the Status-Line of all HTTP responses,
for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
this should also limit the supported features used in the response."""
thread_pool = 10
"""The number of worker threads to start up in the pool."""
thread_pool_max = -1
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit."""
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit.
"""
max_request_header_size = 500 * 1024
"""The maximum number of bytes allowable in the request headers. If exceeded,
the HTTP server should return "413 Request Entity Too Large"."""
"""The maximum number of bytes allowable in the request headers.
If exceeded, the HTTP server should return "413 Request Entity Too Large".
"""
max_request_body_size = 100 * 1024 * 1024
"""The maximum number of bytes allowable in the request body. If exceeded,
the HTTP server should return "413 Request Entity Too Large"."""
instance = None
"""If not None, this should be an HTTP server instance (such as
CPWSGIServer) which cherrypy.server will control. Use this when you need
more control over object instantiation than is available in the various
configuration options."""
ssl_context = None
"""When using PyOpenSSL, an instance of SSL.Context."""
ssl_certificate = None
"""The filename of the SSL certificate to use."""
ssl_certificate_chain = None
"""When using PyOpenSSL, the certificate chain to pass to
Context.load_verify_locations."""
ssl_private_key = None
"""The filename of the private key to use with SSL."""
if py3k:
if six.PY3:
ssl_module = 'builtin'
"""The name of a registered SSL adaptation module to use with the builtin
WSGI server. Builtin options are: 'builtin' (to use the SSL library built
into recent versions of Python). You may also register your
own classes in the wsgiserver.ssl_adapters dict."""
"""The name of a registered SSL adaptation module to use with
the builtin WSGI server. Builtin options are: 'builtin' (to
use the SSL library built into recent versions of Python).
You may also register your own classes in the
wsgiserver.ssl_adapters dict."""
else:
ssl_module = 'pyopenssl'
"""The name of a registered SSL adaptation module to use with the builtin
WSGI server. Builtin options are 'builtin' (to use the SSL library built
into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL
project, which you must install separately). You may also register your
own classes in the wsgiserver.ssl_adapters dict."""
"""The name of a registered SSL adaptation module to use with the
builtin WSGI server. Builtin options are 'builtin' (to use the SSL
library built into recent versions of Python) and 'pyopenssl' (to
use the PyOpenSSL project, which you must install separately). You
may also register your own classes in the wsgiserver.ssl_adapters
dict."""
statistics = False
"""Turns statistics-gathering on or off for aware HTTP servers."""
nodelay = True
"""If True (the default since 3.1), sets the TCP_NODELAY socket option."""
wsgi_version = (1, 0)
"""The WSGI version tuple to use with the builtin WSGI server.
The provided options are (1, 0) [which includes support for PEP 3333,
@@ -125,13 +142,13 @@ class Server(ServerAdapter):
wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
You may create and register your own experimental versions of the WSGI
protocol by adding custom classes to the wsgiserver.wsgi_gateways dict."""
def __init__(self):
self.bus = cherrypy.engine
self.httpserver = None
self.interrupt = None
self.running = False
def httpserver_from_self(self, httpserver=None):
"""Return a (httpserver, bind_addr) pair based on self attributes."""
if httpserver is None:
@@ -139,30 +156,31 @@ class Server(ServerAdapter):
if httpserver is None:
from cherrypy import _cpwsgi_server
httpserver = _cpwsgi_server.CPWSGIServer(self)
if isinstance(httpserver, basestring):
if isinstance(httpserver, text_or_bytes):
# Is anyone using this? Can I add an arg?
httpserver = attributes(httpserver)(self)
return httpserver, self.bind_addr
def start(self):
"""Start the HTTP server."""
if not self.httpserver:
self.httpserver, self.bind_addr = self.httpserver_from_self()
ServerAdapter.start(self)
start.priority = 75
def _get_bind_addr(self):
if self.socket_file:
return self.socket_file
if self.socket_host is None and self.socket_port is None:
return None
return (self.socket_host, self.socket_port)
def _set_bind_addr(self, value):
if value is None:
self.socket_file = None
self.socket_host = None
self.socket_port = None
elif isinstance(value, basestring):
elif isinstance(value, text_or_bytes):
self.socket_file = value
self.socket_host = None
self.socket_port = None
@@ -171,17 +189,21 @@ class Server(ServerAdapter):
self.socket_host, self.socket_port = value
self.socket_file = None
except ValueError:
raise ValueError("bind_addr must be a (host, port) tuple "
"(for TCP sockets) or a string (for Unix "
"domain sockets), not %r" % value)
bind_addr = property(_get_bind_addr, _set_bind_addr,
doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.')
raise ValueError('bind_addr must be a (host, port) tuple '
'(for TCP sockets) or a string (for Unix '
'domain sockets), not %r' % value)
bind_addr = property(
_get_bind_addr,
_set_bind_addr,
doc='A (host, port) tuple for TCP sockets or '
'a str for Unix domain sockets.')
def base(self):
"""Return the base (scheme://host[:port] or sock file) for this server."""
"""Return the base (scheme://host[:port] or sock file) for this server.
"""
if self.socket_file:
return self.socket_file
host = self.socket_host
if host in ('0.0.0.0', '::'):
# 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
@@ -189,17 +211,16 @@ class Server(ServerAdapter):
# safest thing to spit out in a URL.
import socket
host = socket.gethostname()
port = self.socket_port
if self.ssl_certificate:
scheme = "https"
if port != 443:
host += ":%s" % port
else:
scheme = "http"
if port != 80:
host += ":%s" % port
return "%s://%s" % (scheme, host)
port = self.socket_port
if self.ssl_certificate:
scheme = 'https'
if port != 443:
host += ':%s' % port
else:
scheme = 'http'
if port != 80:
host += ':%s' % port
return '%s://%s' % (scheme, host)

View File

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

View File

@@ -2,18 +2,18 @@
Tools are usually designed to be used in a variety of ways (although some
may only offer one if they choose):
Library calls
All tools are callables that can be used wherever needed.
The arguments are straightforward and should be detailed within the
docstring.
Function decorators
All tools, when called, may be used as decorators which configure
individual CherryPy page handlers (methods on the CherryPy tree).
That is, "@tools.anytool()" should "turn on" the tool via the
decorated function's _cp_config attribute.
CherryPy config
If a tool exposes a "_setup" callable, it will be called
once per Request (if the feature is "turned on" via config).
@@ -26,6 +26,12 @@ import sys
import warnings
import cherrypy
from cherrypy._helper import expose
from cherrypy.lib import cptools, encoding, auth, static, jsontools
from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc
from cherrypy.lib import caching as _caching
from cherrypy.lib import auth_basic, auth_digest
def _getargs(func):
@@ -43,17 +49,21 @@ def _getargs(func):
return co.co_varnames[:co.co_argcount]
_attr_error = ("CherryPy Tools cannot be turned on directly. Instead, turn them "
"on via config, or use them as decorators on your page handlers.")
_attr_error = (
'CherryPy Tools cannot be turned on directly. Instead, turn them '
'on via config, or use them as decorators on your page handlers.'
)
class Tool(object):
"""A registered function for use with CherryPy request-processing hooks.
help(tool.callable) should give you more information about this Tool.
"""
namespace = "tools"
namespace = 'tools'
def __init__(self, point, callable, name=None, priority=50):
self._point = point
self.callable = callable
@@ -61,20 +71,21 @@ class Tool(object):
self._priority = priority
self.__doc__ = self.callable.__doc__
self._setargs()
def _get_on(self):
raise AttributeError(_attr_error)
def _set_on(self, value):
raise AttributeError(_attr_error)
on = property(_get_on, _set_on)
def _setargs(self):
"""Copy func parameter names to obj attributes."""
try:
for arg in _getargs(self.callable):
setattr(self, arg, None)
except (TypeError, AttributeError):
if hasattr(self.callable, "__call__"):
if hasattr(self.callable, '__call__'):
for arg in _getargs(self.callable.__call__):
setattr(self, arg, None)
# IronPython 1.0 raises NotImplementedError because
@@ -86,64 +97,66 @@ class Tool(object):
# but if we trap it here it doesn't prevent CP from working.
except IndexError:
pass
def _merged_args(self, d=None):
"""Return a dict of configuration entries for this Tool."""
if d:
conf = d.copy()
else:
conf = {}
tm = cherrypy.serving.request.toolmaps[self.namespace]
if self._name in tm:
conf.update(tm[self._name])
if "on" in conf:
del conf["on"]
if 'on' in conf:
del conf['on']
return conf
def __call__(self, *args, **kwargs):
"""Compile-time decorator (turn on the tool in config).
For example::
@expose
@tools.proxy()
def whats_my_base(self):
return cherrypy.request.base
whats_my_base.exposed = True
"""
if args:
raise TypeError("The %r Tool does not accept positional "
"arguments; you must use keyword arguments."
raise TypeError('The %r Tool does not accept positional '
'arguments; you must use keyword arguments.'
% self._name)
def tool_decorator(f):
if not hasattr(f, "_cp_config"):
if not hasattr(f, '_cp_config'):
f._cp_config = {}
subspace = self.namespace + "." + self._name + "."
f._cp_config[subspace + "on"] = True
subspace = self.namespace + '.' + self._name + '.'
f._cp_config[subspace + 'on'] = True
for k, v in kwargs.items():
f._cp_config[subspace + k] = v
return f
return tool_decorator
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
p = getattr(self.callable, 'priority', self._priority)
cherrypy.serving.request.hooks.attach(self._point, self.callable,
priority=p, **conf)
class HandlerTool(Tool):
"""Tool which is called 'before main', that may skip normal handlers.
If the tool successfully handles the request (by setting response.body),
if should return True. This will cause CherryPy to skip any 'normal' page
handler. If the tool did not handle the request, it should return False
@@ -151,56 +164,57 @@ class HandlerTool(Tool):
tool is declared AS a page handler (see the 'handler' method), returning
False will raise NotFound.
"""
def __init__(self, callable, name=None):
Tool.__init__(self, 'before_handler', callable, name)
def handler(self, *args, **kwargs):
"""Use this tool as a CherryPy page handler.
For example::
class Root:
nav = tools.staticdir.handler(section="/nav", dir="nav",
root=absDir)
"""
@expose
def handle_func(*a, **kw):
handled = self.callable(*args, **self._merged_args(kwargs))
if not handled:
raise cherrypy.NotFound()
return cherrypy.serving.response.body
handle_func.exposed = True
return handle_func
def _wrapper(self, **kwargs):
if self.callable(**kwargs):
cherrypy.serving.request.handler = None
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
p = getattr(self.callable, 'priority', self._priority)
cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
priority=p, **conf)
class HandlerWrapperTool(Tool):
"""Tool which wraps request.handler in a provided wrapper function.
The 'newhandler' arg must be a handler wrapper function that takes a
'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
page handler
functions, it must return an iterable for use as cherrypy.response.body.
For example, to allow your 'inner' page handlers to return dicts
which then get interpolated into a template::
def interpolator(next_handler, *args, **kwargs):
filename = cherrypy.request.config.get('template')
cherrypy.response.template = env.get_template(filename)
@@ -208,32 +222,35 @@ class HandlerWrapperTool(Tool):
return cherrypy.response.template.render(**response_dict)
cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
"""
def __init__(self, newhandler, point='before_handler', name=None, priority=50):
def __init__(self, newhandler, point='before_handler', name=None,
priority=50):
self.newhandler = newhandler
self._point = point
self._name = name
self._priority = priority
def callable(self, debug=False):
def callable(self, *args, **kwargs):
innerfunc = cherrypy.serving.request.handler
def wrap(*args, **kwargs):
return self.newhandler(innerfunc, *args, **kwargs)
cherrypy.serving.request.handler = wrap
class ErrorTool(Tool):
"""Tool which is used to replace the default request.error_response."""
def __init__(self, callable, name=None):
Tool.__init__(self, None, callable, name)
def _wrapper(self):
self.callable(**self._merged_args())
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
@@ -242,52 +259,49 @@ class ErrorTool(Tool):
# Builtin tools #
from cherrypy.lib import cptools, encoding, auth, static, jsontools
from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc
from cherrypy.lib import caching as _caching
from cherrypy.lib import auth_basic, auth_digest
class SessionTool(Tool):
"""Session Tool for CherryPy.
sessions.locking
When 'implicit' (the default), the session will be locked for you,
just before running the page handler.
When 'early', the session will be locked before reading the request
body. This is off by default for safety reasons; for example,
a large upload would block the session, denying an AJAX
progress meter (see http://www.cherrypy.org/ticket/630).
progress meter
(`issue <https://github.com/cherrypy/cherrypy/issues/630>`_).
When 'explicit' (or any other value), you need to call
cherrypy.session.acquire_lock() yourself before using
session data.
"""
def __init__(self):
# _sessions.init must be bound after headers are read
Tool.__init__(self, 'before_request_body', _sessions.init)
def _lock_session(self):
cherrypy.serving.session.acquire_lock()
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
hooks = cherrypy.serving.request.hooks
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
p = getattr(self.callable, 'priority', self._priority)
hooks.attach(self._point, self.callable, priority=p, **conf)
locking = conf.pop('locking', 'implicit')
if locking == 'implicit':
hooks.attach('before_handler', self._lock_session)
@@ -298,15 +312,15 @@ class SessionTool(Tool):
else:
# Don't lock
pass
hooks.attach('before_finalize', _sessions.save)
hooks.attach('on_end_request', _sessions.close)
def regenerate(self):
"""Drop the current session and make a new one (with a new id)."""
sess = cherrypy.serving.session
sess.regenerate()
# Grab cookie-relevant tool args
conf = dict([(k, v) for k, v in self._merged_args().items()
if k in ('path', 'path_header', 'name', 'timeout',
@@ -314,19 +328,18 @@ class SessionTool(Tool):
_sessions.set_response_cookie(**conf)
class XMLRPCController(object):
"""A Controller (page handler collection) for XML-RPC.
To use it, have your controllers subclass this base class (it will
turn on the tool for you).
You can also supply the following optional config entries::
tools.xmlrpc.encoding: 'utf-8'
tools.xmlrpc.allow_none: 0
XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
appropriate handler must first be performed according to the URL, and
then a second dispatch step must take place according to the RPC method
@@ -334,61 +347,62 @@ class XMLRPCController(object):
prefix in the URL, supplies its own handler args in the body, and
requires a 200 OK "Fault" response instead of 404 when the desired
method is not found.
Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
This Controller acts as the dispatch target for the first half (based
on the URL); it then reads the RPC method from the request body and
does its own second dispatch step based on that method. It also reads
body params, and returns a Fault on error.
The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
in your URL's, you can safely skip turning on the XMLRPCDispatcher.
Otherwise, you need to use declare it in config::
request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
"""
# Note we're hard-coding this into the 'tools' namespace. We could do
# a huge amount of work to make it relocatable, but the only reason why
# would be if someone actually disabled the default_toolbox. Meh.
_cp_config = {'tools.xmlrpc.on': True}
@expose
def default(self, *vpath, **params):
rpcparams, rpcmethod = _xmlrpc.process_body()
subhandler = self
for attr in str(rpcmethod).split('.'):
subhandler = getattr(subhandler, attr, None)
if subhandler and getattr(subhandler, "exposed", False):
if subhandler and getattr(subhandler, 'exposed', False):
body = subhandler(*(vpath + rpcparams), **params)
else:
# http://www.cherrypy.org/ticket/533
# https://github.com/cherrypy/cherrypy/issues/533
# if a method is not found, an xmlrpclib.Fault should be returned
# raising an exception here will do that; see
# cherrypy.lib.xmlrpcutil.on_error
raise Exception('method "%s" is not supported' % attr)
conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {})
conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {})
_xmlrpc.respond(body,
conf.get('encoding', 'utf-8'),
conf.get('allow_none', 0))
return cherrypy.serving.response.body
default.exposed = True
class SessionAuthTool(HandlerTool):
def _setargs(self):
for name in dir(cptools.SessionAuth):
if not name.startswith("__"):
if not name.startswith('__'):
setattr(self, name, None)
class CachingTool(Tool):
"""Caching Tool for CherryPy."""
def _wrapper(self, **kwargs):
request = cherrypy.serving.request
if _caching.get(**kwargs):
@@ -397,29 +411,29 @@ class CachingTool(Tool):
if request.cacheable:
# Note the devious technique here of adding hooks on the fly
request.hooks.attach('before_finalize', _caching.tee_output,
priority = 90)
priority=90)
_wrapper.priority = 20
def _setup(self):
"""Hook caching into cherrypy.request."""
conf = self._merged_args()
p = conf.pop("priority", None)
p = conf.pop('priority', None)
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
priority=p, **conf)
class Toolbox(object):
"""A collection of Tools.
This object also functions as a config namespace handler for itself.
Custom toolboxes should be added to each Application's toolboxes dict.
"""
def __init__(self, namespace):
self.namespace = namespace
def __setattr__(self, name, value):
# If the Tool._name is None, supply it from the attribute name.
if isinstance(value, Tool):
@@ -427,47 +441,56 @@ class Toolbox(object):
value._name = name
value.namespace = self.namespace
object.__setattr__(self, name, value)
def __enter__(self):
"""Populate request.toolmaps from tools specified in config."""
cherrypy.serving.request.toolmaps[self.namespace] = map = {}
def populate(k, v):
toolname, arg = k.split(".", 1)
toolname, arg = k.split('.', 1)
bucket = map.setdefault(toolname, {})
bucket[arg] = v
return populate
def __exit__(self, exc_type, exc_val, exc_tb):
"""Run tool._setup() for each tool in our toolmap."""
map = cherrypy.serving.request.toolmaps.get(self.namespace)
if map:
for name, settings in map.items():
if settings.get("on", False):
if settings.get('on', False):
tool = getattr(self, name)
tool._setup()
def register(self, point, **kwargs):
"""Return a decorator which registers the function at the given hook point."""
def decorator(func):
setattr(self, kwargs.get('name', func.__name__), Tool(point, func, **kwargs))
return func
return decorator
class DeprecatedTool(Tool):
_name = None
warnmsg = "This Tool is deprecated."
warnmsg = 'This Tool is deprecated.'
def __init__(self, point, warnmsg=None):
self.point = point
if warnmsg is not None:
self.warnmsg = warnmsg
def __call__(self, *args, **kwargs):
warnings.warn(self.warnmsg)
def tool_decorator(f):
return f
return tool_decorator
def _setup(self):
warnings.warn(self.warnmsg)
default_toolbox = _d = Toolbox("tools")
default_toolbox = _d = Toolbox('tools')
_d.session_auth = SessionAuthTool(cptools.session_auth)
_d.allow = Tool('on_start_resource', cptools.allow)
_d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
@@ -487,12 +510,16 @@ _d.sessions = SessionTool()
_d.xmlrpc = ErrorTool(_xmlrpc.on_error)
_d.caching = CachingTool('before_handler', _caching.get, 'caching')
_d.expires = Tool('before_finalize', _caching.expires)
_d.tidy = DeprecatedTool('before_finalize',
"The tidy tool has been removed from the standard distribution of CherryPy. "
"The most recent version can be found at http://tools.cherrypy.org/browser.")
_d.nsgmls = DeprecatedTool('before_finalize',
"The nsgmls tool has been removed from the standard distribution of CherryPy. "
"The most recent version can be found at http://tools.cherrypy.org/browser.")
_d.tidy = DeprecatedTool(
'before_finalize',
'The tidy tool has been removed from the standard distribution of '
'CherryPy. The most recent version can be found at '
'http://tools.cherrypy.org/browser.')
_d.nsgmls = DeprecatedTool(
'before_finalize',
'The nsgmls tool has been removed from the standard distribution of '
'CherryPy. The most recent version can be found at '
'http://tools.cherrypy.org/browser.')
_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
_d.referer = Tool('before_request_body', cptools.referer)
_d.basic_auth = Tool('on_start_resource', auth.basic_auth)
@@ -506,5 +533,6 @@ _d.json_in = Tool('before_request_body', jsontools.json_in, priority=30)
_d.json_out = Tool('before_handler', jsontools.json_out, priority=30)
_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1)
_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1)
_d.params = Tool('before_handler', cptools.convert_params)
del _d, cptools, encoding, auth, static

View File

@@ -1,235 +1,243 @@
"""CherryPy Application and Tree objects."""
import os
import sys
import six
import cherrypy
from cherrypy._cpcompat import ntou, py3k
from cherrypy._cpcompat import ntou
from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
from cherrypy.lib import httputil
class Application(object):
"""A CherryPy Application.
Servers and gateways should not instantiate Request objects directly.
Instead, they should ask an Application object for a request object.
An instance of this class may also be used as a WSGI callable
(WSGI application object) for itself.
"""
root = None
"""The top-most container of page handlers for this app. Handlers should
be arranged in a hierarchy of attributes, matching the expected URI
hierarchy; the default dispatcher then searches this hierarchy for a
matching handler. When using a dispatcher other than the default,
this value may be None."""
config = {}
"""A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
of {key: value} pairs."""
namespaces = _cpconfig.NamespaceSet()
toolboxes = {'tools': cherrypy.tools}
log = None
"""A LogManager instance. See _cplogging."""
wsgiapp = None
"""A CPWSGIApp instance. See _cpwsgi."""
request_class = _cprequest.Request
response_class = _cprequest.Response
relative_urls = False
def __init__(self, root, script_name="", config=None):
def __init__(self, root, script_name='', config=None):
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
self.root = root
self.script_name = script_name
self.wsgiapp = _cpwsgi.CPWSGIApp(self)
self.namespaces = self.namespaces.copy()
self.namespaces["log"] = lambda k, v: setattr(self.log, k, v)
self.namespaces["wsgi"] = self.wsgiapp.namespace_handler
self.namespaces['log'] = lambda k, v: setattr(self.log, k, v)
self.namespaces['wsgi'] = self.wsgiapp.namespace_handler
self.config = self.__class__.config.copy()
if config:
self.merge(config)
def __repr__(self):
return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__,
return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__,
self.root, self.script_name)
script_name_doc = """The URI "mount point" for this app. A mount point is that portion of
the URI which is constant for all URIs that are serviced by this
application; it does not include scheme, host, or proxy ("virtual host")
portions of the URI.
script_name_doc = """The URI "mount point" for this app. A mount point
is that portion of the URI which is constant for all URIs that are
serviced by this application; it does not include scheme, host, or proxy
("virtual host") portions of the URI.
For example, if script_name is "/my/cool/app", then the URL
"http://www.example.com/my/cool/app/page1" might be handled by a
"page1" method on the root object.
The value of script_name MUST NOT end in a slash. If the script_name
refers to the root of the URI, it MUST be an empty string (not "/").
If script_name is explicitly set to None, then the script_name will be
provided for each call from request.wsgi_environ['SCRIPT_NAME'].
"""
def _get_script_name(self):
if self._script_name is None:
# None signals that the script name should be pulled from WSGI environ.
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/")
return self._script_name
if self._script_name is not None:
return self._script_name
# A `_script_name` with a value of None signals that the script name
# should be pulled from WSGI environ.
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/')
def _set_script_name(self, value):
if value:
value = value.rstrip("/")
value = value.rstrip('/')
self._script_name = value
script_name = property(fget=_get_script_name, fset=_set_script_name,
doc=script_name_doc)
def merge(self, config):
"""Merge the given config into self.config."""
_cpconfig.merge(self.config, config)
# Handle namespaces specified in config.
self.namespaces(self.config.get("/", {}))
self.namespaces(self.config.get('/', {}))
def find_config(self, path, key, default=None):
"""Return the most-specific value for key along path, or default."""
trail = path or "/"
trail = path or '/'
while trail:
nodeconf = self.config.get(trail, {})
if key in nodeconf:
return nodeconf[key]
lastslash = trail.rfind("/")
lastslash = trail.rfind('/')
if lastslash == -1:
break
elif lastslash == 0 and trail != "/":
trail = "/"
elif lastslash == 0 and trail != '/':
trail = '/'
else:
trail = trail[:lastslash]
return default
def get_serving(self, local, remote, scheme, sproto):
"""Create and return a Request and Response object."""
req = self.request_class(local, remote, scheme, sproto)
req.app = self
for name, toolbox in self.toolboxes.items():
req.namespaces[name] = toolbox
resp = self.response_class()
cherrypy.serving.load(req, resp)
cherrypy.engine.publish('acquire_thread')
cherrypy.engine.publish('before_request')
return req, resp
def release_serving(self):
"""Release the current serving (request and response)."""
req = cherrypy.serving.request
cherrypy.engine.publish('after_request')
try:
req.close()
except:
cherrypy.log(traceback=True, severity=40)
cherrypy.serving.clear()
def __call__(self, environ, start_response):
return self.wsgiapp(environ, start_response)
class Tree(object):
"""A registry of CherryPy applications, mounted at diverse points.
An instance of this class may also be used as a WSGI callable
(WSGI application object), in which case it dispatches to all
mounted apps.
"""
apps = {}
"""
A dict of the form {script name: application}, where "script name"
is a string declaring the URI mount point (no trailing slash), and
"application" is an instance of cherrypy.Application (or an arbitrary
WSGI callable if you happen to be using a WSGI server)."""
def __init__(self):
self.apps = {}
def mount(self, root, script_name="", config=None):
def mount(self, root, script_name='', config=None):
"""Mount a new app from a root object, script_name, and config.
root
An instance of a "controller class" (a collection of page
handler methods) which represents the root of the application.
This may also be an Application instance, or None if using
a dispatcher other than the default.
script_name
A string containing the "mount point" of the application.
This should start with a slash, and be the path portion of the
URL at which to mount the given root. For example, if root.index()
will handle requests to "http://www.example.com:8080/dept/app1/",
then the script_name argument would be "/dept/app1".
It MUST NOT end in a slash. If the script_name refers to the
root of the URI, it MUST be an empty string (not "/").
config
A file or dict containing application config.
"""
if script_name is None:
raise TypeError(
"The 'script_name' argument may not be None. Application "
"objects may, however, possess a script_name of None (in "
"order to inpect the WSGI environ for SCRIPT_NAME upon each "
"request). You cannot mount such Applications on this Tree; "
"you must pass them to a WSGI server interface directly.")
'objects may, however, possess a script_name of None (in '
'order to inpect the WSGI environ for SCRIPT_NAME upon each '
'request). You cannot mount such Applications on this Tree; '
'you must pass them to a WSGI server interface directly.')
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/")
script_name = script_name.rstrip('/')
if isinstance(root, Application):
app = root
if script_name != "" and script_name != app.script_name:
raise ValueError("Cannot specify a different script name and "
"pass an Application instance to cherrypy.mount")
if script_name != '' and script_name != app.script_name:
raise ValueError(
'Cannot specify a different script name and pass an '
'Application instance to cherrypy.mount')
script_name = app.script_name
else:
app = Application(root, script_name)
# If mounted at "", add favicon.ico
if (script_name == "" and root is not None
and not hasattr(root, "favicon_ico")):
if (script_name == '' and root is not None
and not hasattr(root, 'favicon_ico')):
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
"favicon.ico")
'favicon.ico')
root.favicon_ico = tools.staticfile.handler(favicon)
if config:
app.merge(config)
self.apps[script_name] = app
return app
def graft(self, wsgi_callable, script_name=""):
def graft(self, wsgi_callable, script_name=''):
"""Mount a wsgi callable at the given script_name."""
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/")
script_name = script_name.rstrip('/')
self.apps[script_name] = wsgi_callable
def script_name(self, path=None):
"""The script_name of the app at the given path, or None.
If path is None, cherrypy.request is used.
"""
if path is None:
@@ -239,52 +247,41 @@ class Tree(object):
request.path_info)
except AttributeError:
return None
while True:
if path in self.apps:
return path
if path == "":
if path == '':
return None
# Move one node up the tree and try again.
path = path[:path.rfind("/")]
path = path[:path.rfind('/')]
def __call__(self, environ, start_response):
# If you're calling this, then you're probably setting SCRIPT_NAME
# to '' (some WSGI servers always set SCRIPT_NAME to '').
# Try to look up the app using the full path.
env1x = environ
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
env1x.get('PATH_INFO', ''))
sn = self.script_name(path or "/")
sn = self.script_name(path or '/')
if sn is None:
start_response('404 Not Found', [])
return []
app = self.apps[sn]
# Correct the SCRIPT_NAME and PATH_INFO environ entries.
environ = environ.copy()
if not py3k:
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
# Python 2/WSGI u.0: all strings MUST be of type unicode
enc = environ[ntou('wsgi.url_encoding')]
environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
environ[ntou('PATH_INFO')] = path[len(sn.rstrip("/")):].decode(enc)
else:
# Python 2/WSGI 1.x: all strings MUST be of type str
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
# Python 2/WSGI u.0: all strings MUST be of type unicode
enc = environ[ntou('wsgi.url_encoding')]
environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc)
else:
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
# Python 3/WSGI u.0: all strings MUST be full unicode
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
else:
# Python 3/WSGI 1.x: all strings MUST be ISO-8859-1 str
environ['SCRIPT_NAME'] = sn.encode('utf-8').decode('ISO-8859-1')
environ['PATH_INFO'] = path[len(sn.rstrip("/")):].encode('utf-8').decode('ISO-8859-1')
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
return app(environ, start_response)

View File

@@ -8,54 +8,62 @@ still be translatable to bytes via the Latin-1 encoding!"
"""
import sys as _sys
import io
import six
import cherrypy as _cherrypy
from cherrypy._cpcompat import BytesIO, bytestr, ntob, ntou, py3k, unicodestr
from cherrypy._cpcompat import ntob, ntou
from cherrypy import _cperror
from cherrypy.lib import httputil
from cherrypy.lib import is_closable_iterator
def downgrade_wsgi_ux_to_1x(environ):
"""Return a new environ dict for WSGI 1.x from the given WSGI u.x environ."""
"""Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.
"""
env1x = {}
url_encoding = environ[ntou('wsgi.url_encoding')]
for k, v in list(environ.items()):
if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
v = v.encode(url_encoding)
elif isinstance(v, unicodestr):
elif isinstance(v, six.text_type):
v = v.encode('ISO-8859-1')
env1x[k.encode('ISO-8859-1')] = v
return env1x
class VirtualHost(object):
"""Select a different WSGI application based on the Host header.
This can be useful when running multiple sites within one CP server.
It allows several domains to point to different applications. For example::
root = Root()
RootApp = cherrypy.Application(root)
Domain2App = cherrypy.Application(root)
SecureApp = cherrypy.Application(Secure())
vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
domains={'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
})
vhost = cherrypy._cpwsgi.VirtualHost(
RootApp,
domains={
'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
},
)
cherrypy.tree.graft(vhost)
"""
default = None
"""Required. The default WSGI application."""
use_x_forwarded_host = True
"""If True (the default), any "X-Forwarded-Host"
request header will be used instead of the "Host" header. This
is commonly added by HTTP servers (such as Apache) when proxying."""
domains = {}
"""A dict of {host header value: application} pairs.
The incoming "Host" request header is looked up in this dict,
@@ -64,17 +72,17 @@ class VirtualHost(object):
separate entries for "example.com" and "www.example.com".
In addition, "Host" headers may contain the port number.
"""
def __init__(self, default, domains=None, use_x_forwarded_host=True):
self.default = default
self.domains = domains or {}
self.use_x_forwarded_host = use_x_forwarded_host
def __call__(self, environ, start_response):
domain = environ.get('HTTP_HOST', '')
if self.use_x_forwarded_host:
domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
domain = environ.get('HTTP_X_FORWARDED_HOST', domain)
nextapp = self.domains.get(domain)
if nextapp is None:
nextapp = self.default
@@ -82,12 +90,13 @@ class VirtualHost(object):
class InternalRedirector(object):
"""WSGI middleware that handles raised cherrypy.InternalRedirect."""
def __init__(self, nextapp, recursive=False):
self.nextapp = nextapp
self.recursive = recursive
def __call__(self, environ, start_response):
redirections = []
while True:
@@ -99,71 +108,82 @@ class InternalRedirector(object):
sn = environ.get('SCRIPT_NAME', '')
path = environ.get('PATH_INFO', '')
qs = environ.get('QUERY_STRING', '')
# Add the *previous* path_info + qs to redirections.
old_uri = sn + path
if qs:
old_uri += "?" + qs
old_uri += '?' + qs
redirections.append(old_uri)
if not self.recursive:
# Check to see if the new URI has been redirected to already
# Check to see if the new URI has been redirected to
# already
new_uri = sn + ir.path
if ir.query_string:
new_uri += "?" + ir.query_string
new_uri += '?' + ir.query_string
if new_uri in redirections:
ir.request.close()
raise RuntimeError("InternalRedirector visited the "
"same URL twice: %r" % new_uri)
tmpl = (
'InternalRedirector visited the same URL twice: %r'
)
raise RuntimeError(tmpl % new_uri)
# Munge the environment and try again.
environ['REQUEST_METHOD'] = "GET"
environ['REQUEST_METHOD'] = 'GET'
environ['PATH_INFO'] = ir.path
environ['QUERY_STRING'] = ir.query_string
environ['wsgi.input'] = BytesIO()
environ['CONTENT_LENGTH'] = "0"
environ['wsgi.input'] = io.BytesIO()
environ['CONTENT_LENGTH'] = '0'
environ['cherrypy.previous_request'] = ir.request
class ExceptionTrapper(object):
"""WSGI middleware that traps exceptions."""
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
self.nextapp = nextapp
self.throws = throws
def __call__(self, environ, start_response):
return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
return _TrappedResponse(
self.nextapp,
environ,
start_response,
self.throws
)
class _TrappedResponse(object):
response = iter([])
def __init__(self, nextapp, environ, start_response, throws):
self.nextapp = nextapp
self.environ = environ
self.start_response = start_response
self.throws = throws
self.started_response = False
self.response = self.trap(self.nextapp, self.environ, self.start_response)
self.response = self.trap(
self.nextapp, self.environ, self.start_response,
)
self.iter_response = iter(self.response)
def __iter__(self):
self.started_response = True
return self
if py3k:
def __next__(self):
return self.trap(next, self.iter_response)
else:
def next(self):
return self.trap(self.iter_response.next)
def __next__(self):
return self.trap(next, self.iter_response)
# todo: https://pythonhosted.org/six/#six.Iterator
if six.PY2:
next = __next__
def close(self):
if hasattr(self.response, 'close'):
self.response.close()
def trap(self, func, *args, **kwargs):
try:
return func(*args, **kwargs)
@@ -173,22 +193,23 @@ class _TrappedResponse(object):
raise
except:
tb = _cperror.format_exc()
#print('trapped (started %s):' % self.started_response, tb)
_cherrypy.log(tb, severity=40)
if not _cherrypy.request.show_tracebacks:
tb = ""
tb = ''
s, h, b = _cperror.bare_error(tb)
if py3k:
if six.PY3:
# What fun.
s = s.decode('ISO-8859-1')
h = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in h]
h = [
(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in h
]
if self.started_response:
# Empty our iterable (so future calls raise StopIteration)
self.iter_response = iter([])
else:
self.iter_response = iter(b)
try:
self.start_response(s, h, _sys.exc_info())
except:
@@ -199,9 +220,9 @@ class _TrappedResponse(object):
# But we still log and call close() to clean up ourselves.
_cherrypy.log(traceback=True, severity=40)
raise
if self.started_response:
return ntob("").join(b)
return ntob('').join(b)
else:
return b
@@ -210,12 +231,13 @@ class _TrappedResponse(object):
class AppResponse(object):
"""WSGI response iterable for CherryPy applications."""
def __init__(self, environ, start_response, cpapp):
self.cpapp = cpapp
try:
if not py3k:
if six.PY2:
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
environ = downgrade_wsgi_ux_to_1x(environ)
self.environ = environ
@@ -224,59 +246,82 @@ class AppResponse(object):
r = _cherrypy.serving.response
outstatus = r.output_status
if not isinstance(outstatus, bytestr):
raise TypeError("response.output_status is not a byte string.")
if not isinstance(outstatus, bytes):
raise TypeError('response.output_status is not a byte string.')
outheaders = []
for k, v in r.header_list:
if not isinstance(k, bytestr):
raise TypeError("response.header_list key %r is not a byte string." % k)
if not isinstance(v, bytestr):
raise TypeError("response.header_list value %r is not a byte string." % v)
if not isinstance(k, bytes):
tmpl = 'response.header_list key %r is not a byte string.'
raise TypeError(tmpl % k)
if not isinstance(v, bytes):
tmpl = (
'response.header_list value %r is not a byte string.'
)
raise TypeError(tmpl % v)
outheaders.append((k, v))
if py3k:
# According to PEP 3333, when using Python 3, the response status
# and headers must be bytes masquerading as unicode; that is, they
# must be of type "str" but are restricted to code points in the
# "latin-1" set.
if six.PY3:
# According to PEP 3333, when using Python 3, the response
# status and headers must be bytes masquerading as unicode;
# that is, they must be of type "str" but are restricted to
# code points in the "latin-1" set.
outstatus = outstatus.decode('ISO-8859-1')
outheaders = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in outheaders]
outheaders = [
(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in outheaders
]
self.iter_response = iter(r.body)
self.write = start_response(outstatus, outheaders)
except:
self.close()
raise
def __iter__(self):
return self
if py3k:
def __next__(self):
return next(self.iter_response)
else:
def next(self):
return self.iter_response.next()
def __next__(self):
return next(self.iter_response)
# todo: https://pythonhosted.org/six/#six.Iterator
if six.PY2:
next = __next__
def close(self):
"""Close and de-reference the current request and response. (Core)"""
streaming = _cherrypy.serving.response.stream
self.cpapp.release_serving()
# We avoid the expense of examining the iterator to see if it's
# closable unless we are streaming the response, as that's the
# only situation where we are going to have an iterator which
# may not have been exhausted yet.
if streaming and is_closable_iterator(self.iter_response):
iter_close = self.iter_response.close
try:
iter_close()
except Exception:
_cherrypy.log(traceback=True, severity=40)
def run(self):
"""Create a Request object using environ."""
env = self.environ.get
local = httputil.Host('', int(env('SERVER_PORT', 80)),
env('SERVER_NAME', ''))
remote = httputil.Host(env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1) or -1),
env('REMOTE_HOST', ''))
local = httputil.Host(
'',
int(env('SERVER_PORT', 80) or -1),
env('SERVER_NAME', ''),
)
remote = httputil.Host(
env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1) or -1),
env('REMOTE_HOST', ''),
)
scheme = env('wsgi.url_scheme')
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1')
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
# LOGON_USER is served by IIS, and is the name of the
# user after having been mapped to a local account.
# Both IIS and Apache set REMOTE_USER, when possible.
@@ -285,99 +330,113 @@ class AppResponse(object):
request.multiprocess = self.environ['wsgi.multiprocess']
request.wsgi_environ = self.environ
request.prev = env('cherrypy.previous_request', None)
meth = self.environ['REQUEST_METHOD']
path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', ''))
path = httputil.urljoin(
self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', ''),
)
qs = self.environ.get('QUERY_STRING', '')
if py3k:
# This isn't perfect; if the given PATH_INFO is in the wrong encoding,
# it may fail to match the appropriate config section URI. But meh.
old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
new_enc = self.cpapp.find_config(self.environ.get('PATH_INFO', ''),
"request.uri_encoding", 'utf-8')
if new_enc.lower() != old_enc.lower():
# Even though the path and qs are unicode, the WSGI server is
# required by PEP 3333 to coerce them to ISO-8859-1 masquerading
# as unicode. So we have to encode back to bytes and then decode
# again using the "correct" encoding.
try:
u_path = path.encode(old_enc).decode(new_enc)
u_qs = qs.encode(old_enc).decode(new_enc)
except (UnicodeEncodeError, UnicodeDecodeError):
# Just pass them through without transcoding and hope.
pass
else:
# Only set transcoded values if they both succeed.
path = u_path
qs = u_qs
path, qs = self.recode_path_qs(path, qs) or (path, qs)
rproto = self.environ.get('SERVER_PROTOCOL')
headers = self.translate_headers(self.environ)
rfile = self.environ['wsgi.input']
request.run(meth, path, qs, rproto, headers, rfile)
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
'CONTENT_LENGTH': 'Content-Length',
'CONTENT_TYPE': 'Content-Type',
'REMOTE_HOST': 'Remote-Host',
'REMOTE_ADDR': 'Remote-Addr',
}
headerNames = {
'HTTP_CGI_AUTHORIZATION': 'Authorization',
'CONTENT_LENGTH': 'Content-Length',
'CONTENT_TYPE': 'Content-Type',
'REMOTE_HOST': 'Remote-Host',
'REMOTE_ADDR': 'Remote-Addr',
}
def recode_path_qs(self, path, qs):
if not six.PY3:
return
# This isn't perfect; if the given PATH_INFO is in the
# wrong encoding, it may fail to match the appropriate config
# section URI. But meh.
old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
new_enc = self.cpapp.find_config(
self.environ.get('PATH_INFO', ''),
'request.uri_encoding', 'utf-8',
)
if new_enc.lower() == old_enc.lower():
return
# Even though the path and qs are unicode, the WSGI server
# is required by PEP 3333 to coerce them to ISO-8859-1
# masquerading as unicode. So we have to encode back to
# bytes and then decode again using the "correct" encoding.
try:
return (
path.encode(old_enc).decode(new_enc),
qs.encode(old_enc).decode(new_enc),
)
except (UnicodeEncodeError, UnicodeDecodeError):
# Just pass them through without transcoding and hope.
pass
def translate_headers(self, environ):
"""Translate CGI-environ header names to HTTP header names."""
for cgiName in environ:
# We assume all incoming header keys are uppercase already.
if cgiName in self.headerNames:
yield self.headerNames[cgiName], environ[cgiName]
elif cgiName[:5] == "HTTP_":
elif cgiName[:5] == 'HTTP_':
# Hackish attempt at recovering original header names.
translatedHeader = cgiName[5:].replace("_", "-")
translatedHeader = cgiName[5:].replace('_', '-')
yield translatedHeader, environ[cgiName]
class CPWSGIApp(object):
"""A WSGI application object for a CherryPy Application."""
pipeline = [('ExceptionTrapper', ExceptionTrapper),
('InternalRedirector', InternalRedirector),
]
pipeline = [
('ExceptionTrapper', ExceptionTrapper),
('InternalRedirector', InternalRedirector),
]
"""A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
constructor that takes an initial, positional 'nextapp' argument,
plus optional keyword arguments, and returns a WSGI application
(that takes environ and start_response arguments). The 'name' can
be any you choose, and will correspond to keys in self.config."""
head = None
"""Rather than nest all apps in the pipeline on each call, it's only
done the first time, and the result is memoized into self.head. Set
this to None again if you change self.pipeline after calling self."""
config = {}
"""A dict whose keys match names listed in the pipeline. Each
value is a further dict which will be passed to the corresponding
named WSGI callable (from the pipeline) as keyword arguments."""
response_class = AppResponse
"""The class to instantiate and return as the next app in the WSGI chain."""
"""The class to instantiate and return as the next app in the WSGI chain.
"""
def __init__(self, cpapp, pipeline=None):
self.cpapp = cpapp
self.pipeline = self.pipeline[:]
if pipeline:
self.pipeline.extend(pipeline)
self.config = self.config.copy()
def tail(self, environ, start_response):
"""WSGI application callable for the actual CherryPy application.
You probably shouldn't call this; call self.__call__ instead,
so that any WSGI middleware in self.pipeline can run first.
"""
return self.response_class(environ, start_response, self.cpapp)
def __call__(self, environ, start_response):
head = self.head
if head is None:
@@ -389,20 +448,19 @@ class CPWSGIApp(object):
head = callable(head, **conf)
self.head = head
return head(environ, start_response)
def namespace_handler(self, k, v):
"""Config handler for the 'wsgi' namespace."""
if k == "pipeline":
if k == 'pipeline':
# Note this allows multiple 'wsgi.pipeline' config entries
# (but each entry will be processed in a 'random' order).
# It should also allow developers to set default middleware
# in code (passed to self.__init__) that deployers can add to
# (but not remove) via config.
self.pipeline.extend(v)
elif k == "response_class":
elif k == 'response_class':
self.response_class = v
else:
name, arg = k.split(".", 1)
name, arg = k.split('.', 1)
bucket = self.config.setdefault(name, {})
bucket[arg] = v

View File

@@ -8,32 +8,39 @@ from cherrypy import wsgiserver
class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
"""Wrapper for wsgiserver.CherryPyWSGIServer.
wsgiserver has been designed to not reference CherryPy in any way,
so that it can be used in other frameworks and applications. Therefore,
we wrap it here, so we can set our own mount points from cherrypy.tree
and apply some attributes from config -> cherrypy.server -> wsgiserver.
"""
def __init__(self, server_adapter=cherrypy.server):
self.server_adapter = server_adapter
self.max_request_header_size = self.server_adapter.max_request_header_size or 0
self.max_request_body_size = self.server_adapter.max_request_body_size or 0
self.max_request_header_size = (
self.server_adapter.max_request_header_size or 0
)
self.max_request_body_size = (
self.server_adapter.max_request_body_size or 0
)
server_name = (self.server_adapter.socket_host or
self.server_adapter.socket_file or
None)
self.wsgi_version = self.server_adapter.wsgi_version
s = wsgiserver.CherryPyWSGIServer
s.__init__(self, server_adapter.bind_addr, cherrypy.tree,
self.server_adapter.thread_pool,
server_name,
max = self.server_adapter.thread_pool_max,
request_queue_size = self.server_adapter.socket_queue_size,
timeout = self.server_adapter.socket_timeout,
shutdown_timeout = self.server_adapter.shutdown_timeout,
max=self.server_adapter.thread_pool_max,
request_queue_size=self.server_adapter.socket_queue_size,
timeout=self.server_adapter.socket_timeout,
shutdown_timeout=self.server_adapter.shutdown_timeout,
accepted_queue_size=self.server_adapter.accepted_queue_size,
accepted_queue_timeout=self.server_adapter.accepted_queue_timeout,
)
self.protocol = self.server_adapter.protocol_version
self.nodelay = self.server_adapter.nodelay
@@ -55,9 +62,9 @@ class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain)
self.stats['Enabled'] = getattr(self.server_adapter, 'statistics', False)
def error_log(self, msg="", level=20, traceback=False):
self.stats['Enabled'] = getattr(
self.server_adapter, 'statistics', False)
def error_log(self, msg='', level=20, traceback=False):
cherrypy.engine.log(msg, level, traceback)

298
cherrypy/_helper.py Normal file
View File

@@ -0,0 +1,298 @@
"""
Helper functions for CP apps
"""
import six
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
from cherrypy._cpcompat import text_or_bytes
import cherrypy
def expose(func=None, alias=None):
"""
Expose the function or class, optionally providing an alias or set of aliases.
"""
def expose_(func):
func.exposed = True
if alias is not None:
if isinstance(alias, text_or_bytes):
parents[alias.replace('.', '_')] = func
else:
for a in alias:
parents[a.replace('.', '_')] = func
return func
import sys
import types
decoratable_types = types.FunctionType, types.MethodType, type,
if six.PY2:
# Old-style classes are type types.ClassType.
decoratable_types += types.ClassType,
if isinstance(func, decoratable_types):
if alias is None:
# @expose
func.exposed = True
return func
else:
# func = expose(func, alias)
parents = sys._getframe(1).f_locals
return expose_(func)
elif func is None:
if alias is None:
# @expose()
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose(alias="alias") or
# @expose(alias=["alias1", "alias2"])
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose("alias") or
# @expose(["alias1", "alias2"])
parents = sys._getframe(1).f_locals
alias = func
return expose_
def popargs(*args, **kwargs):
"""A decorator for _cp_dispatch
(cherrypy.dispatch.Dispatcher.dispatch_method_name).
Optional keyword argument: handler=(Object or Function)
Provides a _cp_dispatch function that pops off path segments into
cherrypy.request.params under the names specified. The dispatch
is then forwarded on to the next vpath element.
Note that any existing (and exposed) member function of the class that
popargs is applied to will override that value of the argument. For
instance, if you have a method named "list" on the class decorated with
popargs, then accessing "/list" will call that function instead of popping
it off as the requested parameter. This restriction applies to all
_cp_dispatch functions. The only way around this restriction is to create
a "blank class" whose only function is to provide _cp_dispatch.
If there are path elements after the arguments, or more arguments
are requested than are available in the vpath, then the 'handler'
keyword argument specifies the next object to handle the parameterized
request. If handler is not specified or is None, then self is used.
If handler is a function rather than an instance, then that function
will be called with the args specified and the return value from that
function used as the next object INSTEAD of adding the parameters to
cherrypy.request.args.
This decorator may be used in one of two ways:
As a class decorator:
@cherrypy.popargs('year', 'month', 'day')
class Blog:
def index(self, year=None, month=None, day=None):
#Process the parameters here; any url like
#/, /2009, /2009/12, or /2009/12/31
#will fill in the appropriate parameters.
def create(self):
#This link will still be available at /create. Defined functions
#take precedence over arguments.
Or as a member of a class:
class Blog:
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
#...
The handler argument may be used to mix arguments with built in functions.
For instance, the following setup allows different activities at the
day, month, and year level:
class DayHandler:
def index(self, year, month, day):
#Do something with this day; probably list entries
def delete(self, year, month, day):
#Delete all entries for this day
@cherrypy.popargs('day', handler=DayHandler())
class MonthHandler:
def index(self, year, month):
#Do something with this month; probably list entries
def delete(self, year, month):
#Delete all entries for this month
@cherrypy.popargs('month', handler=MonthHandler())
class YearHandler:
def index(self, year):
#Do something with this year
#...
@cherrypy.popargs('year', handler=YearHandler())
class Root:
def index(self):
#...
"""
# Since keyword arg comes after *args, we have to process it ourselves
# for lower versions of python.
handler = None
handler_call = False
for k, v in kwargs.items():
if k == 'handler':
handler = v
else:
raise TypeError(
"cherrypy.popargs() got an unexpected keyword argument '{0}'"
.format(k)
)
import inspect
if handler is not None \
and (hasattr(handler, '__call__') or inspect.isclass(handler)):
handler_call = True
def decorated(cls_or_self=None, vpath=None):
if inspect.isclass(cls_or_self):
# cherrypy.popargs is a class decorator
cls = cls_or_self
setattr(cls, cherrypy.dispatch.Dispatcher.dispatch_method_name, decorated)
return cls
# We're in the actual function
self = cls_or_self
parms = {}
for arg in args:
if not vpath:
break
parms[arg] = vpath.pop(0)
if handler is not None:
if handler_call:
return handler(**parms)
else:
cherrypy.request.params.update(parms)
return handler
cherrypy.request.params.update(parms)
# If we are the ultimate handler, then to prevent our _cp_dispatch
# from being called again, we will resolve remaining elements through
# getattr() directly.
if vpath:
return getattr(self, vpath.pop(0), None)
else:
return self
return decorated
def url(path='', qs='', script_name=None, base=None, relative=None):
"""Create an absolute URL for the given path.
If 'path' starts with a slash ('/'), this will return
(base + script_name + path + qs).
If it does not start with a slash, this returns
(base + script_name [+ request.path_info] + path + qs).
If script_name is None, cherrypy.request will be used
to find a script_name, if available.
If base is None, cherrypy.request.base will be used (if available).
Note that you can use cherrypy.tools.proxy to change this.
Finally, note that this function can be used to obtain an absolute URL
for the current request path (minus the querystring) by passing no args.
If you call url(qs=cherrypy.request.query_string), you should get the
original browser URL (assuming no internal redirections).
If relative is None or not provided, request.app.relative_urls will
be used (if available, else False). If False, the output will be an
absolute URL (including the scheme, host, vhost, and script_name).
If True, the output will instead be a URL that is relative to the
current request path, perhaps including '..' atoms. If relative is
the string 'server', the output will instead be a URL that is
relative to the server root; i.e., it will start with a slash.
"""
if isinstance(qs, (tuple, list, dict)):
qs = _urlencode(qs)
if qs:
qs = '?' + qs
if cherrypy.request.app:
if not path.startswith('/'):
# Append/remove trailing slash from path_info as needed
# (this is to support mistyped URL's without redirecting;
# if you want to redirect, use tools.trailing_slash).
pi = cherrypy.request.path_info
if cherrypy.request.is_index is True:
if not pi.endswith('/'):
pi = pi + '/'
elif cherrypy.request.is_index is False:
if pi.endswith('/') and pi != '/':
pi = pi[:-1]
if path == '':
path = pi
else:
path = _urljoin(pi, path)
if script_name is None:
script_name = cherrypy.request.script_name
if base is None:
base = cherrypy.request.base
newurl = base + script_name + path + qs
else:
# No request.app (we're being called outside a request).
# We'll have to guess the base from server.* attributes.
# This will produce very different results from the above
# if you're using vhosts or tools.proxy.
if base is None:
base = cherrypy.server.base()
path = (script_name or '') + path
newurl = base + path + qs
if './' in newurl:
# Normalize the URL by removing ./ and ../
atoms = []
for atom in newurl.split('/'):
if atom == '.':
pass
elif atom == '..':
atoms.pop()
else:
atoms.append(atom)
newurl = '/'.join(atoms)
# At this point, we should have a fully-qualified absolute URL.
if relative is None:
relative = getattr(cherrypy.request.app, 'relative_urls', False)
# See http://www.ietf.org/rfc/rfc2396.txt
if relative == 'server':
# "A relative reference beginning with a single slash character is
# termed an absolute-path reference, as defined by <abs_path>..."
# This is also sometimes called "server-relative".
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
elif relative:
# "A relative reference that does not begin with a scheme name
# or a slash character is termed a relative-path reference."
old = url(relative=False).split('/')[:-1]
new = newurl.split('/')
while old and new:
a, b = old[0], new[0]
if a != b:
break
old.pop(0)
new.pop(0)
new = (['..'] * len(old)) + new
newurl = '/'.join(new)
return newurl

View File

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

106
cherrypy/daemon.py Executable file
View File

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

View File

@@ -1,18 +1,56 @@
"""CherryPy Library"""
# Deprecated in CherryPy 3.2 -- remove in CherryPy 3.3
from cherrypy.lib.reprconf import unrepr, modules, attributes
def is_iterator(obj):
'''Returns a boolean indicating if the object provided implements
the iterator protocol (i.e. like a generator). This will return
false for objects which iterable, but not iterators themselves.'''
from types import GeneratorType
if isinstance(obj, GeneratorType):
return True
elif not hasattr(obj, '__iter__'):
return False
else:
# Types which implement the protocol must return themselves when
# invoking 'iter' upon them.
return iter(obj) is obj
def is_closable_iterator(obj):
# Not an iterator.
if not is_iterator(obj):
return False
# A generator - the easiest thing to deal with.
import inspect
if inspect.isgenerator(obj):
return True
# A custom iterator. Look for a close method...
if not (hasattr(obj, 'close') and callable(obj.close)):
return False
# ... which doesn't require any arguments.
try:
inspect.getcallargs(obj.close)
except TypeError:
return False
else:
return True
class file_generator(object):
"""Yield the given input (a file object) in chunks (default 64k). (Core)"""
def __init__(self, input, chunkSize=65536):
self.input = input
self.chunkSize = chunkSize
def __iter__(self):
return self
def __next__(self):
chunk = self.input.read(self.chunkSize)
if chunk:
@@ -23,6 +61,7 @@ class file_generator(object):
raise StopIteration()
next = __next__
def file_generator_limited(fileobj, count, chunk_size=65536):
"""Yield the given file object in chunks, stopping after `count`
bytes has been emitted. Default chunk size is 64kB. (Core)
@@ -36,10 +75,11 @@ def file_generator_limited(fileobj, count, chunk_size=65536):
remaining -= chunklen
yield chunk
def set_vary_header(response, header_name):
"Add a Vary header to a response"
varies = response.headers.get("Vary", "")
varies = [x.strip() for x in varies.split(",") if x.strip()]
'Add a Vary header to a response'
varies = response.headers.get('Vary', '')
varies = [x.strip() for x in varies.split(',') if x.strip()]
if header_name not in varies:
varies.append(header_name)
response.headers['Vary'] = ", ".join(varies)
response.headers['Vary'] = ', '.join(varies)

View File

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

View File

@@ -2,8 +2,15 @@
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
import binascii
import cherrypy
from cherrypy._cpcompat import base64_decode
__doc__ = """This module provides a CherryPy 3.x tool which implements
the server-side of HTTP Basic Access Authentication, as described in :rfc:`2617`.
the server-side of HTTP Basic Access Authentication, as described in
:rfc:`2617`.
Example usage, using the built-in checkpassword_dict function which uses a dict
as the credentials store::
@@ -21,10 +28,6 @@ as the credentials store::
__author__ = 'visteya'
__date__ = 'April 2009'
import binascii
from cherrypy._cpcompat import base64_decode
import cherrypy
def checkpassword_dict(user_password_dict):
"""Returns a checkpassword function which checks credentials
@@ -60,16 +63,17 @@ def basic_auth(realm, checkpassword, debug=False):
username and password are the values obtained from the request's
'authorization' header. If authentication succeeds, checkpassword
returns True, else it returns False.
"""
if '"' in realm:
raise ValueError('Realm cannot contain the " (quote) character.')
request = cherrypy.serving.request
auth_header = request.headers.get('authorization')
if auth_header is not None:
try:
# split() error, base64.decodestring() error
with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, 'Bad Request'):
scheme, params = auth_header.split(' ', 1)
if scheme.lower() == 'basic':
username, password = base64_decode(params).split(':', 1)
@@ -77,11 +81,10 @@ def basic_auth(realm, checkpassword, debug=False):
if debug:
cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
request.login = username
return # successful authentication
except (ValueError, binascii.Error): # split() error, base64.decodestring() error
raise cherrypy.HTTPError(400, 'Bad Request')
# Respond with 401 status and a WWW-Authenticate header
cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
return # successful authentication
# Respond with 401 status and a WWW-Authenticate header
cherrypy.serving.response.headers[
'www-authenticate'] = 'Basic realm="%s"' % realm
raise cherrypy.HTTPError(
401, 'You are not authorized to access that resource')

View File

@@ -2,6 +2,13 @@
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
import time
from hashlib import md5
import cherrypy
from cherrypy._cpcompat import ntob, parse_http_list, parse_keqv_list
__doc__ = """An implementation of the server-side of HTTP Digest Access
Authentication, which is described in :rfc:`2617`.
@@ -22,11 +29,6 @@ __author__ = 'visteya'
__date__ = 'April 2009'
import time
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
import cherrypy
from cherrypy._cpcompat import md5, ntob
md5_hex = lambda s: md5(ntob(s)).hexdigest()
qop_auth = 'auth'
@@ -41,6 +43,8 @@ def TRACE(msg):
# Three helper functions for users of the tool, providing three variants
# of get_ha1() functions for three different kinds of credential stores.
def get_ha1_dict_plain(user_password_dict):
"""Returns a get_ha1 function which obtains a plaintext password from a
dictionary of the form: {username : password}.
@@ -57,6 +61,7 @@ def get_ha1_dict_plain(user_password_dict):
return get_ha1
def get_ha1_dict(user_ha1_dict):
"""Returns a get_ha1 function which obtains a HA1 password hash from a
dictionary of the form: {username : HA1}.
@@ -67,10 +72,11 @@ def get_ha1_dict(user_ha1_dict):
argument to digest_auth().
"""
def get_ha1(realm, username):
return user_ha1_dict.get(user)
return user_ha1_dict.get(username)
return get_ha1
def get_ha1_file_htdigest(filename):
"""Returns a get_ha1 function which obtains a HA1 password hash from a
flat file with lines of the same format as that produced by the Apache
@@ -99,18 +105,19 @@ def get_ha1_file_htdigest(filename):
def synthesize_nonce(s, key, timestamp=None):
"""Synthesize a nonce value which resists spoofing and can be checked for staleness.
Returns a string suitable as the value for 'nonce' in the www-authenticate header.
"""Synthesize a nonce value which resists spoofing and can be checked
for staleness. Returns a string suitable as the value for 'nonce' in
the www-authenticate header.
s
A string related to the resource, such as the hostname of the server.
key
A secret string known only to the server.
timestamp
An integer seconds-since-the-epoch timestamp
"""
if timestamp is None:
timestamp = int(time.time())
@@ -125,6 +132,7 @@ def H(s):
class HttpDigestAuthorization (object):
"""Class to parse a Digest Authorization header and perform re-calculation
of the digest.
"""
@@ -135,7 +143,7 @@ class HttpDigestAuthorization (object):
def __init__(self, auth_header, http_method, debug=False):
self.http_method = http_method
self.debug = debug
scheme, params = auth_header.split(" ", 1)
scheme, params = auth_header.split(' ', 1)
self.scheme = scheme.lower()
if self.scheme != 'digest':
raise ValueError('Authorization scheme is not "Digest"')
@@ -151,98 +159,109 @@ class HttpDigestAuthorization (object):
self.nonce = paramsd.get('nonce')
self.uri = paramsd.get('uri')
self.method = paramsd.get('method')
self.response = paramsd.get('response') # the response digest
self.algorithm = paramsd.get('algorithm', 'MD5')
self.response = paramsd.get('response') # the response digest
self.algorithm = paramsd.get('algorithm', 'MD5').upper()
self.cnonce = paramsd.get('cnonce')
self.opaque = paramsd.get('opaque')
self.qop = paramsd.get('qop') # qop
self.nc = paramsd.get('nc') # nonce count
self.qop = paramsd.get('qop') # qop
self.nc = paramsd.get('nc') # nonce count
# perform some correctness checks
if self.algorithm not in valid_algorithms:
raise ValueError(self.errmsg("Unsupported value for algorithm: '%s'" % self.algorithm))
raise ValueError(
self.errmsg("Unsupported value for algorithm: '%s'" %
self.algorithm))
has_reqd = self.username and \
self.realm and \
self.nonce and \
self.uri and \
self.response
has_reqd = (
self.username and
self.realm and
self.nonce and
self.uri and
self.response
)
if not has_reqd:
raise ValueError(self.errmsg("Not all required parameters are present."))
raise ValueError(
self.errmsg('Not all required parameters are present.'))
if self.qop:
if self.qop not in valid_qops:
raise ValueError(self.errmsg("Unsupported value for qop: '%s'" % self.qop))
raise ValueError(
self.errmsg("Unsupported value for qop: '%s'" % self.qop))
if not (self.cnonce and self.nc):
raise ValueError(self.errmsg("If qop is sent then cnonce and nc MUST be present"))
raise ValueError(
self.errmsg('If qop is sent then '
'cnonce and nc MUST be present'))
else:
if self.cnonce or self.nc:
raise ValueError(self.errmsg("If qop is not sent, neither cnonce nor nc can be present"))
raise ValueError(
self.errmsg('If qop is not sent, '
'neither cnonce nor nc can be present'))
def __str__(self):
return 'authorization : %s' % self.auth_header
def validate_nonce(self, s, key):
"""Validate the nonce.
Returns True if nonce was generated by synthesize_nonce() and the timestamp
is not spoofed, else returns False.
Returns True if nonce was generated by synthesize_nonce() and the
timestamp is not spoofed, else returns False.
s
A string related to the resource, such as the hostname of the server.
A string related to the resource, such as the hostname of
the server.
key
A secret string known only to the server.
Both s and key must be the same values which were used to synthesize the nonce
we are trying to validate.
Both s and key must be the same values which were used to synthesize
the nonce we are trying to validate.
"""
try:
timestamp, hashpart = self.nonce.split(':', 1)
s_timestamp, s_hashpart = synthesize_nonce(s, key, timestamp).split(':', 1)
s_timestamp, s_hashpart = synthesize_nonce(
s, key, timestamp).split(':', 1)
is_valid = s_hashpart == hashpart
if self.debug:
TRACE('validate_nonce: %s' % is_valid)
return is_valid
except ValueError: # split() error
except ValueError: # split() error
pass
return False
def is_nonce_stale(self, max_age_seconds=600):
"""Returns True if a validated nonce is stale. The nonce contains a
timestamp in plaintext and also a secure hash of the timestamp. You should
first validate the nonce to ensure the plaintext timestamp is not spoofed.
timestamp in plaintext and also a secure hash of the timestamp.
You should first validate the nonce to ensure the plaintext
timestamp is not spoofed.
"""
try:
timestamp, hashpart = self.nonce.split(':', 1)
if int(timestamp) + max_age_seconds > int(time.time()):
return False
except ValueError: # int() error
except ValueError: # int() error
pass
if self.debug:
TRACE("nonce is stale")
TRACE('nonce is stale')
return True
def HA2(self, entity_body=''):
"""Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3."""
# RFC 2617 3.2.2.3
# If the "qop" directive's value is "auth" or is unspecified, then A2 is:
# If the "qop" directive's value is "auth" or is unspecified,
# then A2 is:
# A2 = method ":" digest-uri-value
#
# If the "qop" value is "auth-int", then A2 is:
# A2 = method ":" digest-uri-value ":" H(entity-body)
if self.qop is None or self.qop == "auth":
if self.qop is None or self.qop == 'auth':
a2 = '%s:%s' % (self.http_method, self.uri)
elif self.qop == "auth-int":
a2 = "%s:%s:%s" % (self.http_method, self.uri, H(entity_body))
elif self.qop == 'auth-int':
a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body))
else:
# in theory, this should never happen, since I validate qop in __init__()
raise ValueError(self.errmsg("Unrecognized value for qop!"))
# in theory, this should never happen, since I validate qop in
# __init__()
raise ValueError(self.errmsg('Unrecognized value for qop!'))
return H(a2)
def request_digest(self, ha1, entity_body=''):
"""Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
@@ -253,22 +272,24 @@ class HttpDigestAuthorization (object):
If 'qop' is set to 'auth-int', then A2 includes a hash
of the "entity body". The entity body is the part of the
message which follows the HTTP headers. See :rfc:`2617` section
4.3. This refers to the entity the user agent sent in the request which
has the Authorization header. Typically GET requests don't have an entity,
and POST requests do.
4.3. This refers to the entity the user agent sent in the
request which has the Authorization header. Typically GET
requests don't have an entity, and POST requests do.
"""
ha2 = self.HA2(entity_body)
# Request-Digest -- RFC 2617 3.2.2.1
if self.qop:
req = "%s:%s:%s:%s:%s" % (self.nonce, self.nc, self.cnonce, self.qop, ha2)
req = '%s:%s:%s:%s:%s' % (
self.nonce, self.nc, self.cnonce, self.qop, ha2)
else:
req = "%s:%s" % (self.nonce, ha2)
req = '%s:%s' % (self.nonce, ha2)
# RFC 2617 3.2.2.2
#
# If the "algorithm" directive's value is "MD5" or is unspecified, then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
# If the "algorithm" directive's value is "MD5" or is unspecified,
# then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
#
# If the "algorithm" directive's value is "MD5-sess", then A1 is
# calculated only once - on the first request by the client following
@@ -282,8 +303,8 @@ class HttpDigestAuthorization (object):
return digest
def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stale=False):
def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
stale=False):
"""Constructs a WWW-Authenticate header for Digest authentication."""
if qop not in valid_qops:
raise ValueError("Unsupported value for qop: '%s'" % qop)
@@ -293,7 +314,7 @@ def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stal
if nonce is None:
nonce = synthesize_nonce(realm, key)
s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
realm, nonce, algorithm, qop)
realm, nonce, algorithm, qop)
if stale:
s += ', stale="true"'
return s
@@ -302,16 +323,16 @@ def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stal
def digest_auth(realm, get_ha1, key, debug=False):
"""A CherryPy tool which hooks at before_handler to perform
HTTP Digest Access Authentication, as specified in :rfc:`2617`.
If the request has an 'authorization' header with a 'Digest' scheme, this
tool authenticates the credentials supplied in that header. If
the request has no 'authorization' header, or if it does but the scheme is
not "Digest", or if authentication fails, the tool sends a 401 response with
a 'WWW-Authenticate' Digest header.
If the request has an 'authorization' header with a 'Digest' scheme,
this tool authenticates the credentials supplied in that header.
If the request has no 'authorization' header, or if it does but the
scheme is not "Digest", or if authentication fails, the tool sends
a 401 response with a 'WWW-Authenticate' Digest header.
realm
A string containing the authentication realm.
get_ha1
A callable which looks up a username in a credentials store
and returns the HA1 string, which is defined in the RFC to be
@@ -320,46 +341,50 @@ def digest_auth(realm, get_ha1, key, debug=False):
where username is obtained from the request's 'authorization' header.
If username is not found in the credentials store, get_ha1() returns
None.
key
A secret string known only to the server, used in the synthesis of nonces.
A secret string known only to the server, used in the synthesis
of nonces.
"""
request = cherrypy.serving.request
auth_header = request.headers.get('authorization')
nonce_is_stale = False
if auth_header is not None:
try:
auth = HttpDigestAuthorization(auth_header, request.method, debug=debug)
except ValueError:
raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.")
with cherrypy.HTTPError.handle(ValueError, 400,
'The Authorization header could not be parsed.'):
auth = HttpDigestAuthorization(
auth_header, request.method, debug=debug)
if debug:
TRACE(str(auth))
if auth.validate_nonce(realm, key):
ha1 = get_ha1(realm, auth.username)
if ha1 is not None:
# note that for request.body to be available we need to hook in at
# before_handler, not on_start_resource like 3.1.x digest_auth does.
# note that for request.body to be available we need to
# hook in at before_handler, not on_start_resource like
# 3.1.x digest_auth does.
digest = auth.request_digest(ha1, entity_body=request.body)
if digest == auth.response: # authenticated
if digest == auth.response: # authenticated
if debug:
TRACE("digest matches auth.response")
TRACE('digest matches auth.response')
# Now check if nonce is stale.
# The choice of ten minutes' lifetime for nonce is somewhat arbitrary
# The choice of ten minutes' lifetime for nonce is somewhat
# arbitrary
nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600)
if not nonce_is_stale:
request.login = auth.username
if debug:
TRACE("authentication of %s successful" % auth.username)
TRACE('authentication of %s successful' %
auth.username)
return
# Respond with 401 status and a WWW-Authenticate header
header = www_authenticate(realm, key, stale=nonce_is_stale)
if debug:
TRACE(header)
cherrypy.serving.response.headers['WWW-Authenticate'] = header
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
raise cherrypy.HTTPError(
401, 'You are not authorized to access that resource')

View File

@@ -1,7 +1,7 @@
"""
CherryPy implements a simple caching system as a pluggable Tool. This tool tries
to be an (in-process) HTTP/1.1-compliant cache. It's not quite there yet, but
it's probably good enough for most sites.
CherryPy implements a simple caching system as a pluggable Tool. This tool
tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there
yet, but it's probably good enough for most sites.
In general, GET responses are cached (along with selecting headers) and, if
another request arrives for the same resource, the caching Tool will return 304
@@ -9,8 +9,8 @@ Not Modified if possible, or serve the cached response otherwise. It also sets
request.cached to True if serving a cached representation, and sets
request.cacheable to False (so it doesn't get cached again).
If POST, PUT, or DELETE requests are made for a cached resource, they invalidate
(delete) any cached response.
If POST, PUT, or DELETE requests are made for a cached resource, they
invalidate (delete) any cached response.
Usage
=====
@@ -39,58 +39,58 @@ import time
import cherrypy
from cherrypy.lib import cptools, httputil
from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted
from cherrypy._cpcompat import copyitems, ntob, sorted, Event
class Cache(object):
"""Base class for Cache implementations."""
def get(self):
"""Return the current variant if in the cache, else None."""
raise NotImplemented
def put(self, obj, size):
"""Store the current variant in the cache."""
raise NotImplemented
def delete(self):
"""Remove ALL cached variants of the current resource."""
raise NotImplemented
def clear(self):
"""Reset the cache to its initial, empty state."""
raise NotImplemented
# ------------------------------- Memory Cache ------------------------------- #
# ------------------------------ Memory Cache ------------------------------- #
class AntiStampedeCache(dict):
"""A storage system for cached items which reduces stampede collisions."""
def wait(self, key, timeout=5, debug=False):
"""Return the cached value for the given key, or None.
If timeout is not None, and the value is already
being calculated by another thread, wait until the given timeout has
elapsed. If the value is available before the timeout expires, it is
returned. If not, None is returned, and a sentinel placed in the cache
to signal other threads to wait.
If timeout is None, no waiting is performed nor sentinels used.
"""
value = self.get(key)
if isinstance(value, threading._Event):
if isinstance(value, Event):
if timeout is None:
# Ignore the other thread and recalc it ourselves.
if debug:
cherrypy.log('No timeout', 'TOOLS.CACHING')
return None
# Wait until it's done or times out.
if debug:
cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING')
cherrypy.log('Waiting up to %s seconds' %
timeout, 'TOOLS.CACHING')
value.wait(timeout)
if value.result is not None:
# The other thread finished its calculation. Use it.
@@ -104,7 +104,7 @@ class AntiStampedeCache(dict):
e = threading.Event()
e.result = None
dict.__setitem__(self, key, e)
return None
elif value is None:
# Stick an Event in the slot so other threads wait
@@ -115,12 +115,12 @@ class AntiStampedeCache(dict):
e.result = None
dict.__setitem__(self, key, e)
return value
def __setitem__(self, key, value):
"""Set the cached value for the given key."""
existing = self.get(key)
dict.__setitem__(self, key, value)
if isinstance(existing, threading._Event):
if isinstance(existing, Event):
# Set Event.result so other threads waiting on it have
# immediate access without needing to poll the cache again.
existing.result = value
@@ -128,49 +128,51 @@ class AntiStampedeCache(dict):
class MemoryCache(Cache):
"""An in-memory cache for varying response content.
Each key in self.store is a URI, and each value is an AntiStampedeCache.
The response for any given URI may vary based on the values of
"selecting request headers"; that is, those named in the Vary
response header. We assume the list of header names to be constant
for each URI throughout the lifetime of the application, and store
that list in ``self.store[uri].selecting_headers``.
The items contained in ``self.store[uri]`` have keys which are tuples of
request header values (in the same order as the names in its
selecting_headers), and values which are the actual responses.
"""
maxobjects = 1000
"""The maximum number of cached objects; defaults to 1000."""
maxobj_size = 100000
"""The maximum size of each cached object in bytes; defaults to 100 KB."""
maxsize = 10000000
"""The maximum size of the entire cache in bytes; defaults to 10 MB."""
delay = 600
"""Seconds until the cached content expires; defaults to 600 (10 minutes)."""
"""Seconds until the cached content expires; defaults to 600 (10 minutes).
"""
antistampede_timeout = 5
"""Seconds to wait for other threads to release a cache lock."""
expire_freq = 0.1
"""Seconds to sleep between cache expiration sweeps."""
debug = False
def __init__(self):
self.clear()
# Run self.expire_cache in a separate daemon thread.
t = threading.Thread(target=self.expire_cache, name='expire_cache')
self.expiration_thread = t
set_daemon(t, True)
t.daemon = True
t.start()
def clear(self):
"""Reset the cache to its initial, empty state."""
self.store = {}
@@ -181,10 +183,10 @@ class MemoryCache(Cache):
self.tot_expires = 0
self.tot_non_modified = 0
self.cursize = 0
def expire_cache(self):
"""Continuously examine cached objects, expiring stale ones.
This function is designed to be run in its own daemon thread,
referenced at ``self.expiration_thread``.
"""
@@ -207,17 +209,17 @@ class MemoryCache(Cache):
pass
del self.expirations[expiration_time]
time.sleep(self.expire_freq)
def get(self):
"""Return the current variant if in the cache, else None."""
request = cherrypy.serving.request
self.tot_gets += 1
uri = cherrypy.url(qs=request.query_string)
uricache = self.store.get(uri)
if uricache is None:
return None
header_values = [request.headers.get(h, '')
for h in uricache.selecting_headers]
variant = uricache.wait(key=tuple(sorted(header_values)),
@@ -226,12 +228,12 @@ class MemoryCache(Cache):
if variant is not None:
self.tot_hist += 1
return variant
def put(self, variant, size):
"""Store the current variant in the cache."""
request = cherrypy.serving.request
response = cherrypy.serving.response
uri = cherrypy.url(qs=request.query_string)
uricache = self.store.get(uri)
if uricache is None:
@@ -239,38 +241,38 @@ class MemoryCache(Cache):
uricache.selecting_headers = [
e.value for e in response.headers.elements('Vary')]
self.store[uri] = uricache
if len(self.store) < self.maxobjects:
total_size = self.cursize + size
# checks if there's space for the object
if (size < self.maxobj_size and total_size < self.maxsize):
# add to the expirations list
expiration_time = response.time + self.delay
bucket = self.expirations.setdefault(expiration_time, [])
bucket.append((size, uri, uricache.selecting_headers))
# add to the cache
header_values = [request.headers.get(h, '')
for h in uricache.selecting_headers]
uricache[tuple(sorted(header_values))] = variant
self.tot_puts += 1
self.cursize = total_size
def delete(self):
"""Remove ALL cached variants of the current resource."""
uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
self.store.pop(uri, None)
def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs):
"""Try to obtain cached output. If fresh enough, raise HTTPError(304).
If POST, PUT, or DELETE:
* invalidates (deletes) any cached response for this resource
* sets request.cached = False
* sets request.cacheable = False
else if a cached copy exists:
* sets request.cached = True
* sets request.cacheable = False
@@ -280,7 +282,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
if necessary.
* sets response.status and response.body to the cached values
* returns True
otherwise:
* sets request.cached = False
* sets request.cacheable = True
@@ -288,16 +290,16 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
"""
request = cherrypy.serving.request
response = cherrypy.serving.response
if not hasattr(cherrypy, "_cache"):
if not hasattr(cherrypy, '_cache'):
# Make a process-wide Cache object.
cherrypy._cache = kwargs.pop("cache_class", MemoryCache)()
cherrypy._cache = kwargs.pop('cache_class', MemoryCache)()
# Take all remaining kwargs and set them on the Cache object.
for k, v in kwargs.items():
setattr(cherrypy._cache, k, v)
cherrypy._cache.debug = debug
# POST, PUT, DELETE should invalidate (delete) the cached copy.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
if request.method in invalid_methods:
@@ -308,12 +310,12 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request.cached = False
request.cacheable = False
return False
if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
request.cached = False
request.cacheable = True
return False
cache_data = cherrypy._cache.get()
request.cached = bool(cache_data)
request.cacheable = not request.cached
@@ -325,17 +327,19 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
directive = atoms.pop(0)
if directive == 'max-age':
if len(atoms) != 1 or not atoms[0].isdigit():
raise cherrypy.HTTPError(400, "Invalid Cache-Control header")
raise cherrypy.HTTPError(
400, 'Invalid Cache-Control header')
max_age = int(atoms[0])
break
elif directive == 'no-cache':
if debug:
cherrypy.log('Ignoring cache due to Cache-Control: no-cache',
'TOOLS.CACHING')
cherrypy.log(
'Ignoring cache due to Cache-Control: no-cache',
'TOOLS.CACHING')
request.cached = False
request.cacheable = True
return False
if debug:
cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
s, h, b, create_time = cache_data
@@ -347,15 +351,16 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request.cached = False
request.cacheable = True
return False
# Copy the response headers. See http://www.cherrypy.org/ticket/721.
# Copy the response headers. See
# https://github.com/cherrypy/cherrypy/issues/721.
response.headers = rh = httputil.HeaderMap()
for k in h:
dict.__setitem__(rh, k, dict.__getitem__(h, k))
# Add the required Age header
response.headers["Age"] = str(age)
response.headers['Age'] = str(age)
try:
# Note that validate_since depends on a Last-Modified header;
# this was put into the cached copy, and should have been
@@ -366,7 +371,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
if x.status == 304:
cherrypy._cache.tot_non_modified += 1
raise
# serve it & get out from the request
response.status = s
response.body = b
@@ -379,29 +384,29 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
def tee_output():
"""Tee response output to cache storage. Internal."""
# Used by CachingTool by attaching to request.hooks
request = cherrypy.serving.request
if 'no-store' in request.headers.values('Cache-Control'):
return
def tee(body):
"""Tee response.body into a list."""
if ('no-cache' in response.headers.values('Pragma') or
'no-store' in response.headers.values('Cache-Control')):
'no-store' in response.headers.values('Cache-Control')):
for chunk in body:
yield chunk
return
output = []
for chunk in body:
output.append(chunk)
yield chunk
# save the cache data
body = ntob('').join(output)
cherrypy._cache.put((response.status, response.headers or {},
body, response.time), len(body))
response = cherrypy.serving.response
response.body = tee(response.body)
@@ -415,25 +420,25 @@ def expires(secs=0, force=False, debug=False):
expire. The 'Expires' header will be set to response.time + secs.
If secs is zero, the 'Expires' header is set one year in the past, and
the following "cache prevention" headers are also set:
* Pragma: no-cache
* Cache-Control': no-cache, must-revalidate
force
If False, the following headers are checked:
* Etag
* Last-Modified
* Age
* Expires
If any are already present, none of the above response headers are set.
"""
response = cherrypy.serving.response
headers = response.headers
cacheable = False
if not force:
# some header names that indicate that the response can be cached
@@ -441,7 +446,7 @@ def expires(secs=0, force=False, debug=False):
if indicator in headers:
cacheable = True
break
if not cacheable and not force:
if debug:
cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
@@ -450,16 +455,16 @@ def expires(secs=0, force=False, debug=False):
cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
if isinstance(secs, datetime.timedelta):
secs = (86400 * secs.days) + secs.seconds
if secs == 0:
if force or ("Pragma" not in headers):
headers["Pragma"] = "no-cache"
if force or ('Pragma' not in headers):
headers['Pragma'] = 'no-cache'
if cherrypy.serving.request.protocol >= (1, 1):
if force or "Cache-Control" not in headers:
headers["Cache-Control"] = "no-cache, must-revalidate"
if force or 'Cache-Control' not in headers:
headers['Cache-Control'] = 'no-cache, must-revalidate'
# Set an explicit Expires date in the past.
expiry = httputil.HTTPDate(1169942400.0)
else:
expiry = httputil.HTTPDate(response.time + secs)
if force or "Expires" not in headers:
headers["Expires"] = expiry
if force or 'Expires' not in headers:
headers['Expires'] = expiry

View File

@@ -1,7 +1,7 @@
"""Code-coverage tools for CherryPy.
To use this module, or the coverage tools in the test suite,
you need to download 'coverage.py', either Gareth Rees' `original
you need to download 'coverage.py', either Gareth Rees' `original
implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
or Ned Batchelder's `enhanced version:
<http://www.nedbatchelder.com/code/modules/coverage.html>`_
@@ -23,24 +23,32 @@ it will call ``serve()`` for you.
import re
import sys
import cgi
import os
import os.path
import cherrypy
from cherrypy._cpcompat import quote_plus
import os, os.path
localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache')
the_coverage = None
try:
from coverage import coverage
the_coverage = coverage(data_file=localFile)
def start():
the_coverage.start()
except ImportError:
# Setting the_coverage to None will raise errors
# that need to be trapped downstream.
the_coverage = None
import warnings
warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
warnings.warn(
'No code coverage will be performed; '
'coverage.py could not be imported.')
def start():
pass
start.priority = 20
@@ -69,7 +77,7 @@ TEMPLATE_MENU = """<html>
font-size: small;
font-weight: bold;
font-style: italic;
margin-top: 5px;
margin-top: 5px;
}
input { border: 1px solid #ccc; padding: 2px; }
.directory {
@@ -118,15 +126,18 @@ TEMPLATE_FORM = """
<div id="options">
<form action='menu' method=GET>
<input type='hidden' name='base' value='%(base)s' />
Show percentages <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
Show percentages
<input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
Hide files over
<input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
Exclude files matching<br />
<input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' />
<input type='text' id='exclude' name='exclude'
value='%(exclude)s' size='20' />
<br />
<input type='submit' value='Change view' id="submit"/>
</form>
</div>"""
</div>"""
TEMPLATE_FRAMESET = """<html>
<head><title>CherryPy coverage data</title></head>
@@ -173,7 +184,10 @@ TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
<td>%s</td>
</tr>\n"""
TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
TEMPLATE_ITEM = (
"%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
)
def _percent(statements, missing):
s = len(statements)
@@ -182,24 +196,31 @@ def _percent(statements, missing):
return int(round(100.0 * e / s))
return 0
def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
def _show_branch(root, base, path, pct=0, showpct=False, exclude='',
coverage=the_coverage):
# Show the directory name and any of our children
dirs = [k for k, v in root.items() if v]
dirs.sort()
for name in dirs:
newpath = os.path.join(path, name)
if newpath.lower().startswith(base):
relpath = newpath[len(base):]
yield "| " * relpath.count(os.sep)
yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
(newpath, quote_plus(exclude), name)
for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage):
yield '| ' * relpath.count(os.sep)
yield (
"<a class='directory' "
"href='menu?base=%s&exclude=%s'>%s</a>\n" %
(newpath, quote_plus(exclude), name)
)
for chunk in _show_branch(
root[name], base, newpath, pct, showpct,
exclude, coverage=coverage
):
yield chunk
# Now list the files
if path.lower().startswith(base):
relpath = path[len(base):]
@@ -207,8 +228,8 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
files.sort()
for name in files:
newpath = os.path.join(path, name)
pc_str = ""
pc_str = ''
if showpct:
try:
_, statements, _, missing, _ = coverage.analysis2(newpath)
@@ -217,22 +238,24 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
pass
else:
pc = _percent(statements, missing)
pc_str = ("%3d%% " % pc).replace(' ','&nbsp;')
pc_str = ('%3d%% ' % pc).replace(' ', '&nbsp;')
if pc < float(pct) or pc == -1:
pc_str = "<span class='fail'>%s</span>" % pc_str
else:
pc_str = "<span class='pass'>%s</span>" % pc_str
yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1),
pc_str, newpath, name)
def _skip_file(path, exclude):
if exclude:
return bool(re.search(exclude, path))
def _graft(path, tree):
d = tree
p = path
atoms = []
while True:
@@ -241,14 +264,15 @@ def _graft(path, tree):
break
atoms.append(tail)
atoms.append(p)
if p != "/":
atoms.append("/")
if p != '/':
atoms.append('/')
atoms.reverse()
for node in atoms:
if node:
d = d.setdefault(node, {})
def get_tree(base, exclude, coverage=the_coverage):
"""Return covered module names as a nested dict."""
tree = {}
@@ -258,8 +282,9 @@ def get_tree(base, exclude, coverage=the_coverage):
_graft(path, tree)
return tree
class CoverStats(object):
def __init__(self, coverage, root=None):
self.coverage = coverage
if root is None:
@@ -268,52 +293,53 @@ class CoverStats(object):
import cherrypy
root = os.path.dirname(cherrypy.__file__)
self.root = root
@cherrypy.expose
def index(self):
return TEMPLATE_FRAMESET % self.root.lower()
index.exposed = True
def menu(self, base="/", pct="50", showpct="",
@cherrypy.expose
def menu(self, base='/', pct='50', showpct='',
exclude=r'python\d\.\d|test|tut\d|tutorial'):
# The coverage module uses all-lower-case names.
base = base.lower().rstrip(os.sep)
yield TEMPLATE_MENU
yield TEMPLATE_FORM % locals()
# Start by showing links for parent paths
yield "<div id='crumbs'>"
path = ""
path = ''
atoms = base.split(os.sep)
atoms.pop()
for atom in atoms:
path += atom + os.sep
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
% (path, quote_plus(exclude), atom, os.sep))
yield "</div>"
yield '</div>'
yield "<div id='tree'>"
# Then display the tree
tree = get_tree(base, exclude, self.coverage)
if not tree:
yield "<p>No modules covered.</p>"
yield '<p>No modules covered.</p>'
else:
for chunk in _show_branch(tree, base, "/", pct,
showpct=='checked', exclude, coverage=self.coverage):
for chunk in _show_branch(tree, base, '/', pct,
showpct == 'checked', exclude,
coverage=self.coverage):
yield chunk
yield "</div>"
yield "</body></html>"
menu.exposed = True
yield '</div>'
yield '</body></html>'
def annotated_file(self, filename, statements, excluded, missing):
source = open(filename, 'r')
buffer = []
for lineno, line in enumerate(source.readlines()):
lineno += 1
line = line.strip("\n\r")
line = line.strip('\n\r')
empty_the_buffer = True
if lineno in excluded:
template = TEMPLATE_LOC_EXCLUDED
@@ -329,9 +355,11 @@ class CoverStats(object):
yield template % (lno, cgi.escape(pastline))
buffer = []
yield template % (lineno, cgi.escape(line))
@cherrypy.expose
def report(self, name):
filename, statements, excluded, missing, _ = self.coverage.analysis2(name)
filename, statements, excluded, missing, _ = self.coverage.analysis2(
name)
pc = _percent(statements, missing)
yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
fullpath=name,
@@ -343,23 +371,21 @@ class CoverStats(object):
yield '</table>'
yield '</body>'
yield '</html>'
report.exposed = True
def serve(path=localFile, port=8080, root=None):
if coverage is None:
raise ImportError("The coverage module could not be imported.")
raise ImportError('The coverage module could not be imported.')
from coverage import coverage
cov = coverage(data_file = path)
cov = coverage(data_file=path)
cov.load()
import cherrypy
cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10,
'environment': "production",
'environment': 'production',
})
cherrypy.quickstart(CoverStats(cov, root))
if __name__ == "__main__":
if __name__ == '__main__':
serve(*tuple(sys.argv[1:]))

View File

@@ -21,33 +21,37 @@ to collect stats to import a third-party module. Therefore, we choose to
re-use the `logging` module by adding a `statistics` object to it.
That `logging.statistics` object is a nested dict. It is not a custom class,
because that would 1) require libraries and applications to import a third-
party module in order to participate, 2) inhibit innovation in extrapolation
approaches and in reporting tools, and 3) be slow. There are, however, some
specifications regarding the structure of the dict.
because that would:
{
+----"SQLAlchemy": {
| "Inserts": 4389745,
| "Inserts per Second":
| lambda s: s["Inserts"] / (time() - s["Start"]),
| C +---"Table Statistics": {
| o | "widgets": {-----------+
N | l | "Rows": 1.3M, | Record
a | l | "Inserts": 400, |
m | e | },---------------------+
e | c | "froobles": {
s | t | "Rows": 7845,
p | i | "Inserts": 0,
a | o | },
c | n +---},
e | "Slow Queries":
| [{"Query": "SELECT * FROM widgets;",
| "Processing Time": 47.840923343,
| },
| ],
+----},
}
1. require libraries and applications to import a third-party module in
order to participate
2. inhibit innovation in extrapolation approaches and in reporting tools, and
3. be slow.
There are, however, some specifications regarding the structure of the dict.::
{
+----"SQLAlchemy": {
| "Inserts": 4389745,
| "Inserts per Second":
| lambda s: s["Inserts"] / (time() - s["Start"]),
| C +---"Table Statistics": {
| o | "widgets": {-----------+
N | l | "Rows": 1.3M, | Record
a | l | "Inserts": 400, |
m | e | },---------------------+
e | c | "froobles": {
s | t | "Rows": 7845,
p | i | "Inserts": 0,
a | o | },
c | n +---},
e | "Slow Queries":
| [{"Query": "SELECT * FROM widgets;",
| "Processing Time": 47.840923343,
| },
| ],
+----},
}
The `logging.statistics` dict has four levels. The topmost level is nothing
more than a set of names to introduce modularity, usually along the lines of
@@ -65,13 +69,13 @@ Each namespace, then, is a dict of named statistical values, such as
good on a report: spaces and capitalization are just fine.
In addition to scalars, values in a namespace MAY be a (third-layer)
dict, or a list, called a "collection". For example, the CherryPy StatsTool
keeps track of what each request is doing (or has most recently done)
in a 'Requests' collection, where each key is a thread ID; each
dict, or a list, called a "collection". For example, the CherryPy
:class:`StatsTool` keeps track of what each request is doing (or has most
recently done) in a 'Requests' collection, where each key is a thread ID; each
value in the subdict MUST be a fourth dict (whew!) of statistical data about
each thread. We call each subdict in the collection a "record". Similarly,
the StatsTool also keeps a list of slow queries, where each record contains
data about each slow query, in order.
the :class:`StatsTool` also keeps a list of slow queries, where each record
contains data about each slow query, in order.
Values in a namespace or record may also be functions, which brings us to:
@@ -86,17 +90,17 @@ scalar values you already have on hand.
When it comes time to report on the gathered data, however, we usually have
much more freedom in what we can calculate. Therefore, whenever reporting
tools (like the provided StatsPage CherryPy class) fetch the contents of
`logging.statistics` for reporting, they first call `extrapolate_statistics`
(passing the whole `statistics` dict as the only argument). This makes a
deep copy of the statistics dict so that the reporting tool can both iterate
over it and even change it without harming the original. But it also expands
any functions in the dict by calling them. For example, you might have a
'Current Time' entry in the namespace with the value "lambda scope: time.time()".
The "scope" parameter is the current namespace dict (or record, if we're
currently expanding one of those instead), allowing you access to existing
static entries. If you're truly evil, you can even modify more than one entry
at a time.
tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents
of `logging.statistics` for reporting, they first call
`extrapolate_statistics` (passing the whole `statistics` dict as the only
argument). This makes a deep copy of the statistics dict so that the
reporting tool can both iterate over it and even change it without harming
the original. But it also expands any functions in the dict by calling them.
For example, you might have a 'Current Time' entry in the namespace with the
value "lambda scope: time.time()". The "scope" parameter is the current
namespace dict (or record, if we're currently expanding one of those
instead), allowing you access to existing static entries. If you're truly
evil, you can even modify more than one entry at a time.
However, don't try to calculate an entry and then use its value in further
extrapolations; the order in which the functions are called is not guaranteed.
@@ -108,19 +112,20 @@ After the whole thing has been extrapolated, it's time for:
Reporting
---------
The StatsPage class grabs the `logging.statistics` dict, extrapolates it all,
and then transforms it to HTML for easy viewing. Each namespace gets its own
header and attribute table, plus an extra table for each collection. This is
NOT part of the statistics specification; other tools can format how they like.
The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates
it all, and then transforms it to HTML for easy viewing. Each namespace gets
its own header and attribute table, plus an extra table for each collection.
This is NOT part of the statistics specification; other tools can format how
they like.
You can control which columns are output and how they are formatted by updating
StatsPage.formatting, which is a dict that mirrors the keys and nesting of
`logging.statistics`. The difference is that, instead of data values, it has
formatting values. Use None for a given key to indicate to the StatsPage that a
given column should not be output. Use a string with formatting (such as '%.3f')
to interpolate the value(s), or use a callable (such as lambda v: v.isoformat())
for more advanced formatting. Any entry which is not mentioned in the formatting
dict is output unchanged.
given column should not be output. Use a string with formatting
(such as '%.3f') to interpolate the value(s), or use a callable (such as
lambda v: v.isoformat()) for more advanced formatting. Any entry which is not
mentioned in the formatting dict is output unchanged.
Monitoring
----------
@@ -145,12 +150,12 @@ entries to False or True, if present.
Usage
=====
To collect statistics on CherryPy applications:
To collect statistics on CherryPy applications::
from cherrypy.lib import cpstats
appconfig['/']['tools.cpstats.on'] = True
To collect statistics on your own code:
To collect statistics on your own code::
import logging
# Initialize the repository
@@ -172,20 +177,30 @@ To collect statistics on your own code:
if mystats.get('Enabled', False):
mystats['Important Events'] += 1
To report statistics:
To report statistics::
root.cpstats = cpstats.StatsPage()
To format statistics reports:
To format statistics reports::
See 'Reporting', above.
"""
# -------------------------------- Statistics -------------------------------- #
import logging
if not hasattr(logging, 'statistics'): logging.statistics = {}
import os
import sys
import threading
import time
import cherrypy
from cherrypy._cpcompat import json
# ------------------------------- Statistics -------------------------------- #
if not hasattr(logging, 'statistics'):
logging.statistics = {}
def extrapolate_statistics(scope):
"""Return an extrapolated copy of the given scope."""
@@ -201,22 +216,25 @@ def extrapolate_statistics(scope):
return c
# --------------------- CherryPy Applications Statistics --------------------- #
import threading
import time
import cherrypy
# -------------------- CherryPy Applications Statistics --------------------- #
appstats = logging.statistics.setdefault('CherryPy Applications', {})
appstats.update({
'Enabled': True,
'Bytes Read/Request': lambda s: (s['Total Requests'] and
(s['Total Bytes Read'] / float(s['Total Requests'])) or 0.0),
'Bytes Read/Request': lambda s: (
s['Total Requests'] and
(s['Total Bytes Read'] / float(s['Total Requests'])) or
0.0
),
'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s),
'Bytes Written/Request': lambda s: (s['Total Requests'] and
(s['Total Bytes Written'] / float(s['Total Requests'])) or 0.0),
'Bytes Written/Second': lambda s: s['Total Bytes Written'] / s['Uptime'](s),
'Bytes Written/Request': lambda s: (
s['Total Requests'] and
(s['Total Bytes Written'] / float(s['Total Requests'])) or
0.0
),
'Bytes Written/Second': lambda s: (
s['Total Bytes Written'] / s['Uptime'](s)
),
'Current Time': lambda s: time.time(),
'Current Requests': 0,
'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s),
@@ -228,28 +246,29 @@ appstats.update({
'Total Time': 0,
'Uptime': lambda s: time.time() - s['Start Time'],
'Requests': {},
})
})
proc_time = lambda s: time.time() - s['Start Time']
class ByteCountWrapper(object):
"""Wraps a file-like object, counting the number of bytes read."""
def __init__(self, rfile):
self.rfile = rfile
self.bytes_read = 0
def read(self, size=-1):
data = self.rfile.read(size)
self.bytes_read += len(data)
return data
def readline(self, size=-1):
data = self.rfile.readline(size)
self.bytes_read += len(data)
return data
def readlines(self, sizehint=0):
# Shamelessly stolen from StringIO
total = 0
@@ -262,13 +281,13 @@ class ByteCountWrapper(object):
break
line = self.readline()
return lines
def close(self):
self.rfile.close()
def __iter__(self):
return self
def next(self):
data = self.rfile.next()
self.bytes_read += len(data)
@@ -278,34 +297,40 @@ class ByteCountWrapper(object):
average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0
def _get_threading_ident():
if sys.version_info >= (3, 3):
return threading.get_ident()
return threading._get_ident()
class StatsTool(cherrypy.Tool):
"""Record various information about the current request."""
def __init__(self):
cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
if appstats.get('Enabled', False):
cherrypy.Tool._setup(self)
self.record_start()
def record_start(self):
"""Record the beginning of a request."""
request = cherrypy.serving.request
if not hasattr(request.rfile, 'bytes_read'):
request.rfile = ByteCountWrapper(request.rfile)
request.body.fp = request.rfile
r = request.remote
appstats['Current Requests'] += 1
appstats['Total Requests'] += 1
appstats['Requests'][threading._get_ident()] = {
appstats['Requests'][_get_threading_ident()] = {
'Bytes Read': None,
'Bytes Written': None,
# Use a lambda so the ip gets updated by tools.proxy later
@@ -315,37 +340,39 @@ class StatsTool(cherrypy.Tool):
'Request-Line': request.request_line,
'Response Status': None,
'Start Time': time.time(),
}
}
def record_stop(self, uriset=None, slow_queries=1.0, slow_queries_count=100,
debug=False, **kwargs):
def record_stop(
self, uriset=None, slow_queries=1.0, slow_queries_count=100,
debug=False, **kwargs):
"""Record the end of a request."""
resp = cherrypy.serving.response
w = appstats['Requests'][threading._get_ident()]
w = appstats['Requests'][_get_threading_ident()]
r = cherrypy.request.rfile.bytes_read
w['Bytes Read'] = r
appstats['Total Bytes Read'] += r
if resp.stream:
w['Bytes Written'] = 'chunked'
else:
cl = int(resp.headers.get('Content-Length', 0))
w['Bytes Written'] = cl
appstats['Total Bytes Written'] += cl
w['Response Status'] = getattr(resp, 'output_status', None) or resp.status
w['Response Status'] = getattr(
resp, 'output_status', None) or resp.status
w['End Time'] = time.time()
p = w['End Time'] - w['Start Time']
w['Processing Time'] = p
appstats['Total Time'] += p
appstats['Current Requests'] -= 1
if debug:
cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
if uriset:
rs = appstats.setdefault('URI Set Tracking', {})
r = rs.setdefault(uriset, {
@@ -357,7 +384,7 @@ class StatsTool(cherrypy.Tool):
r['Max'] = p
r['Count'] += 1
r['Sum'] += p
if slow_queries and p > slow_queries:
sq = appstats.setdefault('Slow Queries', [])
sq.append(w.copy())
@@ -365,29 +392,19 @@ class StatsTool(cherrypy.Tool):
sq.pop(0)
import cherrypy
cherrypy.tools.cpstats = StatsTool()
# ---------------------- CherryPy Statistics Reporting ---------------------- #
import os
thisdir = os.path.abspath(os.path.dirname(__file__))
try:
import json
except ImportError:
try:
import simplejson as json
except ImportError:
json = None
missing = object()
locale_date = lambda v: time.strftime('%c', time.gmtime(v))
iso_format = lambda v: time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
def pause_resume(ns):
def _pause_resume(enabled):
pause_disabled = ''
@@ -410,7 +427,7 @@ def pause_resume(ns):
class StatsPage(object):
formatting = {
'CherryPy Applications': {
'Enabled': pause_resume('CherryPy Applications'),
@@ -427,20 +444,20 @@ class StatsPage(object):
'End Time': None,
'Processing Time': '%.3f',
'Start Time': iso_format,
},
},
'URI Set Tracking': {
'Avg': '%.3f',
'Max': '%.3f',
'Min': '%.3f',
'Sum': '%.3f',
},
},
'Requests': {
'Bytes Read': '%s',
'Bytes Written': '%s',
'End Time': None,
'Processing Time': '%.3f',
'Start Time': None,
},
},
},
'CherryPy WSGIServer': {
'Enabled': pause_resume('CherryPy WSGIServer'),
@@ -448,8 +465,8 @@ class StatsPage(object):
'Start time': iso_format,
},
}
@cherrypy.expose
def index(self):
# Transform the raw data into pretty output for HTML
yield """
@@ -500,18 +517,25 @@ table.stats2 th {
""" % title
for i, (key, value) in enumerate(scalars):
colnum = i % 3
if colnum == 0: yield """
if colnum == 0:
yield """
<tr>"""
yield (
"""
<th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" %
vars()
)
if colnum == 2:
yield """
</tr>"""
if colnum == 0:
yield """
<th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" % vars()
if colnum == 2: yield """
</tr>"""
if colnum == 0: yield """
<th></th><td></td>
<th></th><td></td>
</tr>"""
elif colnum == 1: yield """
elif colnum == 1:
yield """
<th></th><td></td>
</tr>"""
yield """
@@ -546,8 +570,7 @@ table.stats2 th {
</body>
</html>
"""
index.exposed = True
def get_namespaces(self):
"""Yield (title, scalars, collections) for each namespace."""
s = extrapolate_statistics(logging.statistics)
@@ -574,12 +597,18 @@ table.stats2 th {
v = format % v
scalars.append((k, v))
yield title, scalars, collections
def get_dict_collection(self, v, formatting):
"""Return ([headers], [rows]) for the given collection."""
# E.g., the 'Requests' dict.
headers = []
for record in v.itervalues():
try:
# python2
vals = v.itervalues()
except AttributeError:
# python3
vals = v.values()
for record in vals:
for k3 in record:
format = formatting.get(k3, missing)
if format is None:
@@ -588,7 +617,7 @@ table.stats2 th {
if k3 not in headers:
headers.append(k3)
headers.sort()
subrows = []
for k2, record in sorted(v.items()):
subrow = [k2]
@@ -604,9 +633,9 @@ table.stats2 th {
v3 = format % v3
subrow.append(v3)
subrows.append(subrow)
return headers, subrows
def get_list_collection(self, v, formatting):
"""Return ([headers], [subrows]) for the given collection."""
# E.g., the 'Slow Queries' list.
@@ -620,7 +649,7 @@ table.stats2 th {
if k3 not in headers:
headers.append(k3)
headers.sort()
subrows = []
for record in v:
subrow = []
@@ -636,27 +665,26 @@ table.stats2 th {
v3 = format % v3
subrow.append(v3)
subrows.append(subrow)
return headers, subrows
if json is not None:
@cherrypy.expose
def data(self):
s = extrapolate_statistics(logging.statistics)
cherrypy.response.headers['Content-Type'] = 'application/json'
return json.dumps(s, sort_keys=True, indent=4)
data.exposed = True
@cherrypy.expose
def pause(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = False
raise cherrypy.HTTPRedirect('./')
pause.exposed = True
pause.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']}
@cherrypy.expose
def resume(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = True
raise cherrypy.HTTPRedirect('./')
resume.exposed = True
resume.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']}

View File

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

View File

@@ -1,27 +1,31 @@
import struct
import time
import io
import six
import cherrypy
from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr
from cherrypy._cpcompat import text_or_bytes, ntob
from cherrypy.lib import file_generator
from cherrypy.lib import is_closable_iterator
from cherrypy.lib import set_vary_header
def decode(encoding=None, default_encoding='utf-8'):
"""Replace or extend the list of charsets used to decode a request entity.
Either argument may be a single string or a list of strings.
encoding
If not None, restricts the set of charsets attempted while decoding
a request entity to the given set (even if a different charset is given in
the Content-Type request header).
a request entity to the given set (even if a different charset is
given in the Content-Type request header).
default_encoding
Only in effect if the 'encoding' argument is not given.
If given, the set of charsets attempted while decoding a request entity is
*extended* with the given value(s).
If given, the set of charsets attempted while decoding a request
entity is *extended* with the given value(s).
"""
body = cherrypy.request.body
if encoding is not None:
@@ -33,21 +37,46 @@ def decode(encoding=None, default_encoding='utf-8'):
default_encoding = [default_encoding]
body.attempt_charsets = body.attempt_charsets + default_encoding
class UTF8StreamEncoder:
def __init__(self, iterator):
self._iterator = iterator
def __iter__(self):
return self
def next(self):
return self.__next__()
def __next__(self):
res = next(self._iterator)
if isinstance(res, six.text_type):
res = res.encode('utf-8')
return res
def close(self):
if is_closable_iterator(self._iterator):
self._iterator.close()
def __getattr__(self, attr):
if attr.startswith('__'):
raise AttributeError(self, attr)
return getattr(self._iterator, attr)
class ResponseEncoder:
default_encoding = 'utf-8'
failmsg = "Response body could not be encoded with %r."
failmsg = 'Response body could not be encoded with %r.'
encoding = None
errors = 'strict'
text_only = True
add_charset = True
debug = False
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
self.attempted_charsets = set()
request = cherrypy.serving.request
if request.handler is not None:
@@ -56,54 +85,53 @@ class ResponseEncoder:
cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
self.oldhandler = request.handler
request.handler = self
def encode_stream(self, encoding):
"""Encode a streaming response body.
Use a generator wrapper, and just pray it works as the stream is
being written out.
"""
if encoding in self.attempted_charsets:
return False
self.attempted_charsets.add(encoding)
def encoder(body):
for chunk in body:
if isinstance(chunk, unicodestr):
if isinstance(chunk, six.text_type):
chunk = chunk.encode(encoding, self.errors)
yield chunk
self.body = encoder(self.body)
return True
def encode_string(self, encoding):
"""Encode a buffered response body."""
if encoding in self.attempted_charsets:
return False
self.attempted_charsets.add(encoding)
try:
body = []
for chunk in self.body:
if isinstance(chunk, unicodestr):
body = []
for chunk in self.body:
if isinstance(chunk, six.text_type):
try:
chunk = chunk.encode(encoding, self.errors)
body.append(chunk)
self.body = body
except (LookupError, UnicodeError):
return False
else:
return True
except (LookupError, UnicodeError):
return False
body.append(chunk)
self.body = body
return True
def find_acceptable_charset(self):
request = cherrypy.serving.request
response = cherrypy.serving.response
if self.debug:
cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE')
cherrypy.log('response.stream %r' %
response.stream, 'TOOLS.ENCODE')
if response.stream:
encoder = self.encode_stream
else:
encoder = self.encode_string
if "Content-Length" in response.headers:
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
# Encoded strings may be of different lengths from their
# unicode equivalents, and even from each other. For example:
@@ -114,23 +142,25 @@ class ResponseEncoder:
# 6
# >>> len(t.encode("utf7"))
# 8
del response.headers["Content-Length"]
del response.headers['Content-Length']
# Parse the Accept-Charset request header, and try to provide one
# of the requested charsets (in order of user preference).
encs = request.headers.elements('Accept-Charset')
charsets = [enc.value.lower() for enc in encs]
if self.debug:
cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
if self.encoding is not None:
# If specified, force this encoding to be used, or fail.
encoding = self.encoding.lower()
if self.debug:
cherrypy.log('Specified encoding %r' % encoding, 'TOOLS.ENCODE')
if (not charsets) or "*" in charsets or encoding in charsets:
cherrypy.log('Specified encoding %r' %
encoding, 'TOOLS.ENCODE')
if (not charsets) or '*' in charsets or encoding in charsets:
if self.debug:
cherrypy.log('Attempting encoding %r' % encoding, 'TOOLS.ENCODE')
cherrypy.log('Attempting encoding %r' %
encoding, 'TOOLS.ENCODE')
if encoder(encoding):
return encoding
else:
@@ -142,11 +172,12 @@ class ResponseEncoder:
if encoder(self.default_encoding):
return self.default_encoding
else:
raise cherrypy.HTTPError(500, self.failmsg % self.default_encoding)
raise cherrypy.HTTPError(500, self.failmsg %
self.default_encoding)
else:
for element in encs:
if element.qvalue > 0:
if element.value == "*":
if element.value == '*':
# Matches any charset. Try our default.
if self.debug:
cherrypy.log('Attempting default encoding due '
@@ -160,8 +191,8 @@ class ResponseEncoder:
'0)' % element, 'TOOLS.ENCODE')
if encoder(encoding):
return encoding
if "*" not in charsets:
if '*' not in charsets:
# If no "*" is present in an Accept-Charset field, then all
# character sets not explicitly mentioned get a quality
# value of 0, except for ISO-8859-1, which gets a quality
@@ -173,21 +204,22 @@ class ResponseEncoder:
'TOOLS.ENCODE')
if encoder(iso):
return iso
# No suitable encoding found.
ac = request.headers.get('Accept-Charset')
if ac is None:
msg = "Your client did not send an Accept-Charset header."
msg = 'Your client did not send an Accept-Charset header.'
else:
msg = "Your client sent this Accept-Charset header: %s." % ac
msg += " We tried these charsets: %s." % ", ".join(self.attempted_charsets)
msg = 'Your client sent this Accept-Charset header: %s.' % ac
_charsets = ', '.join(sorted(self.attempted_charsets))
msg += ' We tried these charsets: %s.' % (_charsets,)
raise cherrypy.HTTPError(406, msg)
def __call__(self, *args, **kwargs):
response = cherrypy.serving.response
self.body = self.oldhandler(*args, **kwargs)
if isinstance(self.body, basestring):
if isinstance(self.body, text_or_bytes):
# strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character
# in a long string.
@@ -200,56 +232,59 @@ class ResponseEncoder:
self.body = file_generator(self.body)
elif self.body is None:
self.body = []
ct = response.headers.elements("Content-Type")
ct = response.headers.elements('Content-Type')
if self.debug:
cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE')
if ct:
cherrypy.log('Content-Type: %r' % [str(h)
for h in ct], 'TOOLS.ENCODE')
if ct and self.add_charset:
ct = ct[0]
if self.text_only:
if ct.value.lower().startswith("text/"):
if ct.value.lower().startswith('text/'):
if self.debug:
cherrypy.log('Content-Type %s starts with "text/"' % ct,
'TOOLS.ENCODE')
cherrypy.log(
'Content-Type %s starts with "text/"' % ct,
'TOOLS.ENCODE')
do_find = True
else:
if self.debug:
cherrypy.log('Not finding because Content-Type %s does '
'not start with "text/"' % ct,
cherrypy.log('Not finding because Content-Type %s '
'does not start with "text/"' % ct,
'TOOLS.ENCODE')
do_find = False
else:
if self.debug:
cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE')
cherrypy.log('Finding because not text_only',
'TOOLS.ENCODE')
do_find = True
if do_find:
# Set "charset=..." param on response Content-Type header
ct.params['charset'] = self.find_acceptable_charset()
if self.add_charset:
if self.debug:
cherrypy.log('Setting Content-Type %s' % ct,
'TOOLS.ENCODE')
response.headers["Content-Type"] = str(ct)
if self.debug:
cherrypy.log('Setting Content-Type %s' % ct,
'TOOLS.ENCODE')
response.headers['Content-Type'] = str(ct)
return self.body
# GZIP
def compress(body, compress_level):
"""Compress 'body' at the given compress_level."""
import zlib
# See http://www.gzip.org/zlib/rfc-gzip.html
yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker
yield ntob('\x08') # CM: compression method
yield ntob('\x00') # FLG: none set
# MTIME: 4 bytes
yield struct.pack("<L", int(time.time()) & int('FFFFFFFF', 16))
yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16))
yield ntob('\x02') # XFL: max compression, slowest algo
yield ntob('\xff') # OS: unknown
crc = zlib.crc32(ntob(""))
crc = zlib.crc32(ntob(''))
size = 0
zobj = zlib.compressobj(compress_level,
zlib.DEFLATED, -zlib.MAX_WBITS,
@@ -259,16 +294,17 @@ def compress(body, compress_level):
crc = zlib.crc32(line, crc)
yield zobj.compress(line)
yield zobj.flush()
# CRC32: 4 bytes
yield struct.pack("<L", crc & int('FFFFFFFF', 16))
yield struct.pack('<L', crc & int('FFFFFFFF', 16))
# ISIZE: 4 bytes
yield struct.pack("<L", size & int('FFFFFFFF', 16))
yield struct.pack('<L', size & int('FFFFFFFF', 16))
def decompress(body):
import gzip
zbuf = BytesIO()
zbuf = io.BytesIO()
zbuf.write(body)
zbuf.seek(0)
zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
@@ -277,9 +313,10 @@ def decompress(body):
return data
def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
debug=False):
"""Try to gzip the response body if Content-Type in mime_types.
cherrypy.response.headers['Content-Type'] must be set to one of the
values in the mime_types arg before calling this function.
@@ -287,32 +324,32 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
* type/subtype
* type/*
* type/*+subtype
No compression is performed if any of the following hold:
* The client sends no Accept-Encoding request header
* No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
* No 'gzip' or 'x-gzip' with a qvalue > 0 is present
* The 'identity' value is given with a qvalue > 0.
"""
request = cherrypy.serving.request
response = cherrypy.serving.response
set_vary_header(response, "Accept-Encoding")
set_vary_header(response, 'Accept-Encoding')
if not response.body:
# Response body is empty (might be a 304 for instance)
if debug:
cherrypy.log('No response body', context='TOOLS.GZIP')
return
# If returning cached content (which should already have been gzipped),
# don't re-zip.
if getattr(request, "cached", False):
if getattr(request, 'cached', False):
if debug:
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
return
acceptable = request.headers.elements('Accept-Encoding')
if not acceptable:
# If no Accept-Encoding field is present in a request,
@@ -325,7 +362,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
if debug:
cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
return
ct = response.headers.get('Content-Type', '').split(';')[0]
for coding in acceptable:
if coding.value == 'identity' and coding.qvalue != 0:
@@ -339,7 +376,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
cherrypy.log('Zero gzip qvalue: %s' % coding,
context='TOOLS.GZIP')
return
if ct not in mime_types:
# If the list of provided mime-types contains tokens
# such as 'text/*' or 'application/*+xml',
@@ -370,19 +407,18 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
cherrypy.log('Content-Type %s not in mime_types %r' %
(ct, mime_types), context='TOOLS.GZIP')
return
if debug:
cherrypy.log('Gzipping', context='TOOLS.GZIP')
# Return a generator that compresses the page
response.headers['Content-Encoding'] = 'gzip'
response.body = compress(response.body, compress_level)
if "Content-Length" in response.headers:
if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
del response.headers['Content-Length']
return
if debug:
cherrypy.log('No acceptable encoding found.', context='GZIP')
cherrypy.HTTPError(406, "identity, gzip").set_response()
cherrypy.HTTPError(406, 'identity, gzip').set_response()

View File

@@ -1,9 +1,8 @@
import gc
import inspect
import os
import sys
import time
try:
import objgraph
except ImportError:
@@ -15,6 +14,7 @@ from cherrypy.process.plugins import SimplePlugin
class ReferrerTree(object):
"""An object which gathers all referrers of an object to a given depth."""
peek_length = 40
@@ -35,7 +35,7 @@ class ReferrerTree(object):
refs = gc.get_referrers(obj)
self.ignore.append(refs)
if len(refs) > self.maxparents:
return [("[%s referrers]" % len(refs), [])]
return [('[%s referrers]' % len(refs), [])]
try:
ascendcode = self.ascend.__code__
@@ -71,27 +71,28 @@ class ReferrerTree(object):
return self.peek(repr(obj))
if isinstance(obj, dict):
return "{" + ", ".join(["%s: %s" % (self._format(k, descend=False),
return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False),
self._format(v, descend=False))
for k, v in obj.items()]) + "}"
for k, v in obj.items()]) + '}'
elif isinstance(obj, list):
return "[" + ", ".join([self._format(item, descend=False)
for item in obj]) + "]"
return '[' + ', '.join([self._format(item, descend=False)
for item in obj]) + ']'
elif isinstance(obj, tuple):
return "(" + ", ".join([self._format(item, descend=False)
for item in obj]) + ")"
return '(' + ', '.join([self._format(item, descend=False)
for item in obj]) + ')'
r = self.peek(repr(obj))
if isinstance(obj, (str, int, float)):
return r
return "%s: %s" % (type(obj), r)
return '%s: %s' % (type(obj), r)
def format(self, tree):
"""Return a list of string reprs from a nested list of referrers."""
output = []
def ascend(branch, depth=1):
for parent, grandparents in branch:
output.append((" " * depth) + self._format(parent))
output.append((' ' * depth) + self._format(parent))
if grandparents:
ascend(grandparents, depth + 1)
ascend(tree)
@@ -103,56 +104,59 @@ def get_instances(cls):
class RequestCounter(SimplePlugin):
def start(self):
self.count = 0
def before_request(self):
self.count += 1
def after_request(self):
self.count -=1
self.count -= 1
request_counter = RequestCounter(cherrypy.engine)
request_counter.subscribe()
def get_context(obj):
if isinstance(obj, _cprequest.Request):
return "path=%s;stage=%s" % (obj.path_info, obj.stage)
return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
elif isinstance(obj, _cprequest.Response):
return "status=%s" % obj.status
return 'status=%s' % obj.status
elif isinstance(obj, _cpwsgi.AppResponse):
return "PATH_INFO=%s" % obj.environ.get('PATH_INFO', '')
elif hasattr(obj, "tb_lineno"):
return "tb_lineno=%s" % obj.tb_lineno
return ""
return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '')
elif hasattr(obj, 'tb_lineno'):
return 'tb_lineno=%s' % obj.tb_lineno
return ''
class GCRoot(object):
"""A CherryPy page handler for testing reference leaks."""
classes = [(_cprequest.Request, 2, 2,
"Should be 1 in this request thread and 1 in the main thread."),
(_cprequest.Response, 2, 2,
"Should be 1 in this request thread and 1 in the main thread."),
(_cpwsgi.AppResponse, 1, 1,
"Should be 1 in this request thread only."),
]
classes = [
(_cprequest.Request, 2, 2,
'Should be 1 in this request thread and 1 in the main thread.'),
(_cprequest.Response, 2, 2,
'Should be 1 in this request thread and 1 in the main thread.'),
(_cpwsgi.AppResponse, 1, 1,
'Should be 1 in this request thread only.'),
]
@cherrypy.expose
def index(self):
return "Hello, world!"
index.exposed = True
return 'Hello, world!'
@cherrypy.expose
def stats(self):
output = ["Statistics:"]
output = ['Statistics:']
for trial in range(10):
if request_counter.count > 0:
break
time.sleep(0.5)
else:
output.append("\nNot all requests closed properly.")
output.append('\nNot all requests closed properly.')
# gc_collect isn't perfectly synchronous, because it may
# break reference cycles that then take time to fully
# finalize. Call it thrice and hope for the best.
@@ -169,11 +173,11 @@ class GCRoot(object):
for x in gc.garbage:
trash[type(x)] = trash.get(type(x), 0) + 1
if trash:
output.insert(0, "\n%s unreachable objects:" % unreachable)
output.insert(0, '\n%s unreachable objects:' % unreachable)
trash = [(v, k) for k, v in trash.items()]
trash.sort()
for pair in trash:
output.append(" " + repr(pair))
output.append(' ' + repr(pair))
# Check declared classes to verify uncollected instances.
# These don't have to be part of a cycle; they can be
@@ -189,26 +193,24 @@ class GCRoot(object):
if lenobj < minobj or lenobj > maxobj:
if minobj == maxobj:
output.append(
"\nExpected %s %r references, got %s." %
'\nExpected %s %r references, got %s.' %
(minobj, cls, lenobj))
else:
output.append(
"\nExpected %s to %s %r references, got %s." %
'\nExpected %s to %s %r references, got %s.' %
(minobj, maxobj, cls, lenobj))
for obj in objs:
if objgraph is not None:
ig = [id(objs), id(inspect.currentframe())]
fname = "graph_%s_%s.png" % (cls.__name__, id(obj))
fname = 'graph_%s_%s.png' % (cls.__name__, id(obj))
objgraph.show_backrefs(
obj, extra_ignore=ig, max_depth=4, too_many=20,
filename=fname, extra_info=get_context)
output.append("\nReferrers for %s (refcount=%s):" %
output.append('\nReferrers for %s (refcount=%s):' %
(repr(obj), sys.getrefcount(obj)))
t = ReferrerTree(ignore=[objs], maxdepth=3)
tree = t.ascend(obj)
output.extend(t.format(tree))
return "\n".join(output)
stats.exposed = True
return '\n'.join(output)

View File

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

View File

@@ -1,5 +1,6 @@
"""
This module defines functions to implement HTTP Digest Authentication (:rfc:`2617`).
This module defines functions to implement HTTP Digest Authentication
(:rfc:`2617`).
This has full compliance with 'Digest' and 'Basic' authentication methods. In
'Digest' it supports both MD5 and MD5-sess algorithms.
@@ -7,21 +8,33 @@ Usage:
First use 'doAuth' to request the client authentication for a
certain resource. You should send an httplib.UNAUTHORIZED response to the
client so he knows he has to authenticate itself.
Then use 'parseAuthorization' to retrieve the 'auth_map' used in
'checkResponse'.
To use 'checkResponse' you must have already verified the password associated
with the 'username' key in 'auth_map' dict. Then you use the 'checkResponse'
function to verify if the password matches the one sent by the client.
To use 'checkResponse' you must have already verified the password
associated with the 'username' key in 'auth_map' dict. Then you use the
'checkResponse' function to verify if the password matches the one sent
by the client.
SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
SUPPORTED_QOP - list of supported 'Digest' 'qop'.
"""
import time
from hashlib import md5
from cherrypy._cpcompat import (
base64_decode, ntob,
parse_http_list, parse_keqv_list
)
__version__ = 1, 0, 1
__author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>"
__author__ = 'Tiago Cogumbreiro <cogumbreiro@users.sf.net>'
__credits__ = """
Peter van Kampen for its recipe which implement most of Digest authentication:
Peter van Kampen for its recipe which implement most of Digest
authentication:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
"""
@@ -29,57 +42,55 @@ __license__ = """
Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Sylvain Hellegouarch nor the names of his contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
* Neither the name of Sylvain Hellegouarch nor the names of his
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
"parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey",
"calculateNonce", "SUPPORTED_QOP")
__all__ = ('digestAuth', 'basicAuth', 'doAuth', 'checkResponse',
'parseAuthorization', 'SUPPORTED_ALGORITHM', 'md5SessionKey',
'calculateNonce', 'SUPPORTED_QOP')
################################################################################
import time
from cherrypy._cpcompat import base64_decode, ntob, md5
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
##########################################################################
MD5 = "MD5"
MD5_SESS = "MD5-sess"
AUTH = "auth"
AUTH_INT = "auth-int"
MD5 = 'MD5'
MD5_SESS = 'MD5-sess'
AUTH = 'auth'
AUTH_INT = 'auth-int'
SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
SUPPORTED_QOP = (AUTH, AUTH_INT)
################################################################################
##########################################################################
# doAuth
#
DIGEST_AUTH_ENCODERS = {
MD5: lambda val: md5(ntob(val)).hexdigest(),
MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
# SHA: lambda val: sha.new(ntob(val)).hexdigest (),
# SHA: lambda val: sha.new(ntob(val)).hexdigest (),
}
def calculateNonce (realm, algorithm = MD5):
def calculateNonce(realm, algorithm=MD5):
"""This is an auxaliary function that calculates 'nonce' value. It is used
to handle sessions."""
@@ -89,44 +100,47 @@ def calculateNonce (realm, algorithm = MD5):
try:
encoder = DIGEST_AUTH_ENCODERS[algorithm]
except KeyError:
raise NotImplementedError ("The chosen algorithm (%s) does not have "\
"an implementation yet" % algorithm)
raise NotImplementedError('The chosen algorithm (%s) does not have '
'an implementation yet' % algorithm)
return encoder ("%d:%s" % (time.time(), realm))
return encoder('%d:%s' % (time.time(), realm))
def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH):
def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH):
"""Challenges the client for a Digest authentication."""
global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP
assert algorithm in SUPPORTED_ALGORITHM
assert qop in SUPPORTED_QOP
if nonce is None:
nonce = calculateNonce (realm, algorithm)
nonce = calculateNonce(realm, algorithm)
return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
realm, nonce, algorithm, qop
)
def basicAuth (realm):
def basicAuth(realm):
"""Challengenes the client for a Basic authentication."""
assert '"' not in realm, "Realms cannot contain the \" (quote) character."
return 'Basic realm="%s"' % realm
def doAuth (realm):
def doAuth(realm):
"""'doAuth' function returns the challenge string b giving priority over
Digest and fallback to Basic authentication when the browser doesn't
support the first one.
This should be set in the HTTP header under the key 'WWW-Authenticate'."""
return digestAuth (realm) + " " + basicAuth (realm)
return digestAuth(realm) + ' ' + basicAuth(realm)
################################################################################
##########################################################################
# Parse authorization parameters
#
def _parseDigestAuthorization (auth_params):
def _parseDigestAuthorization(auth_params):
# Convert the auth params to a dict
items = parse_http_list(auth_params)
params = parse_keqv_list(items)
@@ -134,60 +148,61 @@ def _parseDigestAuthorization (auth_params):
# Now validate the params
# Check for required parameters
required = ["username", "realm", "nonce", "uri", "response"]
required = ['username', 'realm', 'nonce', 'uri', 'response']
for k in required:
if k not in params:
return None
# If qop is sent then cnonce and nc MUST be present
if "qop" in params and not ("cnonce" in params \
and "nc" in params):
if 'qop' in params and not ('cnonce' in params
and 'nc' in params):
return None
# If qop is not sent, neither cnonce nor nc can be present
if ("cnonce" in params or "nc" in params) and \
"qop" not in params:
if ('cnonce' in params or 'nc' in params) and \
'qop' not in params:
return None
return params
def _parseBasicAuthorization (auth_params):
username, password = base64_decode(auth_params).split(":", 1)
return {"username": username, "password": password}
def _parseBasicAuthorization(auth_params):
username, password = base64_decode(auth_params).split(':', 1)
return {'username': username, 'password': password}
AUTH_SCHEMES = {
"basic": _parseBasicAuthorization,
"digest": _parseDigestAuthorization,
'basic': _parseBasicAuthorization,
'digest': _parseDigestAuthorization,
}
def parseAuthorization (credentials):
def parseAuthorization(credentials):
"""parseAuthorization will convert the value of the 'Authorization' key in
the HTTP header to a map itself. If the parsing fails 'None' is returned.
"""
global AUTH_SCHEMES
auth_scheme, auth_params = credentials.split(" ", 1)
auth_scheme = auth_scheme.lower ()
auth_scheme, auth_params = credentials.split(' ', 1)
auth_scheme = auth_scheme.lower()
parser = AUTH_SCHEMES[auth_scheme]
params = parser (auth_params)
params = parser(auth_params)
if params is None:
return
assert "auth_scheme" not in params
params["auth_scheme"] = auth_scheme
assert 'auth_scheme' not in params
params['auth_scheme'] = auth_scheme
return params
################################################################################
##########################################################################
# Check provided response for a valid password
#
def md5SessionKey (params, password):
def md5SessionKey(params, password):
"""
If the "algorithm" directive's value is "MD5-sess", then A1
If the "algorithm" directive's value is "MD5-sess", then A1
[the session key] is calculated only once - on the first request by the
client following receipt of a WWW-Authenticate challenge from the server.
@@ -204,67 +219,70 @@ def md5SessionKey (params, password):
specification.
"""
keys = ("username", "realm", "nonce", "cnonce")
keys = ('username', 'realm', 'nonce', 'cnonce')
params_copy = {}
for key in keys:
params_copy[key] = params[key]
params_copy["algorithm"] = MD5_SESS
return _A1 (params_copy, password)
params_copy['algorithm'] = MD5_SESS
return _A1(params_copy, password)
def _A1(params, password):
algorithm = params.get ("algorithm", MD5)
algorithm = params.get('algorithm', MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
if algorithm == MD5:
# If the "algorithm" directive's value is "MD5" or is
# unspecified, then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
return "%s:%s:%s" % (params["username"], params["realm"], password)
return '%s:%s:%s' % (params['username'], params['realm'], password)
elif algorithm == MD5_SESS:
# This is A1 if qop is set
# A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
# ":" unq(nonce-value) ":" unq(cnonce-value)
h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password))
return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
h_a1 = H('%s:%s:%s' % (params['username'], params['realm'], password))
return '%s:%s:%s' % (h_a1, params['nonce'], params['cnonce'])
def _A2(params, method, kwargs):
# If the "qop" directive's value is "auth" or is unspecified, then A2 is:
# A2 = Method ":" digest-uri-value
qop = params.get ("qop", "auth")
if qop == "auth":
return method + ":" + params["uri"]
elif qop == "auth-int":
qop = params.get('qop', 'auth')
if qop == 'auth':
return method + ':' + params['uri']
elif qop == 'auth-int':
# If the "qop" value is "auth-int", then A2 is:
# A2 = Method ":" digest-uri-value ":" H(entity-body)
entity_body = kwargs.get ("entity_body", "")
H = kwargs["H"]
entity_body = kwargs.get('entity_body', '')
H = kwargs['H']
return "%s:%s:%s" % (
return '%s:%s:%s' % (
method,
params["uri"],
params['uri'],
H(entity_body)
)
else:
raise NotImplementedError ("The 'qop' method is unknown: %s" % qop)
raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs):
def _computeDigestResponse(auth_map, password, method='GET', A1=None,
**kwargs):
"""
Generates a response respecting the algorithm defined in RFC 2617
"""
params = auth_map
algorithm = params.get ("algorithm", MD5)
algorithm = params.get('algorithm', MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
KD = lambda secret, data: H(secret + ":" + data)
KD = lambda secret, data: H(secret + ':' + data)
qop = params.get ("qop", None)
qop = params.get('qop', None)
H_A2 = H(_A2(params, method, kwargs))
@@ -273,7 +291,7 @@ def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwarg
else:
H_A1 = H(_A1(params, password))
if qop in ("auth", "auth-int"):
if qop in ('auth', 'auth-int'):
# If the "qop" value is "auth" or "auth-int":
# request-digest = <"> < KD ( H(A1), unq(nonce-value)
# ":" nc-value
@@ -281,11 +299,11 @@ def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwarg
# ":" unq(qop-value)
# ":" H(A2)
# ) <">
request = "%s:%s:%s:%s:%s" % (
params["nonce"],
params["nc"],
params["cnonce"],
params["qop"],
request = '%s:%s:%s:%s:%s' % (
params['nonce'],
params['nc'],
params['cnonce'],
params['qop'],
H_A2,
)
elif qop is None:
@@ -293,11 +311,12 @@ def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwarg
# for compatibility with RFC 2069):
# request-digest =
# <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
request = "%s:%s" % (params["nonce"], H_A2)
request = '%s:%s' % (params['nonce'], H_A2)
return KD(H_A1, request)
def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs):
def _checkDigestResponse(auth_map, password, method='GET', A1=None, **kwargs):
"""This function is used to verify the response given by the client when
he tries to authenticate.
Optional arguments:
@@ -312,43 +331,48 @@ def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs
if auth_map['realm'] != kwargs.get('realm', None):
return False
response = _computeDigestResponse(auth_map, password, method, A1,**kwargs)
response = _computeDigestResponse(
auth_map, password, method, A1, **kwargs)
return response == auth_map["response"]
return response == auth_map['response']
def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs):
def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
**kwargs):
# Note that the Basic response doesn't provide the realm value so we cannot
# test it
pass_through = lambda password, username=None: password
encrypt = encrypt or pass_through
try:
return encrypt(auth_map["password"], auth_map["username"]) == password
candidate = encrypt(auth_map['password'], auth_map['username'])
except TypeError:
return encrypt(auth_map["password"]) == password
# if encrypt only takes one parameter, it's the password
candidate = encrypt(auth_map['password'])
return candidate == password
AUTH_RESPONSES = {
"basic": _checkBasicResponse,
"digest": _checkDigestResponse,
'basic': _checkBasicResponse,
'digest': _checkDigestResponse,
}
def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs):
def checkResponse(auth_map, password, method='GET', encrypt=None, **kwargs):
"""'checkResponse' compares the auth_map with the password and optionally
other arguments that each implementation might need.
If the response is of type 'Basic' then the function has the following
signature::
checkBasicResponse (auth_map, password) -> bool
checkBasicResponse(auth_map, password) -> bool
If the response is of type 'Digest' then the function has the following
signature::
checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool
checkDigestResponse(auth_map, password, method='GET', A1=None) -> bool
The 'A1' argument is only used in MD5_SESS algorithm based responses.
Check md5SessionKey() for more info.
"""
checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
checker = AUTH_RESPONSES[auth_map['auth_scheme']]
return checker(auth_map, password, method=method, encrypt=encrypt,
**kwargs)

View File

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

View File

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

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

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

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

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

View File

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

View File

@@ -25,14 +25,9 @@ except ImportError:
from ConfigParser import ConfigParser
try:
set
text_or_bytes
except NameError:
from sets import Set as set
try:
basestring
except NameError:
basestring = str
text_or_bytes = str
try:
# Python 3
@@ -44,9 +39,10 @@ except ImportError:
import operator as _operator
import sys
def as_dict(config):
"""Return a dict from 'config' whether it is a dict, file, or filename."""
if isinstance(config, basestring):
if isinstance(config, text_or_bytes):
config = Parser().dict_from_file(config)
elif hasattr(config, 'read'):
config = Parser().dict_from_file(config)
@@ -54,26 +50,27 @@ def as_dict(config):
class NamespaceSet(dict):
"""A dict of config namespace names and handlers.
Each config entry should begin with a namespace name; the corresponding
namespace handler will be called once for each config entry in that
namespace, and will be passed two arguments: the config key (with the
namespace removed) and the config value.
Namespace handlers may be any Python callable; they may also be
Python 2.5-style 'context managers', in which case their __enter__
method should return a callable to be used as the handler.
See cherrypy.tools (the Toolbox class) for an example.
"""
def __call__(self, config):
"""Iterate through config and pass it to each namespace handler.
config
A flat dict, where keys use dots to separate
namespaces, and values are arbitrary.
The first name in each config key is used to look up the corresponding
namespace handler. For example, a config entry of {'tools.gzip.on': v}
will call the 'tools' namespace handler with the args: ('gzip.on', v)
@@ -81,11 +78,11 @@ class NamespaceSet(dict):
# Separate the given config into namespaces
ns_confs = {}
for k in config:
if "." in k:
ns, name = k.split(".", 1)
if '.' in k:
ns, name = k.split('.', 1)
bucket = ns_confs.setdefault(ns, {})
bucket[name] = config[k]
# I chose __enter__ and __exit__ so someday this could be
# rewritten using Python 2.5's 'with' statement:
# for ns, handler in self.iteritems():
@@ -93,7 +90,7 @@ class NamespaceSet(dict):
# for k, v in ns_confs.get(ns, {}).iteritems():
# callable(k, v)
for ns, handler in self.items():
exit = getattr(handler, "__exit__", None)
exit = getattr(handler, '__exit__', None)
if exit:
callable = handler.__enter__()
no_exc = True
@@ -116,11 +113,11 @@ class NamespaceSet(dict):
else:
for k, v in ns_confs.get(ns, {}).items():
handler(k, v)
def __repr__(self):
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
dict.__repr__(self))
def __copy__(self):
newobj = self.__class__()
newobj.update(self)
@@ -129,30 +126,31 @@ class NamespaceSet(dict):
class Config(dict):
"""A dict-like set of configuration data, with defaults and namespaces.
May take a file, filename, or dict.
"""
defaults = {}
environments = {}
namespaces = NamespaceSet()
def __init__(self, file=None, **kwargs):
self.reset()
if file is not None:
self.update(file)
if kwargs:
self.update(kwargs)
def reset(self):
"""Reset self to default values."""
self.clear()
dict.update(self, self.defaults)
def update(self, config):
"""Update self from a dict, file or filename."""
if isinstance(config, basestring):
if isinstance(config, text_or_bytes):
# Filename
config = Parser().dict_from_file(config)
elif hasattr(config, 'read'):
@@ -161,7 +159,7 @@ class Config(dict):
else:
config = config.copy()
self._apply(config)
def _apply(self, config):
"""Update self from a dict."""
which_env = config.get('environment')
@@ -170,25 +168,26 @@ class Config(dict):
for k in env:
if k not in config:
config[k] = env[k]
dict.update(self, config)
self.namespaces(config)
def __setitem__(self, k, v):
dict.__setitem__(self, k, v)
self.namespaces({k: v})
class Parser(ConfigParser):
"""Sub-class of ConfigParser that keeps the case of options and that
"""Sub-class of ConfigParser that keeps the case of options and that
raises an exception if the file cannot be read.
"""
def optionxform(self, optionstr):
return optionstr
def read(self, filenames):
if isinstance(filenames, basestring):
if isinstance(filenames, text_or_bytes):
filenames = [filenames]
for filename in filenames:
# try:
@@ -200,7 +199,7 @@ class Parser(ConfigParser):
self._read(fp, filename)
finally:
fp.close()
def as_dict(self, raw=False, vars=None):
"""Convert an INI file to a dictionary"""
# Load INI file into a dict
@@ -214,13 +213,13 @@ class Parser(ConfigParser):
value = unrepr(value)
except Exception:
x = sys.exc_info()[1]
msg = ("Config error in section: %r, option: %r, "
"value: %r. Config values must be valid Python." %
msg = ('Config error in section: %r, option: %r, '
'value: %r. Config values must be valid Python.' %
(section, option, value))
raise ValueError(msg, x.__class__.__name__, x.args)
result[section][option] = value
return result
def dict_from_file(self, file):
if hasattr(file, 'read'):
self.readfp(file)
@@ -233,14 +232,14 @@ class Parser(ConfigParser):
class _Builder2:
def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None:
raise TypeError("unrepr does not recognize %s" %
raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__))
return m(o)
def astnode(self, s):
"""Return a Python2 ast Node compiled from a string."""
try:
@@ -249,40 +248,59 @@ class _Builder2:
# Fallback to eval when compiler package is not available,
# e.g. IronPython 1.0.
return eval(s)
p = compiler.parse("__tempvalue__ = " + s)
p = compiler.parse('__tempvalue__ = ' + s)
return p.getChildren()[1].getChildren()[0].getChildren()[1]
def build_Subscript(self, o):
expr, flags, subs = o.getChildren()
expr = self.build(expr)
subs = self.build(subs)
return expr[subs]
def build_CallFunc(self, o):
children = map(self.build, o.getChildren())
callee = children.pop(0)
kwargs = children.pop() or {}
starargs = children.pop() or ()
args = tuple(children) + tuple(starargs)
children = o.getChildren()
# Build callee from first child
callee = self.build(children[0])
# Build args and kwargs from remaining children
args = []
kwargs = {}
for child in children[1:]:
class_name = child.__class__.__name__
# None is ignored
if class_name == 'NoneType':
continue
# Keywords become kwargs
if class_name == 'Keyword':
kwargs.update(self.build(child))
# Everything else becomes args
else :
args.append(self.build(child))
return callee(*args, **kwargs)
def build_Keyword(self, o):
key, value_obj = o.getChildren()
value = self.build(value_obj)
kw_dict = {key: value}
return kw_dict
def build_List(self, o):
return map(self.build, o.getChildren())
def build_Const(self, o):
return o.value
def build_Dict(self, o):
d = {}
i = iter(map(self.build, o.getChildren()))
for el in i:
d[el] = i.next()
return d
def build_Tuple(self, o):
return tuple(self.build_List(o))
def build_Name(self, o):
name = o.name
if name == 'None':
@@ -291,21 +309,21 @@ class _Builder2:
return True
if name == 'False':
return False
# See if the Name is a package or module. If it is, import it.
try:
return modules(name)
except ImportError:
pass
# See if the Name is in builtins.
try:
return getattr(builtins, name)
except AttributeError:
pass
raise TypeError("unrepr could not resolve the name %s" % repr(name))
raise TypeError('unrepr could not resolve the name %s' % repr(name))
def build_Add(self, o):
left, right = map(self.build, o.getChildren())
return left + right
@@ -313,30 +331,30 @@ class _Builder2:
def build_Mul(self, o):
left, right = map(self.build, o.getChildren())
return left * right
def build_Getattr(self, o):
parent = self.build(o.expr)
return getattr(parent, o.attrname)
def build_NoneType(self, o):
return None
def build_UnarySub(self, o):
return -self.build(o.getChildren()[0])
def build_UnaryAdd(self, o):
return self.build(o.getChildren()[0])
class _Builder3:
def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None:
raise TypeError("unrepr does not recognize %s" %
raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__))
return m(o)
def astnode(self, s):
"""Return a Python3 ast Node compiled from a string."""
try:
@@ -346,51 +364,86 @@ class _Builder3:
# e.g. IronPython 1.0.
return eval(s)
p = ast.parse("__tempvalue__ = " + s)
p = ast.parse('__tempvalue__ = ' + s)
return p.body[0].value
def build_Subscript(self, o):
return self.build(o.value)[self.build(o.slice)]
def build_Index(self, o):
return self.build(o.value)
def build_Call(self, o):
def _build_call35(self, o):
"""
Workaround for python 3.5 _ast.Call signature, docs found here
https://greentreesnakes.readthedocs.org/en/latest/nodes.html
"""
import ast
callee = self.build(o.func)
args = []
if o.args is not None:
for a in o.args:
if isinstance(a, ast.Starred):
args.append(self.build(a.value))
else:
args.append(self.build(a))
kwargs = {}
for kw in o.keywords:
if kw.arg is None: # double asterix `**`
rst = self.build(kw.value)
if not isinstance(rst, dict):
raise TypeError('Invalid argument for call.'
'Must be a mapping object.')
# give preference to the keys set directly from arg=value
for k, v in rst.items():
if k not in kwargs:
kwargs[k] = v
else: # defined on the call as: arg=value
kwargs[kw.arg] = self.build(kw.value)
return callee(*args, **kwargs)
def build_Call(self, o):
if sys.version_info >= (3, 5):
return self._build_call35(o)
callee = self.build(o.func)
if o.args is None:
args = ()
else:
args = tuple([self.build(a) for a in o.args])
else:
args = tuple([self.build(a) for a in o.args])
if o.starargs is None:
starargs = ()
else:
starargs = self.build(o.starargs)
starargs = tuple(self.build(o.starargs))
if o.kwargs is None:
kwargs = {}
else:
kwargs = self.build(o.kwargs)
if o.keywords is not None: # direct a=b keywords
for kw in o.keywords:
# preference because is a direct keyword against **kwargs
kwargs[kw.arg] = self.build(kw.value)
return callee(*(args + starargs), **kwargs)
def build_List(self, o):
return list(map(self.build, o.elts))
def build_Str(self, o):
return o.s
def build_Num(self, o):
return o.n
def build_Dict(self, o):
return dict([(self.build(k), self.build(v))
for k, v in zip(o.keys, o.values)])
def build_Tuple(self, o):
return tuple(self.build_List(o))
def build_Name(self, o):
name = o.id
if name == 'None':
@@ -399,28 +452,31 @@ class _Builder3:
return True
if name == 'False':
return False
# See if the Name is a package or module. If it is, import it.
try:
return modules(name)
except ImportError:
pass
# See if the Name is in builtins.
try:
import builtins
return getattr(builtins, name)
except AttributeError:
pass
raise TypeError("unrepr could not resolve the name %s" % repr(name))
raise TypeError('unrepr could not resolve the name %s' % repr(name))
def build_NameConstant(self, o):
return o.value
def build_UnaryOp(self, o):
op, operand = map(self.build, [o.op, o.operand])
return op(operand)
def build_BinOp(self, o):
left, op, right = map(self.build, [o.left, o.op, o.right])
left, op, right = map(self.build, [o.left, o.op, o.right])
return op(left, right)
def build_Add(self, o):
@@ -428,7 +484,7 @@ class _Builder3:
def build_Mult(self, o):
return _operator.mul
def build_USub(self, o):
return _operator.neg
@@ -454,23 +510,18 @@ def unrepr(s):
def modules(modulePath):
"""Load a module and retrieve a reference to that module."""
try:
mod = sys.modules[modulePath]
if mod is None:
raise KeyError()
except KeyError:
# The last [''] is important.
mod = __import__(modulePath, globals(), locals(), [''])
return mod
__import__(modulePath)
return sys.modules[modulePath]
def attributes(full_attribute_name):
"""Load a module and retrieve an attribute of that module."""
# Parse out the path, module, and attribute
last_dot = full_attribute_name.rfind(".")
last_dot = full_attribute_name.rfind('.')
attr_name = full_attribute_name[last_dot + 1:]
mod_path = full_attribute_name[:last_dot]
mod = modules(mod_path)
# Let an AttributeError propagate outward.
try:
@@ -478,8 +529,6 @@ def attributes(full_attribute_name):
except AttributeError:
raise AttributeError("'%s' object has no attribute '%s'"
% (mod_path, attr_name))
# Return a reference to the attribute.
return attr

View File

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -3,6 +3,7 @@ import sys
import cherrypy
from cherrypy._cpcompat import ntob
def get_xmlrpclib():
try:
import xmlrpc.client as x
@@ -10,6 +11,7 @@ def get_xmlrpclib():
import xmlrpclib as x
return x
def process_body():
"""Return (params, method) from request body."""
try:
@@ -48,8 +50,8 @@ def respond(body, encoding='utf-8', allow_none=0):
encoding=encoding,
allow_none=allow_none))
def on_error(*args, **kwargs):
body = str(sys.exc_info()[1])
xmlrpclib = get_xmlrpclib()
_set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))

View File

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

View File

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

View File

@@ -13,7 +13,9 @@ protocols, etc.), you can manually register each one and then start them all
with engine.start::
s1 = ServerAdapter(cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80))
s2 = ServerAdapter(cherrypy.engine, another.HTTPServer(host='127.0.0.1', SSL=True))
s2 = ServerAdapter(cherrypy.engine,
another.HTTPServer(host='127.0.0.1',
SSL=True))
s1.subscribe()
s2.subscribe()
cherrypy.engine.start()
@@ -54,16 +56,16 @@ hello.py::
#!/usr/bin/python
import cherrypy
class HelloWorld:
\"""Sample request handler class.\"""
@cherrypy.expose
def index(self):
return "Hello world!"
index.exposed = True
cherrypy.tree.mount(HelloWorld())
# CherryPy autoreload must be disabled for the flup server to work
cherrypy.config.update({'engine.autoreload_on':False})
cherrypy.config.update({'engine.autoreload.on':False})
Then run :doc:`/deployguide/cherryd` with the '-f' arg::
@@ -107,75 +109,93 @@ directive, configure your fastcgi script like the following::
} # end of $HTTP["url"] =~ "^/"
Please see `Lighttpd FastCGI Docs
<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for an explanation
of the possible configuration options.
<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for
an explanation of the possible configuration options.
"""
import os
import sys
import time
import warnings
class ServerAdapter(object):
"""Adapter for an HTTP server.
If you need to start more than one HTTP server (to serve on multiple
ports, or protocols, etc.), you can manually register each one and then
start them all with bus.start:
start them all with bus.start::
s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
s1.subscribe()
s2.subscribe()
bus.start()
"""
def __init__(self, bus, httpserver=None, bind_addr=None):
self.bus = bus
self.httpserver = httpserver
self.bind_addr = bind_addr
self.interrupt = None
self.running = False
def subscribe(self):
self.bus.subscribe('start', self.start)
self.bus.subscribe('stop', self.stop)
def unsubscribe(self):
self.bus.unsubscribe('start', self.start)
self.bus.unsubscribe('stop', self.stop)
def start(self):
"""Start the HTTP server."""
if self.bind_addr is None:
on_what = "unknown interface (dynamic?)"
on_what = 'unknown interface (dynamic?)'
elif isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
on_what = "%s:%s" % (host, port)
on_what = self._get_base()
else:
on_what = "socket file: %s" % self.bind_addr
on_what = 'socket file: %s' % self.bind_addr
if self.running:
self.bus.log("Already serving on %s" % on_what)
self.bus.log('Already serving on %s' % on_what)
return
self.interrupt = None
if not self.httpserver:
raise ValueError("No HTTP server has been created.")
# Start the httpserver in a new thread.
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
raise ValueError('No HTTP server has been created.')
if not os.environ.get('LISTEN_PID', None):
# Start the httpserver in a new thread.
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
import threading
t = threading.Thread(target=self._start_http_thread)
t.setName("HTTPServer " + t.getName())
t.setName('HTTPServer ' + t.getName())
t.start()
self.wait()
self.running = True
self.bus.log("Serving on %s" % on_what)
self.bus.log('Serving on %s' % on_what)
start.priority = 75
def _get_base(self):
if not self.httpserver:
return ''
host, port = self.bind_addr
if getattr(self.httpserver, 'ssl_adapter', None):
scheme = 'https'
if port != 443:
host += ':%s' % port
else:
scheme = 'http'
if port != 80:
host += ':%s' % port
return '%s://%s' % (scheme, host)
def _start_http_thread(self):
"""HTTP servers MUST be running in new threads, so that the
main thread persists to receive KeyboardInterrupt's. If an
@@ -186,33 +206,36 @@ class ServerAdapter(object):
try:
self.httpserver.start()
except KeyboardInterrupt:
self.bus.log("<Ctrl-C> hit: shutting down HTTP server")
self.bus.log('<Ctrl-C> hit: shutting down HTTP server')
self.interrupt = sys.exc_info()[1]
self.bus.exit()
except SystemExit:
self.bus.log("SystemExit raised: shutting down HTTP server")
self.bus.log('SystemExit raised: shutting down HTTP server')
self.interrupt = sys.exc_info()[1]
self.bus.exit()
raise
except:
self.interrupt = sys.exc_info()[1]
self.bus.log("Error in HTTP server: shutting down",
self.bus.log('Error in HTTP server: shutting down',
traceback=True, level=40)
self.bus.exit()
raise
def wait(self):
"""Wait until the HTTP server is ready to receive requests."""
while not getattr(self.httpserver, "ready", False):
while not getattr(self.httpserver, 'ready', False):
if self.interrupt:
raise self.interrupt
time.sleep(.1)
# Wait for port to be occupied
if isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
wait_for_occupied_port(host, port)
if not os.environ.get('LISTEN_PID', None):
# Wait for port to be occupied if not running via socket-activation
# (for socket-activation the port will be managed by systemd )
if isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
wait_for_occupied_port(host, port)
def stop(self):
"""Stop the HTTP server."""
if self.running:
@@ -222,11 +245,11 @@ class ServerAdapter(object):
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
self.running = False
self.bus.log("HTTP Server %s shut down" % self.httpserver)
self.bus.log('HTTP Server %s shut down' % self.httpserver)
else:
self.bus.log("HTTP Server %s already shut down" % self.httpserver)
self.bus.log('HTTP Server %s already shut down' % self.httpserver)
stop.priority = 25
def restart(self):
"""Restart the HTTP server."""
self.stop()
@@ -234,31 +257,33 @@ class ServerAdapter(object):
class FlupCGIServer(object):
"""Adapter for a flup.server.cgi.WSGIServer."""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.ready = False
def start(self):
"""Start the CGI server."""
# We have to instantiate the server class here because its __init__
# starts a threadpool. If we do it too early, daemonize won't work.
from flup.server.cgi import WSGIServer
self.cgiserver = WSGIServer(*self.args, **self.kwargs)
self.ready = True
self.cgiserver.run()
def stop(self):
"""Stop the HTTP server."""
self.ready = False
class FlupFCGIServer(object):
"""Adapter for a flup.server.fcgi.WSGIServer."""
def __init__(self, *args, **kwargs):
if kwargs.get('bindAddress', None) is None:
import socket
@@ -270,7 +295,7 @@ class FlupFCGIServer(object):
self.args = args
self.kwargs = kwargs
self.ready = False
def start(self):
"""Start the FCGI server."""
# We have to instantiate the server class here because its __init__
@@ -290,24 +315,26 @@ class FlupFCGIServer(object):
self.fcgiserver._oldSIGs = []
self.ready = True
self.fcgiserver.run()
def stop(self):
"""Stop the HTTP server."""
# Forcibly stop the fcgi server main event loop.
self.fcgiserver._keepGoing = False
# Force all worker threads to die off.
self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount
self.fcgiserver._threadPool.maxSpare = (
self.fcgiserver._threadPool._idleCount)
self.ready = False
class FlupSCGIServer(object):
"""Adapter for a flup.server.scgi.WSGIServer."""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.ready = False
def start(self):
"""Start the SCGI server."""
# We have to instantiate the server class here because its __init__
@@ -327,7 +354,7 @@ class FlupSCGIServer(object):
self.scgiserver._oldSIGs = []
self.ready = True
self.scgiserver.run()
def stop(self):
"""Stop the HTTP server."""
self.ready = False
@@ -344,19 +371,21 @@ def client_host(server_host):
return '127.0.0.1'
if server_host in ('::', '::0', '::0.0.0.0'):
# :: is IN6ADDR_ANY, which should answer on localhost.
# ::0 and ::0.0.0.0 are non-canonical but common ways to write IN6ADDR_ANY.
# ::0 and ::0.0.0.0 are non-canonical but common
# ways to write IN6ADDR_ANY.
return '::1'
return server_host
def check_port(host, port, timeout=1.0):
"""Raise an error if the given port is not free on the given host."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
host = client_host(host)
port = int(port)
import socket
# AF_INET or AF_INET6 socket
# Get the correct address family for our host (allows IPv6 addresses)
try:
@@ -364,10 +393,12 @@ def check_port(host, port, timeout=1.0):
socket.SOCK_STREAM)
except socket.gaierror:
if ':' in host:
info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0))]
info = [(
socket.AF_INET6, socket.SOCK_STREAM, 0, '', (host, port, 0, 0)
)]
else:
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, '', (host, port))]
for res in info:
af, socktype, proto, canonname, sa = res
s = None
@@ -378,25 +409,27 @@ def check_port(host, port, timeout=1.0):
s.settimeout(timeout)
s.connect((host, port))
s.close()
raise IOError("Port %s is in use on %s; perhaps the previous "
"httpserver did not shut down properly." %
(repr(port), repr(host)))
except socket.error:
if s:
s.close()
else:
raise IOError('Port %s is in use on %s; perhaps the previous '
'httpserver did not shut down properly.' %
(repr(port), repr(host)))
# Feel free to increase these defaults on slow systems:
free_port_timeout = 0.1
occupied_port_timeout = 1.0
def wait_for_free_port(host, port, timeout=None):
"""Wait for the specified port to become free (drop requests)."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
if timeout is None:
timeout = free_port_timeout
for trial in range(50):
try:
# we are expecting a free port, so reduce the timeout
@@ -406,8 +439,9 @@ def wait_for_free_port(host, port, timeout=None):
time.sleep(timeout)
else:
return
raise IOError("Port %r not free on %r" % (port, host))
raise IOError('Port %r not free on %r' % (port, host))
def wait_for_occupied_port(host, port, timeout=None):
"""Wait for the specified port to become active (receive requests)."""
@@ -415,13 +449,22 @@ def wait_for_occupied_port(host, port, timeout=None):
raise ValueError("Host values of '' or None are not allowed.")
if timeout is None:
timeout = occupied_port_timeout
for trial in range(50):
try:
check_port(host, port, timeout=timeout)
except IOError:
# port is occupied
return
else:
time.sleep(timeout)
raise IOError("Port %r not bound on %r" % (port, host))
if host == client_host(host):
raise IOError('Port %r not bound on %r' % (port, host))
# On systems where a loopback interface is not available and the
# server is bound to all interfaces, it's difficult to determine
# whether the server is in fact occupying the port. In this case,
# just issue a warning and move on. See issue #1100.
msg = 'Unable to verify that the server is bound on %r' % port
warnings.warn(msg)

View File

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

View File

@@ -61,14 +61,20 @@ the new state.::
"""
import atexit
import ctypes
import operator
import os
import subprocess
import sys
import threading
import time
import traceback as _traceback
import warnings
from cherrypy._cpcompat import set
import six
from cherrypy._cpcompat import _args_from_interpreter_flags
# Here I save the value of os.getcwd(), which, if I am imported early enough,
# will be the directory from which the startup script was run. This is needed
@@ -78,24 +84,25 @@ from cherrypy._cpcompat import set
# sys.executable is a relative-path, and/or cause other problems).
_startup_cwd = os.getcwd()
class ChannelFailures(Exception):
"""Exception raised when errors occur in a listener during Bus.publish()."""
"""Exception raised when errors occur in a listener during Bus.publish().
"""
delimiter = '\n'
def __init__(self, *args, **kwargs):
# Don't use 'super' here; Exceptions are old-style in Py2.4
# See http://www.cherrypy.org/ticket/959
Exception.__init__(self, *args, **kwargs)
super(Exception, self).__init__(*args, **kwargs)
self._exceptions = list()
def handle_exception(self):
"""Append the current exception to self."""
self._exceptions.append(sys.exc_info()[1])
def get_instances(self):
"""Return a list of seen exception instances."""
return self._exceptions[:]
def __str__(self):
exception_strings = map(repr, self.get_instances())
return self.delimiter.join(exception_strings)
@@ -107,12 +114,16 @@ class ChannelFailures(Exception):
__nonzero__ = __bool__
# Use a flag to indicate the state of the bus.
class _StateEnum(object):
class State(object):
name = None
def __repr__(self):
return "states.%s" % self.name
return 'states.%s' % self.name
def __setattr__(self, key, value):
if isinstance(value, self.State):
value.name = key
@@ -137,61 +148,60 @@ else:
class Bus(object):
"""Process state-machine and messenger for HTTP site deployment.
All listeners for a given channel are guaranteed to be called even
if others at the same channel fail. Each failure is logged, but
execution proceeds on to the next listener. The only way to stop all
processing from inside a listener is to raise SystemExit and stop the
whole server.
"""
states = states
state = states.STOPPED
execv = False
max_cloexec_files = max_files
def __init__(self):
self.execv = False
self.state = states.STOPPED
channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main'
self.listeners = dict(
[(channel, set()) for channel
in ('start', 'stop', 'exit', 'graceful', 'log', 'main')])
(channel, set())
for channel in channels
)
self._priorities = {}
def subscribe(self, channel, callback, priority=None):
"""Add the given callback at the given channel (if not present)."""
if channel not in self.listeners:
self.listeners[channel] = set()
self.listeners[channel].add(callback)
ch_listeners = self.listeners.setdefault(channel, set())
ch_listeners.add(callback)
if priority is None:
priority = getattr(callback, 'priority', 50)
self._priorities[(channel, callback)] = priority
def unsubscribe(self, channel, callback):
"""Discard the given callback (if present)."""
listeners = self.listeners.get(channel)
if listeners and callback in listeners:
listeners.discard(callback)
del self._priorities[(channel, callback)]
def publish(self, channel, *args, **kwargs):
"""Return output of all subscribers for the given channel."""
if channel not in self.listeners:
return []
exc = ChannelFailures()
output = []
items = [(self._priorities[(channel, listener)], listener)
for listener in self.listeners[channel]]
try:
items.sort(key=lambda item: item[0])
except TypeError:
# Python 2.3 had no 'key' arg, but that doesn't matter
# since it could sort dissimilar types just fine.
items.sort()
raw_items = (
(self._priorities[(channel, listener)], listener)
for listener in self.listeners[channel]
)
items = sorted(raw_items, key=operator.itemgetter(0))
for priority, listener in items:
try:
output.append(listener(*args, **kwargs))
@@ -209,26 +219,26 @@ class Bus(object):
# Assume any further messages to 'log' will fail.
pass
else:
self.log("Error in %r listener %r" % (channel, listener),
self.log('Error in %r listener %r' % (channel, listener),
level=40, traceback=True)
if exc:
raise exc
return output
def _clean_exit(self):
"""An atexit handler which asserts the Bus is not running."""
if self.state != states.EXITING:
warnings.warn(
"The main thread is exiting, but the Bus is in the %r state; "
"shutting it down automatically now. You must either call "
"bus.block() after start(), or call bus.exit() before the "
"main thread exits." % self.state, RuntimeWarning)
'The main thread is exiting, but the Bus is in the %r state; '
'shutting it down automatically now. You must either call '
'bus.block() after start(), or call bus.exit() before the '
'main thread exits.' % self.state, RuntimeWarning)
self.exit()
def start(self):
"""Start all services."""
atexit.register(self._clean_exit)
self.state = states.STARTING
self.log('Bus STARTING')
try:
@@ -238,7 +248,7 @@ class Bus(object):
except (KeyboardInterrupt, SystemExit):
raise
except:
self.log("Shutting down due to error in start listener:",
self.log('Shutting down due to error in start listener:',
level=40, traceback=True)
e_info = sys.exc_info()[1]
try:
@@ -248,13 +258,13 @@ class Bus(object):
pass
# Re-raise the original error
raise e_info
def exit(self):
"""Stop all services and prepare to exit the process."""
exitstate = self.state
try:
self.stop()
self.state = states.EXITING
self.log('Bus EXITING')
self.publish('exit')
@@ -266,32 +276,32 @@ class Bus(object):
# signal handler, console handler, or atexit handler), so we
# can't just let exceptions propagate out unhandled.
# Assume it's been logged and just die.
os._exit(70) # EX_SOFTWARE
os._exit(70) # EX_SOFTWARE
if exitstate == states.STARTING:
# exit() was called before start() finished, possibly due to
# Ctrl-C because a start listener got stuck. In this case,
# we could get stuck in a loop where Ctrl-C never exits the
# process, so we just call os.exit here.
os._exit(70) # EX_SOFTWARE
os._exit(70) # EX_SOFTWARE
def restart(self):
"""Restart the process (may close connections).
This method does not restart the process from the calling thread;
instead, it stops the bus and asks the main thread to call execv.
"""
self.execv = True
self.exit()
def graceful(self):
"""Advise all services to reload."""
self.log('Bus graceful')
self.publish('graceful')
def block(self, interval=0.1):
"""Wait for the EXITING state, KeyboardInterrupt or SystemExit.
This function is intended to be called only by the main thread.
After waiting for the EXITING state, it also waits for all threads
to terminate, and then calls os.execv if self.execv is True. This
@@ -309,40 +319,48 @@ class Bus(object):
self.log('SystemExit raised: shutting down bus')
self.exit()
raise
# Waiting for ALL child threads to finish is necessary on OS X.
# See http://www.cherrypy.org/ticket/581.
# See https://github.com/cherrypy/cherrypy/issues/581.
# It's also good to let them all shut down before allowing
# the main thread to call atexit handlers.
# See http://www.cherrypy.org/ticket/751.
self.log("Waiting for child threads to terminate...")
# See https://github.com/cherrypy/cherrypy/issues/751.
self.log('Waiting for child threads to terminate...')
for t in threading.enumerate():
if t != threading.currentThread() and t.isAlive():
# Validate the we're not trying to join the MainThread
# that will cause a deadlock and the case exist when
# implemented as a windows service and in any other case
# that another thread executes cherrypy.engine.exit()
if (
t != threading.currentThread() and
t.isAlive() and
not isinstance(t, threading._MainThread)
):
# Note that any dummy (external) threads are always daemonic.
if hasattr(threading.Thread, "daemon"):
if hasattr(threading.Thread, 'daemon'):
# Python 2.6+
d = t.daemon
else:
d = t.isDaemon()
if not d:
self.log("Waiting for thread %s." % t.getName())
self.log('Waiting for thread %s.' % t.getName())
t.join()
if self.execv:
self._do_execv()
def wait(self, state, interval=0.1, channel=None):
"""Poll for the given state(s) at intervals; publish to channel."""
if isinstance(state, (tuple, list)):
states = state
else:
states = [state]
def _wait():
while self.state not in states:
time.sleep(interval)
self.publish(channel)
# From http://psyco.sourceforge.net/psycoguide/bugs.html:
# "The compiled machine code does not include the regular polling
# done by Python, meaning that a KeyboardInterrupt will not be
@@ -353,23 +371,34 @@ class Bus(object):
sys.modules['psyco'].cannotcompile(_wait)
except (KeyError, AttributeError):
pass
_wait()
def _do_execv(self):
"""Re-execute the current process.
This must be called from the main thread, because certain platforms
(OS X) don't allow execv to be called in a child thread very well.
"""
args = sys.argv[:]
try:
args = self._get_true_argv()
except NotImplementedError:
"""It's probably win32"""
# For the SABnzbd.exe binary we don't want interpreter flags
# https://github.com/cherrypy/cherrypy/issues/1526
if getattr(sys, 'frozen', False):
args = [sys.executable] + sys.argv
else:
args = [sys.executable] + _args_from_interpreter_flags() + sys.argv
self.log('Re-spawning %s' % ' '.join(args))
self._extend_pythonpath(os.environ)
if sys.platform[:4] == 'java':
from _systemrestart import SystemRestart
raise SystemRestart
else:
args.insert(0, sys.executable)
if sys.platform == 'win32':
args = ['"%s"' % arg for arg in args]
@@ -377,25 +406,77 @@ class Bus(object):
if self.max_cloexec_files:
self._set_cloexec()
os.execv(sys.executable, args)
@staticmethod
def _get_true_argv():
"""Retrieves all real arguments of the python interpreter
...even those not listed in ``sys.argv``
:seealso: http://stackoverflow.com/a/28338254/595220
:seealso: http://stackoverflow.com/a/6683222/595220
:seealso: http://stackoverflow.com/a/28414807/595220
"""
try:
char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p
argv = ctypes.POINTER(char_p)()
argc = ctypes.c_int()
ctypes.pythonapi.Py_GetArgcArgv(ctypes.byref(argc), ctypes.byref(argv))
except AttributeError:
"""It looks Py_GetArgcArgv is completely absent in MS Windows
:seealso: https://github.com/cherrypy/cherrypy/issues/1506
:ref: https://chromium.googlesource.com/infra/infra/+/69eb0279c12bcede5937ce9298020dd4581e38dd%5E!/
"""
raise NotImplementedError
else:
return argv[:argc.value]
@staticmethod
def _extend_pythonpath(env):
"""
If sys.path[0] is an empty string, the interpreter was likely
invoked with -m and the effective path is about to change on
re-exec. Add the current directory to $PYTHONPATH to ensure
that the new process sees the same path.
This issue cannot be addressed in the general case because
Python cannot reliably reconstruct the
original command line (http://bugs.python.org/issue14208).
(This idea filched from tornado.autoreload)
"""
path_prefix = '.' + os.pathsep
existing_path = env.get('PYTHONPATH', '')
needs_patch = (
sys.path[0] == '' and
not existing_path.startswith(path_prefix)
)
if needs_patch:
env['PYTHONPATH'] = path_prefix + existing_path
def _set_cloexec(self):
"""Set the CLOEXEC flag on all open files (except stdin/out/err).
If self.max_cloexec_files is an integer (the default), then on
platforms which support it, it represents the max open files setting
for the operating system. This function will be called just before
the process is restarted via os.execv() to prevent open files
from persisting into the new process.
Set self.max_cloexec_files to 0 to disable this behavior.
"""
for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
try:
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
except IOError:
continue
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
def stop(self):
"""Stop all services."""
self.state = states.STOPPING
@@ -403,7 +484,7 @@ class Bus(object):
self.publish('stop')
self.state = states.STOPPED
self.log('Bus STOPPED')
def start_with_callback(self, func, args=None, kwargs=None):
"""Start 'func' in a new thread T, then start self (and return T)."""
if args is None:
@@ -411,22 +492,28 @@ class Bus(object):
if kwargs is None:
kwargs = {}
args = (func,) + args
def _callback(func, *a, **kw):
self.wait(states.STARTED)
func(*a, **kw)
t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
t.setName('Bus Callback ' + t.getName())
t.start()
self.start()
return t
def log(self, msg="", level=20, traceback=False):
def log(self, msg='', level=20, traceback=False):
"""Log the given message. Append the last traceback if requested."""
if traceback:
msg += "\n" + "".join(_traceback.format_exception(*sys.exc_info()))
# Work-around for bug in Python's traceback implementation
# which crashes when the error message contains %1, %2 etc.
errors = sys.exc_info()
if '%' in errors[1].message:
errors[1].message = errors[1].message.replace('%', '#')
errors[1].args = [item.replace('%', '#') for item in errors[1].args]
msg += "\n" + "".join(_traceback.format_exception(*errors))
self.publish('log', msg, level)
bus = Bus()

View File

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,7 +1,7 @@
"""A library for integrating pyOpenSSL with CherryPy.
The OpenSSL module must be importable for SSL functionality.
You can obtain it from http://pyopenssl.sourceforge.net/
You can obtain it from `here <https://launchpad.net/pyopenssl>`_.
To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of
SSLAdapter. There are two ways to use SSL:
@@ -43,15 +43,16 @@ except ImportError:
SSL = None
class SSL_fileobject(wsgiserver.CP_fileobject):
class SSL_fileobject(wsgiserver.CP_makefile):
"""SSL file object attached to a socket object."""
ssl_timeout = 3
ssl_retry = .01
def _safe_call(self, is_reader, call, *args, **kwargs):
"""Wrap the given call with SSL error-trapping.
is_reader: if False EOF errors will be raised. If True, EOF errors
will return "" (to emulate normal sockets).
"""
@@ -67,45 +68,38 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
time.sleep(self.ssl_retry)
except SSL.WantWriteError:
time.sleep(self.ssl_retry)
except SSL.SysCallError, e:
except SSL.SysCallError as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ""
return ''
errnum = e.args[0]
if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
return ""
return ''
raise socket.error(errnum)
except SSL.Error, e:
except SSL.Error as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ""
return ''
thirdarg = None
try:
thirdarg = e.args[0][0][2]
except IndexError:
pass
if thirdarg == 'http request':
# The client is talking HTTP to an HTTPS server.
raise wsgiserver.NoSSLError()
raise wsgiserver.FatalSSLAlert(*e.args)
except:
raise
if time.time() - start > self.ssl_timeout:
raise socket.timeout("timed out")
def recv(self, *args, **kwargs):
buf = []
r = super(SSL_fileobject, self).recv
while True:
data = self._safe_call(True, r, *args, **kwargs)
buf.append(data)
p = self._sock.pending()
if not p:
return "".join(buf)
raise socket.timeout('timed out')
def recv(self, size):
return self._safe_call(True, super(SSL_fileobject, self).recv, size)
def sendall(self, *args, **kwargs):
return self._safe_call(False, super(SSL_fileobject, self).sendall,
*args, **kwargs)
@@ -116,15 +110,16 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
class SSLConnection:
"""A thread-safe wrapper for an SSL.Connection.
``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
"""
def __init__(self, *args):
self._ssl_conn = SSL.Connection(*args)
self._lock = threading.RLock()
for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
'renegotiate', 'bind', 'listen', 'connect', 'accept',
'setblocking', 'fileno', 'close', 'get_cipher_list',
@@ -140,7 +135,7 @@ class SSLConnection:
finally:
self._lock.release()
""" % (f, f))
def shutdown(self, *args):
self._lock.acquire()
try:
@@ -151,33 +146,34 @@ class SSLConnection:
class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
"""A wrapper for integrating pyOpenSSL with CherryPy."""
context = None
"""An instance of SSL.Context."""
certificate = None
"""The filename of the server SSL certificate."""
private_key = None
"""The filename of the server's private key file."""
certificate_chain = None
"""Optional. The filename of CA's intermediate certificate bundle.
This is needed for cheaper "chained root" SSL certificates, and should be
left as None if not required."""
def __init__(self, certificate, private_key, certificate_chain=None):
if SSL is None:
raise ImportError("You must install pyOpenSSL to use HTTPS.")
raise ImportError('You must install pyOpenSSL to use HTTPS.')
self.context = None
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
self._environ = None
def bind(self, sock):
"""Wrap and return the given socket."""
if self.context is None:
@@ -185,36 +181,32 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
conn = SSLConnection(self.context, sock)
self._environ = self.get_environ()
return conn
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
return sock, self._environ.copy()
def get_context(self):
"""Return an SSL.Context from self attributes."""
# See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
c = SSL.Context(SSL.SSLv23_METHOD)
c.use_privatekey_file(self.private_key)
if self.certificate_chain:
if isinstance(self.certificate_chain, unicode) and self.certificate_chain.encode('cp1252', 'ignore') == self.certificate_chain.encode('cp1252', 'replace'):
# Support buggy PyOpenSSL 0.14, which cannot handle Unicode names
c.load_verify_locations(self.certificate_chain.encode('cp1252', 'ignore'))
else:
c.load_verify_locations(self.certificate_chain)
c.load_verify_locations(self.certificate_chain)
c.use_certificate_file(self.certificate)
return c
def get_environ(self):
"""Return WSGI environ entries to be merged into each request."""
ssl_environ = {
"HTTPS": "on",
'HTTPS': 'on',
# pyOpenSSL doesn't provide access to any of these AFAICT
## 'SSL_PROTOCOL': 'SSLv2',
## SSL_CIPHER string The cipher specification name
## SSL_VERSION_INTERFACE string The mod_ssl program version
## SSL_VERSION_LIBRARY string The OpenSSL program version
}
# 'SSL_PROTOCOL': 'SSLv2',
# SSL_CIPHER string The cipher specification name
# SSL_VERSION_INTERFACE string The mod_ssl program version
# SSL_VERSION_LIBRARY string The OpenSSL program version
}
if self.certificate:
# Server certificate attributes
cert = open(self.certificate, 'rb').read()
@@ -222,33 +214,35 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
ssl_environ.update({
'SSL_SERVER_M_VERSION': cert.get_version(),
'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
## 'SSL_SERVER_V_START': Validity of server's certificate (start time),
## 'SSL_SERVER_V_END': Validity of server's certificate (end time),
})
for prefix, dn in [("I", cert.get_issuer()),
("S", cert.get_subject())]:
# 'SSL_SERVER_V_START':
# Validity of server's certificate (start time),
# 'SSL_SERVER_V_END':
# Validity of server's certificate (end time),
})
for prefix, dn in [('I', cert.get_issuer()),
('S', cert.get_subject())]:
# X509Name objects don't seem to have a way to get the
# complete DN string. Use str() and slice it instead,
# because str(dn) == "<X509Name object '/C=US/ST=...'>"
dnstr = str(dn)[18:-2]
wsgikey = 'SSL_SERVER_%s_DN' % prefix
ssl_environ[wsgikey] = dnstr
# The DN should be of the form: /k1=v1/k2=v2, but we must allow
# for any value to contain slashes itself (in a URL).
while dnstr:
pos = dnstr.rfind("=")
pos = dnstr.rfind('=')
dnstr, value = dnstr[:pos], dnstr[pos + 1:]
pos = dnstr.rfind("/")
pos = dnstr.rfind('/')
dnstr, key = dnstr[:pos], dnstr[pos + 1:]
if key and value:
wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
ssl_environ[wsgikey] = value
return ssl_environ
def makefile(self, sock, mode='r', bufsize=-1):
if SSL and isinstance(sock, SSL.ConnectionType):
timeout = sock.gettimeout()
@@ -257,4 +251,3 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
return f
else:
return wsgiserver.CP_fileobject(sock, mode, bufsize)

View File

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

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/sabnzbd16_32.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
icons/sabnzbd16_32green.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

@@ -2,6 +2,7 @@
uniConfig for SABnzbd 0.7.x
zoggy@sabnzbd.org
Changed by Safihre for 1.0.x
========================================================
LIBRARIES USED
@@ -12,43 +13,14 @@ jQuery
* http://www.gnu.org/licenses/gpl.html
* http://www.opensource.org/licenses/mit-license.php
jQuery UI
* Project repository: https://github.com/jquery/jquery-ui
* Dual licensed under the MIT and GPL licenses:
* http://www.gnu.org/licenses/gpl.html
* http://www.opensource.org/licenses/mit-license.php
jQuery Form Plugin
* Project repository: https://github.com/malsup/form
* Dual licensed under the MIT and GPL licenses:
* http://www.gnu.org/licenses/gpl.html
* http://www.opensource.org/licenses/mit-license.php
jQuery Tools (tabs)
* Project repository: https://github.com/jquerytools/jquerytools
Formalize
* Project repository: https://github.com/nathansmith/formalize
* Dual licensed under the MIT and GPL licenses:
* http://www.gnu.org/licenses/gpl.html
* http://www.opensource.org/licenses/mit-license.php
normalize.css
* Project repository: https://github.com/necolas/normalize.css
* Licensed under public domain
qTip2
* Project repository: https://github.com/craga89/qtip2
* Dual licensed under the MIT and GPL licenses:
* http://www.gnu.org/licenses/gpl.html
* http://www.opensource.org/licenses/mit-license.php
========================================================
IMAGES USED
/images/loading-*.gif
- source: http://www.AjaxLoad.info
/images/flags/*
- source: http://www.famfamfam.com/lab/icons/flags/
Bootstrap v3.3.6 (http://getbootstrap.com)
* Licensed under the MIT license
Changed by Safihre, Nov 2015
We include the icon-file directly into the CSS,
this way we avoid errors when HTTPS is enabled

View File

@@ -1,14 +1,36 @@
<!-- Content end -->
<!-- Content end -->
<div class="clearfix"></div>
<!-- Filebrowser modal -->
<div class="modal fade" id="filebrowser_modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title"></h4>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<!--#if not $nt#-->
<div class="checkbox">
<label>
<input type="checkbox" id="show_hidden_folders"> <span>$T('systemFolders')</span>
</label>
</div>
<!--#end if#-->
<button type="button" class="btn btn-danger" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> $T('cancel')</button>
<button type="button" class="btn btn-default" id="filebrowser_modal_accept"><span class="glyphicon glyphicon-ok"></span> $T('rss-accept')</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>
<div class="main-restarting modal-backdrop fade in" style="display: none;">
<div>
<strong><span class="glyphicon glyphicon-retweet"></span> $T('restarting-sab')</strong><br />
<small><span class="restarting-url"></span><span class="dotOne">.</span><span class="dotTwo">.</span><span class="dotThree">.</span></small>
</div>
</div>
<script>
function is_touch_device() {
return !!('ontouchstart' in window) ? 1 : 0;
}
if (is_touch_device() === 1) {
// touch device found
\$('#content').css('overflow-y', 'inherit');
\$('html').css('overflow-y', 'scroll');
}
</script>
</body>
</html>

View File

@@ -1,180 +1,205 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>SABnzbd $version - $T('queued'): $mbleft $T('MB')</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="noindex">
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
#if $pane == "Config"#
#set global $root = '../'#
#set global $root = '../'#
#else#
#set global $root = '../../'#
#set global $root = '../../'#
#end if#
<!DOCTYPE HTML>
<html lang="$active_lang">
<head>
<title>
SABnzbd $T('menu-config')
#if $pane != "Config" then "-" else ""#
#if $pane == "General" then $T('cmenu-general') else ""#
#if $pane == "Folders" then $T('cmenu-folders') else ""#
#if $pane == "Switches" then $T('cmenu-switches') else ""#
#if $pane == "Servers" then $T('cmenu-servers') else ""#
#if $pane == "Scheduling" then $T('cmenu-scheduling') else ""#
#if $pane == "Email" then $T('cmenu-notif') else ""#
#if $pane == "Categories" then $T('cmenu-cat') else ""#
#if $pane == "Sorting" then $T('cmenu-sorting') else ""#
#if $pane == "Special" then $T('cmenu-special') else ""#
#if $pane == "RSS" then $T('cmenu-rss') else ""#
</title>
<link rel="shortcut icon" href="${root}staticcfg/ico/favicon.ico">
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="${root}staticcfg/ico/apple-touch-icon-144x144-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="${root}staticcfg/ico/apple-touch-icon-114x114-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="${root}staticcfg/ico/apple-touch-icon-72x72-precomposed.png">
<link rel="apple-touch-icon-precomposed" href="${root}staticcfg/ico/apple-touch-icon-57x57-precomposed.png">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="apple-mobile-web-app-title" content="SABnzbd" />
<link rel="stylesheet" type="text/css" href="${root}staticcfg/css/style.css" />
<script src="${root}staticcfg/js/script.js"></script>
<link rel="apple-touch-icon" sizes="76x76" href="${root}staticcfg/ico/apple-touch-icon-76x76-precomposed.png" />
<link rel="apple-touch-icon" sizes="120x120" href="${root}staticcfg/ico/apple-touch-icon-120x120-precomposed.png" />
<link rel="apple-touch-icon" sizes="152x152" href="${root}staticcfg/ico/apple-touch-icon-152x152-precomposed.png" />
<link rel="apple-touch-icon" sizes="180x180" href="${root}staticcfg/ico/apple-touch-icon-180x180-precomposed.png" />
<link rel="apple-touch-icon" sizes="192x192" href="${root}staticcfg/ico/android-192x192.png" />
<script type="text/javascript">
\$.Browser = {
defaults: {
title: 'Choose Directory',
url: '${root}api?mode=browse&output=json&apikey=$session',
autocompleteURL: '${root}api?mode=browse&output=json&compact=1&apikey=$session'
<link rel="stylesheet" type="text/css" href="${root}staticcfg/bootstrap/css/bootstrap.min.css?v=$version" />
<link rel="stylesheet" type="text/css" href="${root}staticcfg/css/style.css?p=$pid" />
<link rel="shortcut icon" href="${root}staticcfg/ico/favicon.ico?v=$version" />
<script type="text/javascript">
// Keeping track of the form state
var formHasChanged = false;
var formWasSubmitted = false;
// Information we need
var sabSession = '$session';
var rootURL = '${root}'
var folderBrowseUrl = '${root}tapi?mode=browse&output=json&apikey=$session';
var folderSeperator = '#if $os.sep == '\\' then '\\\\' else '/'#'
// Translations
var configTranslate = new Object();
configTranslate.browseText = "$T('browse-folder')";
configTranslate.saveChanges = "$T('button-saveChanges')";
configTranslate.saving = "$T('smpl-saving')";
configTranslate.failed = "$T('smpl-failed')";
configTranslate.explainRestart = "$T('explain-Restart')";
configTranslate.wizzardRestart = "$T('restarting-sab')";
configTranslate.needRestart = "$T('restartRequired') $T('explain-needNewLogin')".replace(/\<br(\s*\/|)\>/g, '\n');
configTranslate.buttonRestart = "$T('button-restart')";
configTranslate.confirmLeave = "$T('confirmWithoutSavingPrompt')";
configTranslate.searchPages = ['$T('cmenu-general')', '$T('cmenu-folders')', '$T('cmenu-switches')', '$T('cmenu-sorting')', '$T('cmenu-notif')', '$T('cmenu-special')']
</script>
<script type="text/javascript" src="${root}staticcfg/js/jquery-3.1.1.min.js?v=$version"></script>
<script type="text/javascript" src="${root}staticcfg/bootstrap/js/bootstrap.min.js?v=$version"></script>
<script type="text/javascript" src="${root}staticcfg/js/script.js?v=$version"></script>
<script type="text/javascript">
// Set default functions for the autocomplete everywhere
\$.extend(\$.fn.typeahead.defaults, {
source: function (query, process) {
// If there's no seperator, it must be a relative path
if(query.split(folderSeperator).length < 2 && this.\$element.data('initialdir')) {
query = this.\$element.data('initialdir') + folderSeperator + query;
}
};
var config_pane = "$pane";
var help_uri = "$help_uri";
var apikey = "$session";
var confirmWithoutSavingPrompt = "$T('Plush-confirmWithoutSavingPrompt')";
function config_success() {
\$('.saveButton').each(function () {
\$(this).removeAttr("disabled").attr("value", "$T('button-saveChanges')");
});
}
function config_failure() {
\$('.saveButton').each(function () {
\$(this).removeAttr("disabled").attr("value", "$T('smpl-failed')");
// Get info from the API
return \$.get(folderBrowseUrl + '&compact=1&term=' + query, function (data) {
return process(data);
});
},
updater: function(item) {
// Is it a relative path?
if(item.indexOf(this.\$element.data('initialdir')) === 0) {
// Remove start
return item.replace(this.\$element.data('initialdir')+folderSeperator, '');
}
// Full path
return item
}
})
\$(document).ready(function () {
#if $pane != "Servers"#
\$('.col2 H3').click(function () { \$(this).parent().next().toggle() });
#end if#
\$('.sabnzbd_restart').click(function () {
\$('.sabnzbd_restart').each(function () {
\$(this).attr("disabled", "disabled");
});
var msg = "$T('explain-Restart')";
if (confirm(msg.replace(/\<br(\s*\/|)\>/g, '\n'))) {
\$(this).attr("value", "$T('wizard-restarting')");
location.href = '${root}config/restart?session=$session';
}
\$('.sabnzbd_restart').each(function () {
\$(this).removeAttr("disabled");
});
return false;
});
\$('#fullform').ajaxForm({
datatype: 'json',
beforeSubmit: function () {
\$('.saveButton').each(function () {
\$(this).attr("disabled", "disabled").attr("value", "$T('smpl-saving')");
});
},
success: function (json) {
if (json.error) {
\$('#config_err_msg').text(json.error);
setTimeout(config_failure, 1000);
}
else {
\$('#config_err_msg').text(" ");
setTimeout(config_success, 1000);
}
},
error: function () {
setTimeout(config_failure, 1000);
},
timeout: 3000
});
\$("#sidebar-trigger").click(function () {
if (\$("#sidebar-trigger").hasClass("trigger-left")) {
\$("#sidebar").animate({marginLeft: "-150px", queue: false}, 250);
\$("#sidebar-pane").animate({marginLeft: "-150px", queue: false}, 250);
\$("#content").animate({left: "6px", queue: false}, 250);
\$("#sidebar-trigger").removeClass("trigger-left").addClass("trigger-right");
} else {
\$("#sidebar").animate({marginLeft: "0px", queue: false}, 250);
\$("#sidebar-pane").animate({marginLeft: "0px", queue: false}, 250);
\$("#content").animate({left: "156px", queue: false}, 250);
\$("#sidebar-trigger").removeClass("trigger-right").addClass("trigger-left");
}
});
});
/*
* takes the inputs-elements found in the current selector
* and extracts the values into the provided data-object.
*/
\$.fn.extractFormDataTo = function(target)
{
var inputs = \$("input[type != 'submit'][type != 'button']", this);
// could use .serializeArray() but that omits unchecked items
inputs.each(function (i,elem) {
target[elem.name] = elem.value;
});
return this;
}
</script>
// to top right away
if(window.location.hash) {
scroll(0,0);
// void some browsers issue
setTimeout(function() { scroll(0,0); }, 1);
}
</script>
</head>
<body class="$pane">
<div id="sidebar">
<a href="${root}"><img src="${root}staticcfg/images/logo.png" width="120" height="45" id="logo" alt="[home]" /></a>
<div id="tab-container">
<a href="${root}config/">
<div #if $pane == "Config" then 'class="active"' else ""#>$T('menu-config')</div>
</a>
<a href="${root}config/general/">
<div #if $pane == "General" then 'class="active"' else ""#>$T('cmenu-general')</div>
</a>
<a href="${root}config/folders/">
<div #if $pane == "Folders" then 'class="active"' else ""#>$T('cmenu-folders')</div>
</a>
<a href="${root}config/switches/">
<div #if $pane == "Switches" then 'class="active"' else ""#>$T('cmenu-switches')</div>
</a>
<a href="${root}config/server/">
<div #if $pane == "Servers" then 'class="active"' else ""#>$T('cmenu-servers')</div>
</a>
<a href="${root}config/scheduling/">
<div #if $pane == "Scheduling" then 'class="active"' else ""#>$T('Plush-cmenu-scheduling')</div>
</a>
<a href="${root}config/notify/">
<div #if $pane == "Email" then 'class="active"' else ""#>$T('cmenu-notif')</div>
</a>
<!--#if 0#-->
<a href="${root}config/indexers/">
<div #if $pane == "Index Sites" then 'class="active"' else ""#>$T('cmenu-newzbin')</div>
</a>
<!--#end if#-->
<a href="${root}config/categories/">
<div #if $pane == "Categories" then 'class="active"' else ""#>$T('cmenu-cat')</div>
</a>
<a href="${root}config/sorting/">
<div #if $pane == "Sorting" then 'class="active"' else ""#>$T('cmenu-sorting')</div>
</a>
<a href="${root}config/special/">
<div #if $pane == "Special" then 'class="active"' else ""#>$T('cmenu-special')</div>
</a>
<a href="${root}config/rss/">
<div #if $pane == "RSS" then 'class="active"' else ""#>$T('cmenu-rss')</div>
</a>
<br/>
<a href="$helpuri$help_uri" target="_blank">
<div>$T('menu-help')</div>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-logo navbar-logo-small" href="${root}" title="$T('Home')">
#include $webdir + "/staticcfg/images/logo-small.svg"#
</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>
<a href="${root}config/general/" #if $pane == "General" then 'class="active"' else ""#>
<span class="glyphicon glyphicon-cog"></span>
<strong>$T('cmenu-general')</strong>
</a>
</li>
<li>
<a href="${root}config/folders/" #if $pane == "Folders" then 'class="active"' else ""#>
<span class="glyphicon glyphicon-folder-open"></span>
<strong>$T('cmenu-folders')</strong>
</a>
</li>
<li>
<a href="${root}config/server/" #if $pane == "Servers" then 'class="active"' else ""#>
<span class="glyphicon glyphicon-sort"></span>
<strong>$T('cmenu-servers')</strong>
</a>
</li>
<li>
<a href="${root}config/categories/" #if $pane == "Categories" then 'class="active"' else ""#>
<span class="glyphicon glyphicon-tag"></span>
<strong>$T('cmenu-cat')</strong>
</a>
</li>
<li>
<a href="${root}config/switches/" #if $pane == "Switches" then 'class="active"' else ""#>
<span class="glyphicon glyphicon-check"></span>
<strong>$T('cmenu-switches')</strong>
</a>
</li>
<li>
<a href="${root}config/sorting/" #if $pane == "Sorting" then 'class="active"' else ""#>
<span class="glyphicon glyphicon-sort-by-alphabet"></span>
<strong>$T('cmenu-sorting')</strong>
</a>
</li>
<li>
<a href="${root}config/notify/" #if $pane == "Email" then 'class="active"' else ""#>
<span class="glyphicon glyphicon-comment"></span>
<strong>$T('cmenu-notif')</strong>
</a>
</li>
<li>
<a href="${root}config/scheduling/" #if $pane == "Scheduling" then 'class="active"' else ""#>
<span class="glyphicon glyphicon-time"></span>
<strong>$T('cmenu-scheduling')</strong>
</a>
</li>
<li>
<a href="${root}config/rss/" #if $pane == "RSS" then 'class="active"' else ""#>
<svg class="rss-icon-svg" viewBox="0 0 8 8">
<rect class="rss-button" width="8" height="8" />
<circle class="rss-symbol" cx="2" cy="6" r="1" />
<path class="rss-symbol" d="m 1,4 a 3,3 0 0 1 3,3 h 1 a 4,4 0 0 0 -4,-4 z" />
<path class="rss-symbol" d="m 1,2 a 5,5 0 0 1 5,5 h 1 a 6,6 0 0 0 -6,-6 z" />
</svg>
<strong>$T('cmenu-rss')</strong>
</a>
</li>
<li>
<a href="${root}config/special/" #if $pane == "Special" then 'class="active"' else ""#>
<span class="glyphicon glyphicon-education"></span>
<strong>$T('cmenu-special')</strong>
</a>
</li>
<li>
<a href="$helpuri$help_uri" target="_blank">
<span class="glyphicon glyphicon-question-sign"></span>
<strong>$T('menu-help')</strong>
</a>
</li>
<li class="dropdown" id="search-menu">
<a data-toggle="dropdown" href="#">
<span class="glyphicon glyphicon-search"></span>
<strong><span class="glyphicon glyphicon-search"></span></strong>
</a>
<ul class="dropdown-menu" id="search-dropdown">
<li>
<form onsubmit="return false">
<input type="text" name="config-search" id="search-box" placeholder="$T('cmenu-search')" onkeyup="doConfigSearch(this)">
</form>
</li>
</ul>
</li>
</ul>
</div>
</div>
<div id="sidebar-pane"><div id="sidebar-trigger" class="trigger-left"></div></div>
<div id="content">
<!-- Content start -->
</nav>
<div id="content" class="container">
<!-- Content start -->

View File

@@ -1,29 +1,151 @@
<!--#set global $pane="Config"#-->
<!--#set global $help_uri="configure-0-7"#-->
<!--#set global $help_uri="configuration/2.1/configure"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<!--#from locale import getpreferredencoding#-->
<div class="colmask">
<div class="section padTable">
<table id="infoTable">
<tbody>
<tr class="alt"><td class="infoTableHeader">$T('version'): </td><td class="infoTableCell">$version</td></tr>
<tr><td class="infoTableHeader">$T('uptime'): </td><td class="infoTableCell">$uptime</td></tr>
<tr class="alt"><td class="infoTableHeader">$T('confgFile'): </td><td class="infoTableCell">$configfn</td></tr>
<tr><td class="infoTableHeader">$T('cache').capitalize(): </td><td class="infoTableCell"><!--#set $msg=$T('ft-buffer@2')%($cache_art, $cache_size)#-->$msg</td></tr>
<tr class="alt"><td class="infoTableHeader">$T('parameters'): </td><td class="infoTableCell">$cmdline</td></tr>
<tr><td class="infoTableHeader">$T('pythonVersion'): </td><td class="infoTableCell">$sys.version[:120]</td></tr>
<tr class="infoTableSeperator alt"><td class="infoTableHeader">$T('homePage') </td><td class="infoTableCell"><a href="http://sabnzbd.org/" target="_blank">http://sabnzbd.org/</a></td></tr>
<tr><td class="infoTableHeader">$T('menu-wiki') </td><td class="infoTableCell"><a href="http://wiki.sabnzbd.org/faq" target="_blank">http://wiki.sabnzbd.org/faq</a></td></tr>
<tr class="alt"><td class="infoTableHeader">$T('menu-forums') </td><td class="infoTableCell"><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td></tr>
<tr><td class="infoTableHeader">$T('source') </td><td class="infoTableCell"><a href="https://github.com/sabnzbd/sabnzbd" target="_blank">https://github.com/sabnzbd/sabnzbd</a></td></tr>
<tr class="alt"><td class="infoTableHeader">$T('menu-irc') </td><td class="infoTableCell"><a href="irc://irc.synirc.net/#sabnzbd"><i>#sabnzbd</i> on <i>irc.synirc.net</i></a> $T('or') (<a href="http://sabnzbd.org/live-chat/" target="_blank">webchat</a>)</td></tr>
</tbody>
</table>
</div>
<div class="padding alt">
<h5 class="copyright">Copyright &copy; 2008-2015 The SABnzbd Team &lt;<span style="color: #0000ff;">team@sabnzbd.org</span>&gt;</h5>
<p class="copyright"><small>$T('yourRights')</small></p>
<div class="section padTable">
<table class="table table-striped">
<tbody>
<tr>
<th scope="row">$T('version'): </th>
<td>$version [$build]</td>
</tr>
<tr>
<th scope="row">$T('uptime'): </th>
<td>$uptime</td>
</tr>
<tr>
<th scope="row">$T('confgFile'): </th>
<td>$configfn</td>
</tr>
<tr>
<th scope="row">$T('parameters'): </th>
<td>$cmdline</td>
</tr>
<tr>
<th scope="row">$T('pythonVersion'): </th>
<td>$sys.version[:120] [$getpreferredencoding()]</td>
</tr>
<tr>
<th scope="row">OpenSSL:</th>
<td>
$ssl_version &nbsp; [$ssl_protocols]
</td>
</tr>
<!--#if not $have_ssl_context#-->
<tr>
<th scope="row"></th>
<td>
<span class="label label-danger">$T('warning')</span> $T('explain-nosslcontext')
</td>
</tr>
<!--#end if#-->
<!--#if not $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#-->
<span class="label label-warning">$T('notAvailable')</span> $T('explain-getpar2mt')
<a href="${helpuri}installation/multicore-par2" target="_blank">${helpuri}installation/multicore-par2</a>
<!--#end if#-->
</td>
</tr>
<!--#end if#-->
<!--#if not $have_cryptography #-->
<tr>
<th scope="row">Python Cryptography:</th>
<td>
<span class="label label-warning">$T('notAvailable')</span>
<a href="$helpuri$help_uri#no_cryptography" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
<!--#if not $have_yenc and not $have_sabyenc#-->
<tr>
<th scope="row">yEnc:</th>
<td>
<span class="label label-danger">$T('notAvailable')</span>
<a href="$helpuri$help_uri#no_yenc" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
<!--#if not $have_sabyenc#-->
<tr>
<th scope="row">SABYenc:</th>
<td>
<span class="label label-danger">$T('notAvailable')</span>
<a href="$helpuri$help_uri#no_sabyenc" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
<!--#if not $have_unzip #-->
<tr>
<th scope="row">$T('opt-enable_unzip'):</th>
<td>
<span class="label label-warning">$T('notAvailable')</span>
<a href="${helpuri}installation/install-off-modules#toc8" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
<!--#if not $have_7zip #-->
<tr>
<th scope="row">$T('opt-enable_7zip'):</th>
<td>
<span class="label label-warning">$T('notAvailable')</span>
<a href="${helpuri}installation/install-off-modules#toc8" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a>
</td>
</tr>
<!--#end if#-->
</tbody>
</table>
</div>
</div>
<div class="colmask">
<div class="section padTable">
<table class="table table-striped">
<tbody>
<tr>
<th scope="row">$T('homePage') </th>
<td><a href="https://sabnzbd.org/" target="_blank">https://sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('menu-wiki') </th>
<td><a href="https://sabnzbd.org/wiki/" target="_blank">https://sabnzbd.org/wiki/</a></td>
</tr>
<tr>
<th scope="row">$T('menu-forums') </th>
<td><a href="https://forums.sabnzbd.org/" target="_blank">https://forums.sabnzbd.org/</a></td>
</tr>
<tr>
<th scope="row">$T('source') </th>
<td><a href="https://github.com/sabnzbd/sabnzbd" target="_blank">https://github.com/sabnzbd/sabnzbd</a></td>
</tr>
<tr>
<th scope="row">$T('menu-irc') </th>
<td><a href="irc://irc.synirc.net/#sabnzbd"><i>#sabnzbd</i> on <i>irc.synirc.net</i></a> $T('or') (<a href="http://sabnzbd.org/live-chat/" target="_blank">webchat</a>)</td>
</tr>
<tr>
<th scope="row">$T('menu-issues') </th>
<td><a href="https://sabnzbd.org/wiki/introduction/known-issues" target="_blank">https://sabnzbd.org/wiki/introduction/known-issues</a></td>
</tr>
<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>
</table>
</div>
</div>
<div class="colmask">
<div class="padding alt">
<h5 class="copyright">Copyright &copy; 2008-2017 The SABnzbd Team &lt;<a href="mailto:team@sabnzbd.org">team@sabnzbd.org</a>&gt;</h5>
<p class="copyright"><small>$T('yourRights')</small></p>
</div>
</div>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

View File

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

View File

@@ -1,14 +1,14 @@
<!--#set global $pane="Folders"#-->
<!--#set global $help_uri="configure-folders-0-7"#-->
<!--#set global $help_uri="configuration/2.1/folders"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<form action="saveDirectories" method="post" name="fullform" id="fullform">
<form action="saveDirectories" method="post" name="fullform" class="fullform" autocomplete="off">
<input type="hidden" id="session" name="session" value="$session" />
<input type="hidden" id="ajax" name="ajax" value=1 />
<input type="hidden" id="ajax" name="ajax" value="1" />
<div class="section">
<div class="col2">
<h3>$T('userFolders')</h3>
<h3>$T('userFolders') <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p>$T('explain-folderConfig')</p>
</div><!-- /col2 -->
<div class="col1">
@@ -16,53 +16,56 @@
<div class="field-pair">
<h5 class="darkred nomargin">$T('base-folder'): <span class="path">$my_home</span></h5>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="download_dir">$T('opt-download_dir')</label>
<input type="text" name="download_dir" id="download_dir" value="$download_dir" size="45" />
<input type="text" name="download_dir" id="download_dir" value="$download_dir" data-initialdir="$my_home" />
<span class="desc">$T('explain-download_dir')</span>
</div>
<div class="field-pair">
<label class="config" for="download_free">$T('opt-download_free')</label>
<input type="text" name="download_free" id="download_free" value="$download_free" size="8" />
<input type="text" name="download_free" id="download_free" value="$download_free" class="smaller_input" />
<span class="desc">$T('explain-download_free')</span>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="complete_dir">$T('opt-complete_dir')</label>
<input type="text" name="complete_dir" id="complete_dir" value="$complete_dir" size="45" />
<input type="text" name="complete_dir" id="complete_dir" value="$complete_dir" data-initialdir="$my_home" />
<span class="desc">$T('explain-complete_dir')</span>
</div>
<div class="field-pair <!--#if $nt then "disabled" else "" #-->">
<!--#if not $nt#-->
<div class="field-pair">
<label class="config" for="permissions">$T('opt-permissions')</label>
<input type="text" name="permissions" id="permissions" value="$permissions" size="8" <!--#if $nt then 'readonly="readonly" disabled="disabled"' else "" #--> />
<input type="text" name="permissions" id="permissions" value="$permissions" class="smaller_input" />
<span class="desc">$T('explain-permissions')</span>
</div>
<div class="field-pair alt">
<!--#end if#-->
<div class="field-pair">
<label class="config" for="dirscan_dir">$T('opt-dirscan_dir')</label>
<input type="text" name="dirscan_dir" id="dirscan_dir" value="$dirscan_dir" size="45" />
<input type="text" name="dirscan_dir" id="dirscan_dir" value="$dirscan_dir" data-initialdir="$my_home" />
<span class="desc">$T('explain-dirscan_dir')</span>
</div>
<div class="field-pair">
<label class="config" for="dirscan_speed">$T('opt-dirscan_speed')</label>
<input type="number" name="dirscan_speed" id="dirscan_speed" value="$dirscan_speed" size="8" min="0" max="3600" />
<input type="number" name="dirscan_speed" id="dirscan_speed" value="$dirscan_speed" min="0" max="3600" />
<span class="desc">$T('explain-dirscan_speed')</span>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="script_dir">$T('opt-script_dir')</label>
<input type="text" name="script_dir" id="script_dir" value="$script_dir" size="45" />
<input type="text" name="script_dir" id="script_dir" value="$script_dir" data-initialdir="$my_home" />
<span class="desc">$T('explain-script_dir')</span>
</div>
<div class="field-pair">
<label class="config" for="email_dir">$T('opt-email_dir')</label>
<input type="text" name="email_dir" id="email_dir" value="$email_dir" size="45" />
<input type="text" name="email_dir" id="email_dir" value="$email_dir" data-initialdir="$my_home" />
<span class="desc">$T('explain-email_dir')</span>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="password_file">$T('opt-password_file')</label>
<input type="text" name="password_file" id="password_file" value="$password_file" size="45" />
<input type="text" name="password_file" id="password_file" value="$password_file" />
<span class="desc">$T('explain-password_file')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span id="config_err_msg" class="darkred nomargin">&nbsp;</span>
</div>
@@ -71,7 +74,7 @@
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('systemFolders')</h3>
<h3>$T('systemFolders') <a href="$helpuri$help_uri#toc1" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p>$T('explain-folderConfig')</p>
</div><!-- /col2 -->
<div class="col1">
@@ -79,43 +82,35 @@
<div class="field-pair">
<h5 class="darkred nomargin">$T('base-folder'): <span class="path">$my_lcldata</span></h5>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="admin_dir">$T('opt-admin_dir')</label>
<input type="text" name="admin_dir" id="admin_dir" value="$admin_dir" size="45" />
<input type="text" name="admin_dir" id="admin_dir" value="$admin_dir" data-initialdir="$my_lcldata" />
<span class="desc">$T('explain-admin_dir1')</span>
<span class="desc">$T('explain-admin_dir2')</span>
</div>
<div class="field-pair">
<label class="config" for="log_dir">$T('opt-log_dir')</label>
<input type="text" name="log_dir" id="log_dir" value="$log_dir" size="45" />
<input type="text" name="log_dir" id="log_dir" value="$log_dir" data-initialdir="$my_lcldata" />
<span class="desc">$T('explain-log_dir')</span>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="nzb_backup_dir">$T('opt-nzb_backup_dir')</label>
<input type="text" name="nzb_backup_dir" id="nzb_backup_dir" value="$nzb_backup_dir" size="45" />
<input type="text" name="nzb_backup_dir" id="nzb_backup_dir" value="$nzb_backup_dir" data-initialdir="$my_lcldata" />
<span class="desc">$T('explain-nzb_backup_dir')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
</div>
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="padding alt">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
</div>
</form>
</div><!-- /colmask -->
<script>
\$(document).ready(function(){
\$('#download_dir').fileBrowser({ title: 'Select $T('opt-download_dir')' });
\$('#complete_dir').fileBrowser({ title: 'Select $T('opt-complete_dir')' });
\$('#dirscan_dir').fileBrowser({ title: 'Select $T('opt-dirscan_dir')' });
\$('#script_dir').fileBrowser({ title: 'Select $T('opt-script_dir')' });
\$('#email_dir').fileBrowser({ title: 'Select $T('opt-email_dir')' });
});
<script type="text/javascript">
jQuery(document).ready(function() {
// Add autocomplete and file-browser
\$('.col1 input[name$="_dir"]').typeahead().fileBrowser();
})
</script>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

View File

@@ -1,198 +1,260 @@
<!--#set global $pane="General"#-->
<!--#set global $help_uri="configure-general-0-8"#-->
<!--#set global $help_uri="configuration/2.1/general"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<form action="saveGeneral" method="post" name="fullform" id="fullform" novalidate>
<form action="saveGeneral" method="post" name="fullform" class="fullform" autocomplete="off" novalidate>
<input type="hidden" id="session" name="session" value="$session" />
<input type="hidden" id="ajax" name="ajax" value=1 />
<div class="section">
<div class="col2">
<h3>$T('webServer')</h3>
<h3>$T('webServer') <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>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="host">$T('opt-host')</label>
<input type="url" name="host" id="host" value="$host" size="40" />
<input type="text" name="host" id="host" value="$host" />
<span class="desc">$T('explain-host')</span>
</div>
<div class="field-pair">
<label class="config" for="port">$T('opt-port')</label>
<input type="text" name="port" id="port" value="$port" size="8" />
<input type="number" name="port" id="port" value="$port" size="8" data-original="$port" />
<span class="desc">$T('explain-port')</span>
</div>
<div class="field-pair alt">
<label class="config" for="username">$T('opt-web_username')</label>
<input type="text" name="username" id="username" value="$username" size="30" />
<span class="desc">$T('explain-web_username')</span>
<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">
<label class="config" for="password">$T('opt-web_password')</label>
<input type="password" name="password" id="password" value="$password" size="30" />
<span class="desc">$T('explain-web_password')</span>
</div>
<div class="field-pair alt">
<label class="config" for="web_dir">$T('opt-web_dir')</label>
<select name="web_dir" id="web_dir">
<!--#for $webline in $web_list#-->
<!--#if $webline.lower() == $web_dir.lower()#-->
<option value="$webline" selected="selected" class="selected">$webline</option>
<!--#else#-->
<option value="$webline">$webline</option>
<!--#end if#-->
<!--#end for#-->
<select name="web_dir" id="web_dir">
<!--#for $webline in $web_list#-->
<!--#if $webline.lower() == $web_dir.lower()#-->
<option value="$webline" selected="selected">$webline</option>
<!--#else#-->
<option value="$webline">$webline</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-web_dir')&nbsp;&nbsp;<a href="$caller_url1">$caller_url1</a></span>
<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">
<!--#for $webline in $web_list2#-->
<!--#if $webline.lower() == $web_dir2.lower()#-->
<option value="$webline" selected="selected" class="selected">$webline</option>
<!--#else#-->
<option value="$webline">$webline</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-web_dir2')&nbsp;&nbsp;<a href="$caller_url2">$caller_url2</a></span>
</div>
<div class="field-pair alt">
<label class="config" for="language">$T('opt-language')</label>
<select name="language" id="language" id="language" class="select">
<!--#for $webline in $lang_list#-->
<!--#if $webline[0].lower() == $language.lower()#-->
<option value="$webline[0]" selected="selected" class="selected">$webline[1]</option>
<!--#else#-->
<option value="$webline[0]">$webline[1]</option>
<!--#end if#-->
<!--#end for#-->
</select>
<select name="language" id="language" class="select">
<!--#for $webline in $lang_list#-->
<!--#if $webline[0].lower() == $language.lower()#-->
<option value="$webline[0]" selected="selected">$webline[1]</option>
<!--#else#-->
<option value="$webline[0]">$webline[1]</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-language')</span>
<div class="alert alert-info alert-translate">
$T('explain-ask-language') <a href="https://sabnzbd.org/wiki/translate" target="_blank" class="alert-link">https://sabnzbd.org/wiki/translate</a>
</div>
</div>
<div class="field-pair advanced-settings">
<h5 class="darkred nomargin">$T('base-folder'): <span class="path">$my_lcldata</span></h5>
</div>
<div class="field-pair advanced-settings">
<label class="config" for="https_port">$T('opt-https_port')</label>
<input type="number" name="https_port" id="https_port" value="$https_port" size="8" 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">
<label class="config" for="local_ranges">$T('opt-local_ranges')</label>
<input type="text" name="local_ranges" id="local_ranges" value="$local_ranges" size="30" />
<span class="desc">$T('explain-local_ranges')</span>
</div>
<div class="field-pair alt">
<label class="config" for="inet_exposure">$T('opt-inet_exposure')</label>
<select name="inet_exposure" id="inet_exposure" id="inet_exposure" class="select">
<option value="0" <!--#if $inet_exposure == 0 then 'selected="selected" class="selected"' else ""#-->>$T('inet-local')</option>
<option value="1" <!--#if $inet_exposure == 1 then 'selected="selected" class="selected"' else ""#-->>$T('inet-nzb')</option>
<option value="2" <!--#if $inet_exposure == 2 then 'selected="selected" class="selected"' else ""#-->>$T('inet-api')</option>
<option value="3" <!--#if $inet_exposure == 3 then 'selected="selected" class="selected"' else ""#-->>$T('inet-fullapi')</option>
<option value="4" <!--#if $inet_exposure == 4 then 'selected="selected" class="selected"' else ""#-->>$T('inet-ui')</option>
</select>
<span class="desc">$T('explain-inet_exposure')</span>
</div>
<div class="field-pair">
<label class="config" for="disable_api_key">$T('opt-disableApikey')</label>
<input type="checkbox" name="disable_api_key" id="disable_api_key" value="1" <!--#if int($disable_api_key) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-disableApikey')</span>
</div>
<div class="field-pair alt">
<label class="config" for="apikey">$T('opt-apikey')</label>
<input type="text" id="apikey" value="$session" size="35" />
<input type="button" value="$T('button-apikey')" id="generate_new_apikey" />
<input type="button" value="$T('qr-code')" title="$T('explain-qr-code')" class="show_qrcode" rel="$session" />
<span class="desc">$T('explain-apikey')</span>
</div>
<div class="field-pair">
<label class="config" for="nzbkey">$T('opt-nzbkey')</label>
<input type="text" id="nzbkey" value="$nzb_key" size="35" />
<input type="button" value="$T('button-apikey')" id="generate_new_nzbkey" />
<input type="button" value="$T('qr-code')" title="$T('explain-qr-code')" class="show_qrcode" rel="$nzb_key" />
<span class="desc">$T('explain-nzbkey')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
<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><!-- /col1 -->
</div><!-- /section -->
</div>
</div>
<div class="section">
<div class="col2">
<h3>$T('httpsSupport')</h3>
<h3>$T('security') <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p><b>$T('restartRequired')</b></p>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<!-- Tricks to avoid browser auto-fill, fixed on-submit with javascript -->
<div class="field-pair">
<h5 class="darkred nomargin">$T('base-folder'): <span class="path">$my_lcldata</span></h5>
</div>
<div class="field-pair alt <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="enable_https">$T('opt-enable_https')</label>
<input type="checkbox" name="enable_https" id="enable_https" value="1" <!--#if int($enable_https) > 0 then 'checked="checked"' else ""#--> <!--#if int($have_ssl) == 0 then "disabled" else ""#--> />
<span class="desc">$T('explain-enable_https')</span>
<label class="config" for="${pid}_wu">$T('opt-web_username')</label>
<input type="text" name="${pid}_wu" id="${pid}_wu" value="$username" data-hide="username" />
<span class="desc">$T('explain-web_username')</span>
</div>
<div class="field-pair">
<label class="config" for="https_port">$T('opt-https_port')</label>
<input type="text" name="https_port" id="https_port" value="$https_port" size="8" />
<span class="desc">$T('explain-https_port')</span>
</div>
<div class="field-pair alt">
<label class="config" for="https_cert">$T('opt-https_cert')</label>
<input type="text" name="https_cert" id="https_cert" value="$https_cert" size="50" />
<span class="desc">$T('explain-https_cert')</span>
<label class="config" for="${pid}_wp">$T('opt-web_password')</label>
<input type="text" name="${pid}_wp" id="${pid}_wp" value="$password" data-hide="password" />
<span class="desc">$T('explain-web_password')</span>
</div>
<div class="field-pair">
<label class="config" for="https_key">$T('opt-https_key')</label>
<input type="text" name="https_key" id="https_key" value="$https_key" size="50" />
<span class="desc">$T('explain-https_key')</span>
</div>
<div class="field-pair alt">
<label class="config" for="https_chain">$T('opt-https_chain')</label>
<input type="text" name="https_chain" id="https_chain" value="$https_chain" size="50" />
<span class="desc">$T('explain-https_chain')</span>
<label class="config" for="inet_exposure">$T('opt-inet_exposure')</label>
<select name="inet_exposure" id="inet_exposure" class="select">
<optgroup label="API">
<option value="0" <!--#if $inet_exposure == 0 then 'selected="selected"' else ""#-->>$T('inet-local')</option>
<option value="1" <!--#if $inet_exposure == 1 then 'selected="selected"' else ""#-->>$T('inet-nzb')</option>
<option value="2" <!--#if $inet_exposure == 2 then 'selected="selected"' else ""#-->>$T('inet-api')</option>
<option value="3" <!--#if $inet_exposure == 3 then 'selected="selected"' else ""#-->>$T('inet-fullapi')</option>
</optgroup>
<optgroup label="$T('inet-fullapi') &amp; $T('opt-web_dir')">
<option value="4" <!--#if $inet_exposure == 4 then 'selected="selected"' else ""#-->>$T('inet-ui')</option>
<option value="5" <!--#if $inet_exposure == 5 then 'selected="selected"' else ""#-->>$T('inet-ui') - $T('inet-external_login')</option>
</optgroup>
</select>
<span class="desc">$T('explain-inet_exposure').replace('. ','.<br><span class="label label-warning">'+$T('warning').upper()+'</span> ')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
<label class="config" for="local_ranges">$T('opt-local_ranges')</label>
<input type="text" name="local_ranges" id="local_ranges" value="$local_ranges" />
<span class="desc">$T('explain-local_ranges')</span>
</div>
<div class="field-pair">
<label class="config" for="apikey">$T('opt-apikey')</label>
<input type="text" id="apikey" class="fileBrowserField" value="$session" readonly />
<button class="btn btn-default show_qrcode" title="$T('explain-qr-code')" rel="$session" ><span class="glyphicon glyphicon-qrcode"></span></button>
<button class="btn btn-default generate_key" id="generate_new_apikey" title="$T('button-apikey')"><span class="glyphicon glyphicon-repeat"></span></button>
<span class="desc">$T('explain-apikey')</span>
</div>
<div class="field-pair">
<label class="config" for="nzbkey">$T('opt-nzbkey')</label>
<input type="text" id="nzbkey" class="fileBrowserField" value="$nzb_key" readonly />
<button class="btn btn-default show_qrcode" title="$T('explain-qr-code')" rel="$nzb_key" ><span class="glyphicon glyphicon-qrcode"></span></button>
<button class="btn btn-default generate_key" id="generate_new_nzbkey" title="$T('button-apikey')"><span class="glyphicon glyphicon-repeat"></span></button>
<span class="desc">$T('explain-nzbkey')</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('tuning')</h3>
<h3>$T('cmenu-switches') <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair alt">
<label class="config" for="bandwidth_max">$T('opt-bandwidth_max')</label>
<input type="text" name="bandwidth_max" id="bandwidth_max" value="$bandwidth_max"/>
<span class="desc">$T('explain-bandwidth_max')</span>
<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="bandwidth_perc">$T('opt-bandwidth_perc')</label>
<input type="number" name="bandwidth_perc" id="bandwidth_perc" value="$bandwidth_perc" size="8" step="10" min="0" max="100"/>
<span class="desc">$T('explain-bandwidth_perc')</span>
<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 alt">
<label class="config" for="cache_limit">$T('opt-cache_limitstr')</label>
<input type="text" name="cache_limit" id="cache_limit" value="$cache_limit" size="8" />
<span class="desc">$T('explain-cache_limitstr')</span>
<div class="field-pair <!--#if int($have_ssl_context) == 0 then "disabled" else ""#-->">
<label class="config" for="enable_https_verification">$T('opt-enable_https_verification')</label>
<input type="checkbox" name="enable_https_verification" id="enable_https_verification" value="1" <!--#if int($enable_https_verification) > 0 then 'checked="checked"' else ""#--> <!--#if int($have_ssl_context) == 0 then "disabled=\"disabled\"" else ""#--> />
<span class="desc">$T('explain-enable_https_verification')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="padding alt">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
</div>
<div class="section">
<div class="col2">
<h3>$T('tuning') <a href="$helpuri$help_uri#toc2" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair value-and-select">
<label class="config" for="bandwidth_max_value">$T('opt-bandwidth_max')</label>
<input type="number" name="bandwidth_max_value" id="bandwidth_max_value" class="smaller_input" />
<select name="bandwidth_max_dropdown" id="bandwidth_max_dropdown">
<option value="">B/s</option>
<option value="K">KB/s</option>
<option value="M" selected>MB/s</option>
</select>
<input type="hidden" name="bandwidth_max" id="bandwidth_max" value="$bandwidth_max" />
</div>
<div class="field-pair">
<label class="config" for="bandwidth_perc">$T('opt-bandwidth_perc')</label>
<input type="number" name="bandwidth_perc" id="bandwidth_perc" value="$bandwidth_perc" step="10" min="0" max="100"/>
<span class="desc">$T('explain-bandwidth_perc')</span>
</div>
<div class="field-pair">
<label class="config" for="cache_limit">$T('opt-cache_limitstr')</label>
<input type="text" name="cache_limit" id="cache_limit" value="$cache_limit" class="smaller_input" />
<span class="desc">$T('explain-cache_limitstr').replace("64M", "256M").replace("128M", "512M")</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
</form>
</div><!-- /colmask -->
<script>
<div class="modal fade" id="modal_qr">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<!-- For the QR code -->
</div>
</div>
</div>
</div>
<script type="text/javascript">
\$(document).ready(function(){
// Show the message about translating when it's non-English
function hideOrShowTranslate() {
if(\$('#language').val() == 'en') {
\$('.alert-translate').hide()
} else {
\$('.alert-translate').show()
}
}
\$('#language').on('change', 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()
\$('#apikey, #nzbkey').click(function () { \$(this).select() });
\$('#generate_new_apikey').click(function () {
if (confirm("$T('Plush-confirm')")) {
$.ajax({
@@ -201,7 +263,7 @@
data: {mode:'config', name:'set_apikey', apikey: \$('#apikey').val()},
success: function(msg){
\$('#apikey').val(msg);
window.location.reload();
document.location = document.location;
}
});
}
@@ -211,43 +273,75 @@
$.ajax({
type: "POST",
url: "../../tapi",
data: {mode:'config', name:'set_nzbkey', apikey: \$('#apikey').val()},
data: { mode:'config', name:'set_nzbkey', apikey: \$('#apikey').val() },
success: function(msg){
\$('#nzbkey').val(msg);
window.location.reload();
document.location = document.location;
}
});
}
});
\$('.show_qrcode').each(function () {
var qrkey = \$(this).attr('rel'),
qrcode = \$('<img />', {
src: 'https://chart.googleapis.com/chart?chs=300x300&cht=qr&chl=' + qrkey,
alt: 'loading...',
width: 300,
height: 300
});
\$(this).qtip( {
content: {
text: qrcode
},
position: {
my: 'center',
at: 'center',
target: \$(window)
},
show: {
event: 'click',
solo: true,
modal: true
},
hide: false,
style: 'ui-tooltip-light ui-tooltip-rounded ui-tooltip-shadow ui-tooltip-qrcode'
\$('.show_qrcode').click(function (e) {
// Show in modal
\$('#modal_qr .modal-dialog').width(330)
\$('#modal_qr .modal-body').html('').qrcode({
"size": 280,
"color": "#3a3",
"text": \$(this).attr('rel')
});
\$('#modal_qr').modal('show');
// No save on this button click
e.preventDefault();
});
\$('.generate_cert').click(function(e) {
if(!confirm('$T('explain-new-cert')')) {
return;
}
// Submit request and then restart
$.ajax({
type: "POST",
url: "../../tapi",
data: { mode: 'config', name: 'regenerate_certs', apikey: \$('#apikey').val() },
success: function(msg) {
do_restart()
}
});
e.preventDefault();
})
// Only allow re-generate if default certs
if(\$('#https_cert').val() != 'server.cert') {
\$('.generate_cert').attr('disabled', 'disabled')
}
// Parse the text
var bandwidthLimit = \$('#bandwidth_max').val()
if(bandwidthLimit) {
var bandwithLimitNumber = parseFloat(bandwidthLimit)
var bandwithLimitText = bandwidthLimit.replace(/[^a-zA-Z]+/g, '');
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>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

View File

@@ -1,23 +1,34 @@
<!--#set global $pane="Email"#-->
<!--#set global $help_uri="configure-notifications-0-7"#-->
<!--#set global $help_uri="configuration/2.1/notifications"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<!--#def show_notify_checkboxes($section_label)#-->
<!--#for $type in $notify_keys#-->
<div class="field-pair">
<label class="config wide" for="${section_label}_prio_$type">
$T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
</label>
<input type="checkbox" name="${section_label}_prio_$type" id="${section_label}_prio_$type" value="1" <!--#if int($getVar($section_label + '_prio_' + $type)) > 0 then 'checked="checked"' else ""#--> />
</div>
<!--#end for#-->
<!--#end def#-->
<div class="colmask">
<form action="saveEmail" method="post" name="fullform" id="fullform" 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="ajax" name="ajax" value=1 />
<input type="hidden" id="ajax" name="ajax" value="1" />
<div class="section" id="email">
<div class="col2">
<h3>$T('cmenu-email')</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="col1">
<fieldset>
<div class="field-pair alt">
<label class="config" for="email_endjob_radio">$T('opt-email_endjob')</label>
<div class="field-pair">
<label class="config" for="email_endjob">$T('opt-email_endjob')</label>
<select name="email_endjob" id="email_endjob">
<option value="0" <!--#if int($email_endjob) == 0 then 'selected="selected" class="selected"' else ""#--> >$T('email-never')</option>
<option value="1" <!--#if int($email_endjob) == 1 then 'selected="selected" class="selected"' else ""#--> >$T('email-always')</option>
<option value="2" <!--#if int($email_endjob) == 2 then 'selected="selected" class="selected"' else ""#--> >$T('email-errorOnly')</option>
<option value="0" <!--#if int($email_endjob) == 0 then 'selected="selected"' else ""#--> >$T('email-never')</option>
<option value="1" <!--#if int($email_endjob) == 1 then 'selected="selected"' else ""#--> >$T('email-always')</option>
<option value="2" <!--#if int($email_endjob) == 2 then 'selected="selected"' else ""#--> >$T('email-errorOnly')</option>
</select>
</div>
<div class="field-pair">
@@ -25,499 +36,391 @@
<input type="checkbox" name="email_full" id="email_full" value="1" <!--#if int($email_full) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-email_full')</span>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="email_rss">$T('opt-email_rss')</label>
<input type="checkbox" name="email_rss" id="email_rss" value="1" <!--#if int($email_rss) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-email_rss')</span>
</div>
<div class="field-pair">
<label class="config" for="email_dir">$T('opt-email_dir')</label>
<input type="text" name="email_dir" id="email_dir" value="$email_dir" size="45" />
<input type="text" name="email_dir" id="email_dir" value="$email_dir" data-initialdir="$my_home" />
<span class="desc">$T('explain-email_dir')</span>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="email_server">$T('opt-email_server')</label>
<input type="text" name="email_server" id="email_server" value="$email_server" size="40" />
<span class="desc">$T('explain-email_server')</span>
<input type="text" name="email_server" id="email_server" value="$email_server" />
<span class="desc">$T('explain-email_server') ($T('host'):$T('srv-port'))</span>
</div>
<div class="field-pair">
<label class="config" for="email_to">$T('opt-email_to')</label>
<input type="email" name="email_to" id="email_to" value="$email_to" size="40" />
<input type="text" name="email_to" id="email_to" value="$email_to" />
<span class="desc">$T('explain-email_to')</span>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="email_from">$T('opt-email_from')</label>
<input type="email" name="email_from" id="email_from" value="$email_from" size="40" />
<input type="text" name="email_from" id="email_from" value="$email_from" />
<span class="desc">$T('explain-email_from')</span>
</div>
<div class="field-pair">
<label class="config" for="email_account">$T('opt-email_account')</label>
<input type="email" name="email_account" id="email_account" value="$email_account" size="30" />
<input type="text" name="email_account" id="email_account" value="$email_account" />
<span class="desc">$T('explain-email_account')</span>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="email_pwd">$T('opt-email_pwd')</label>
<input type="password" name="email_pwd" id="email_pwd" value="$email_pwd" size="30" />
<input type="text" name="email_pwd" id="email_pwd" value="$email_pwd" />
<span class="desc">$T('explain-email_pwd')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('link-testEmail')" id="test_email" rel="$T('askTestEmail')" />
<span id="testmail-result" class="icon path darkred">&nbsp;</span>
<span id="config_err_msg" class="icon path darkred">&nbsp;</span>
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_email" rel="$T('askTestEmail')"><span class="glyphicon glyphicon-envelope"></span> $T('link-testEmail')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<!--#if $have_ncenter#-->
<div class="section">
<div class="col2">
<h3>$T('section-NC')</h3>
<table>
<tr>
<td><input type="checkbox" name="ncenter_enable" id="ncenter_enable" value="1" <!--#if int($ncenter_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="ncenter_enable"> $T('opt-ncenter_enable')</label></td>
</tr>
</table>
</div><!-- /col2 -->
<div class="col1" <!--#if int($ncenter_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
$show_notify_checkboxes('ncenter')
<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_notif"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<!--#end if#-->
<!--#if $nt#-->
<div class="section">
<div class="col2">
<h3>$T('section-AC')</h3>
<table>
<tr>
<td><input type="checkbox" name="acenter_enable" id="acenter_enable" value="1" <!--#if int($acenter_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="acenter_enable"> $T('opt-acenter_enable')</label></td>
</tr>
</table>
</div><!-- /col2 -->
<div class="col1" <!--#if int($acenter_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
$show_notify_checkboxes('acenter')
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_windows"><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 -->
<!--#end if#-->
<!--#if $have_ntfosd#-->
<div class="section">
<div class="col2">
<h3>$T('section-OSD') <a href="$helpuri$help_uri#toc4" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<table>
<tr>
<td><input type="checkbox" name="ntfosd_enable" id="ntfosd_enable" value="1" <!--#if int($ntfosd_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="ntfosd_enable"> $T('opt-ntfosd_enable')</label></td>
</tr>
</table>
</div><!-- /col2 -->
<div class="col1" <!--#if int($ntfosd_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
$show_notify_checkboxes('ntfosd')
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_osd"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<!--#end if#-->
<div class="section" id="growl">
<div class="col2">
<h3>$T('growlSettings')</h3>
<h3>$T('growlSettings') <a href="$helpuri$help_uri#toc3" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<table>
<tr>
<td><input type="checkbox" name="growl_enable" id="growl_enable" value="1" <!--#if int($growl_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="growl_enable"> $T('opt-growl_enable')</label></td>
</tr>
</table>
</div><!-- /col2 -->
<div class="col1">
<div class="col1" <!--#if int($growl_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair" >
<label class="config" for="growl_enable">$T('opt-growl_enable')</label>
<input type="checkbox" name="growl_enable" id="growl_enable" value="1" <!--#if int($growl_enable) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-growl_enable')</span>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="growl_server">$T('opt-growl_server')</label>
<input type="text" name="growl_server" id="growl_server" value="$growl_server" size="40" />
<input type="text" name="growl_server" id="growl_server" value="$growl_server" />
<span class="desc">$T('explain-growl_server')</span>
</div>
<div class="field-pair">
<label class="config" for="growl_password">$T('opt-growl_password')</label>
<input type="password" name="growl_password" id="growl_password" value="$growl_password" size="30" />
<input type="text" name="growl_password" id="growl_password" value="$growl_password" />
<span class="desc">$T('explain-growl_password')</span>
</div>
<div class="field-pair alt">
<label class="config" for="growl_prio_startup">$T($notify_texts['startup'])</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>
$show_notify_checkboxes('growl')
<div class="field-pair">
<label class="config" for="growl_prio_download">$T($notify_texts['download'])</label>
<input type="checkbox" name="growl_prio_download" id="growl_prio_download" value="1" <!--#if int($growl_prio_download) > 0 then 'checked="checked"' else ""#--> />
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_growl"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair alt">
<label class="config" 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 class="field-pair result-box">
<div class="alert"></div>
</div>
<div class="field-pair">
<label class="config" 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 alt">
<label class="config" 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" 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 alt">
<label class="config" 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" 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 alt">
<label class="config" 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" 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">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('testNotify')" id="test_growl" />
<span id="testgrowl-result" class="icon path darkred">&nbsp;</span>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('section-NC')</h3>
</div><!-- /col2 -->
<div class="col1" <!--#if $have_ncenter then '' else 'style="display:none;"'#-->">
<fieldset>
<div class="field-pair <!--#if not $have_ncenter then "disabled" else "" #-->">
<label class="config" for="ncenter_enable">$T('opt-ncenter_enable')</label>
<input type="checkbox" name="ncenter_enable" id="ncenter_enable" value="1" <!--#if int($ncenter_enable) > 0 then 'checked="checked"' else ""#--> <!--#if not $have_ncenter then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-ncenter_enable')</span>
</div>
<div class="field-pair alt">
<label class="config" for="ncenter_prio_startup">$T($notify_texts['startup'])</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" for="ncenter_prio_download">$T($notify_texts['download'])</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 alt">
<label class="config" 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" 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 alt">
<label class="config" 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" 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 alt">
<label class="config" 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" 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 alt">
<label class="config" 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" 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">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('testNotify')" id="test_notification" />
<span id="testnotice-result" class="icon path darkred">&nbsp;</span>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('section-OSD')</h3>
</div><!-- /col2 -->
<div class="col1" <!--#if $have_ntfosd then '' else 'style="display:none;"'#-->">
<fieldset>
<div class="field-pair alt <!--#if not $have_ntfosd then "disabled" else "" #-->">
<label class="config" for="ntfosd_enable">$T('opt-ntfosd_enable')</label>
<input type="checkbox" name="ntfosd_enable" id="ntfosd_enable" value="1" <!--#if int($ntfosd_enable) > 0 then 'checked="checked"' else ""#--> <!--#if not $have_ntfosd then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-ntfosd_enable')</span>
</div>
<div class="field-pair alt">
<label class="config" for="ntfosd_prio_startup">$T($notify_texts['startup'])</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" for="ntfosd_prio_download">$T($notify_texts['download'])</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 alt">
<label class="config" 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" 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 alt">
<label class="config" 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" 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 alt">
<label class="config" 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" 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 alt">
<label class="config" 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" 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">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('testNotify')" id="test_osd" />
<span id="testosd-result" class="icon path darkred">&nbsp;</span>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section" id="prowl">
<div class="col2">
<h3>$T('section-Prowl')</h3>
<table>
<tr>
<td><input type="checkbox" name="prowl_enable" id="prowl_enable" value="1" <!--#if int($prowl_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="prowl_enable"> $T('opt-prowl_enable')</label></td>
</tr>
</table>
<em>$T('explain-prowl_enable')</em>
</div><!-- /col2 -->
<div class="col1">
<div class="col1" <!--#if int($prowl_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair alt" >
<label class="config" for="prowl_enable">$T('opt-prowl_enable')</label>
<input type="checkbox" name="prowl_enable" id="prowl_enable" value="1" <!--#if int($prowl_enable) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-prowl_enable')</span>
</div>
<div class="field-pair">
<label class="config" for="prowl_apikey">$T('opt-prowl_apikey')</label>
<input type="text" name="prowl_apikey" id="prowl_apikey" value="$prowl_apikey" size="50" />
<input type="text" name="prowl_apikey" id="prowl_apikey" value="$prowl_apikey" />
<span class="desc">$T('explain-prowl_apikey')</span>
</div>
<div class="field-pair alt">
<label class="config" for="prowl_prio_startup">$T($notify_texts['startup'])</label>
<select name="prowl_prio_startup" id="prowl_prio_startup">
<option value="-3" <!--#if $prowl_prio_startup == -3 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_startup == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_startup == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_startup == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_startup == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_startup == 2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<!--#set $section_label = 'prowl'#-->
<!--#for $type in $notify_keys#-->
<div class="field-pair">
<label class="config" for="${section_label}_prio_$type">
$T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
</label>
<select name="${section_label}_prio_$type" id="${section_label}_prio_$type">
<option value="-3" <!--#if $getVar($section_label + '_prio_' + $type) == -3 then 'selected="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $getVar($section_label + '_prio_' + $type) == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $getVar($section_label + '_prio_' + $type) == -1 then 'selected="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $getVar($section_label + '_prio_' + $type) == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $getVar($section_label + '_prio_' + $type) == 1 then 'selected="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $getVar($section_label + '_prio_' + $type) == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<!--#end for#-->
<div class="field-pair">
<label class="config" for="prowl_prio_download">$T($notify_texts['download'])</label>
<select name="prowl_prio_download" id="prowl_prio_download">
<option value="-3" <!--#if $prowl_prio_download == -3 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_download == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_download == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_download == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_download == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_download == 2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair alt">
<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" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_pp == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_pp == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_pp == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_pp == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_pp == 2 then 'selected="selected" class="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" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_complete == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_complete == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_complete == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_complete == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_complete == 2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair alt">
<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" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_failed == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_failed == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_failed == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_failed == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_failed == 2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
<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>
</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" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_queue_done == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_queue_done == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_queue_done == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_queue_done == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_queue_done == 2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair alt">
<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" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_disk_full == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_disk_full == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_disk_full == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_disk_full == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_disk_full == 2 then 'selected="selected" class="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" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_warning == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_warning == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_warning == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_warning == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_warning == 2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair alt">
<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" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_error == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_error == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_error == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_error == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_error == 2 then 'selected="selected" class="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" class="selected"' else ""#--> >$T('prowl-off')</option>
<option value="-2" <!--#if $prowl_prio_other == -2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $prowl_prio_other == -1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-moderate')</option>
<option value="0" <!--#if $prowl_prio_other == 0 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $prowl_prio_other == 1 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-high')</option>
<option value="2" <!--#if $prowl_prio_other == 2 then 'selected="selected" class="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('testNotify')" id="test_prowl" />
<span id="testprowl-result" class="icon path darkred">&nbsp;</span>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section" id="pushover">
<div class="col2">
<h3>$T('section-Pushover')</h3>
<table>
<tr>
<td><input type="checkbox" name="pushover_enable" id="pushover_enable" value="1" <!--#if int($pushover_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="pushover_enable"> $T('opt-pushover_enable')</label></td>
</tr>
</table>
<em>$T('explain-pushover_enable')</em>
</div><!-- /col2 -->
<div class="col1" <!--#if int($pushover_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="pushover_token">$T('opt-pushover_token')</label>
<input type="text" name="pushover_token" id="pushover_token" value="$pushover_token" />
<span class="desc">$T('explain-pushover_token')</span>
</div>
<div class="field-pair">
<label class="config" for="pushover_userkey">$T('opt-pushover_userkey')</label>
<input type="text" name="pushover_userkey" id="pushover_userkey" value="$pushover_userkey" />
<span class="desc">$T('explain-pushover_userkey')</span>
</div>
<div class="field-pair">
<label class="config" for="pushover_device">$T('opt-pushover_device')</label>
<input type="text" name="pushover_device" id="pushover_device" value="$pushover_device" />
<span class="desc">$T('explain-pushover_device')</span>
</div>
<!--#set $section_label = 'pushover'#-->
<!--#for $type in $notify_keys#-->
<div class="field-pair">
<label class="config" for="${section_label}_prio_$type">
$T($notify_texts[$type]).replace('/', ' / ') <!--#if $type == 'download'#--> / $T('link-pause') / $T('link-resume')<!--#end if#-->
</label>
<select name="${section_label}_prio_$type" id="${section_label}_prio_$type">
<option value="-3" <!--#if $getVar($section_label + '_prio_' + $type) == -3 then 'selected="selected"' else ""#--> >$T('pushover-off')</option>
<option value="-2" <!--#if $getVar($section_label + '_prio_' + $type) == -2 then 'selected="selected"' else ""#--> >$T('prowl-very-low')</option>
<option value="-1" <!--#if $getVar($section_label + '_prio_' + $type) == -1 then 'selected="selected"' else ""#--> >$T('pushover-low')</option>
<option value="0" <!--#if $getVar($section_label + '_prio_' + $type) == 0 then 'selected="selected"' else ""#--> >$T('prowl-normal')</option>
<option value="1" <!--#if $getVar($section_label + '_prio_' + $type) == 1 then 'selected="selected"' else ""#--> >$T('pushover-high')</option>
<option value="2" <!--#if $getVar($section_label + '_prio_' + $type) == 2 then 'selected="selected"' else ""#--> >$T('prowl-emergency')</option>
</select>
</div>
<!--#end for#-->
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_pushover"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section" id="pushbullet">
<div class="col2">
<h3>$T('section-Pushbullet')</h3>
<table>
<tr>
<td><input type="checkbox" name="pushbullet_enable" id="pushbullet_enable" value="1" <!--#if int($pushbullet_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="pushbullet_enable"> $T('opt-pushbullet_enable')</label></td>
</tr>
</table>
<em>$T('explain-pushbullet_enable')</em>
</div><!-- /col2 -->
<div class="col1" <!--#if int($pushbullet_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="pushbullet_apikey">$T('opt-pushbullet_apikey')</label>
<input type="text" name="pushbullet_apikey" id="pushbullet_apikey" value="$pushbullet_apikey" />
<span class="desc">$T('explain-pushbullet_apikey')</span>
</div>
<!--#if 0#-->
<div class="field-pair">
<label class="config" for="pushbullet_device">$T('opt-pushbullet_device')</label>
<input type="text" name="pushbullet_device" id="pushbullet_device" value="$pushbullet_device" />
<span class="desc">$T('explain-pushbullet_device')</span>
</div>
<!--#end if#-->
$show_notify_checkboxes('pushbullet')
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_pushbullet"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section" id="nscript">
<div class="col2">
<h3>$T('section-NScript')</h3>
<table>
<tr>
<td><input type="checkbox" name="nscript_enable" id="nscript_enable" value="1" <!--#if int($nscript_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="nscript_enable"> $T('opt-nscript_enable')</label></td>
</tr>
</table>
<em>$T('explain-nscript_enable')</em>
</div><!-- /col2 -->
<div class="col1" <!--#if int($nscript_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="nscript_script">$T('opt-nscript_script')</label>
<select name="nscript_script">
<!--#for $sc in $scripts#-->
<option value="$sc" <!--#if $nscript_script == $sc then 'selected="selected"' else ""#-->>$Tspec($sc)</option>
<!--#end for#-->
</select>
<span class="desc">$T('explain-nscript_script')</span>
</div>
<div class="field-pair">
<label class="config" for="nscript_parameters">$T('opt-nscript_parameters')</label>
<input type="text" name="nscript_parameters" id="nscript_parameters" value="$nscript_parameters" />
<span class="desc">$T('Optional') - $T('explain-nscript_parameters')</span>
</div>
$show_notify_checkboxes('nscript')
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_nscript"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="padding alt">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
</div>
</form>
</div><!-- /colmask -->
<script>
<script type="text/javascript">
\$(document).ready(function(){
\$('#email_dir').fileBrowser({ title: 'Select $T('opt-email_dir')' });
\$('#test_email').click(function () {
if (confirm(\$('#test_email').attr('rel'))) {
var data = { mode: 'test_email', apikey: '$session', output: 'json' };
\$("#email").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: data,
beforeSend: function () {
\$('#test_email').attr("disabled", "disabled");
\$('#testmail-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_email').removeAttr("disabled");
\$('#testmail-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testmail-result').addClass("success").html('$T('smpl-emailsent')');
} else {
\$('#testmail-result').addClass("failure").html(data.error);
}
}
});
// Autocomplete and filebrowser
\$('#email_dir').typeahead().fileBrowser();
// Expand on enable
\$('.col2 input[name$="enable"]').change(function() {
if(this.checked) {
\$(this).parents('.section').find('.col1').show()
} else {
\$(this).parents('.section').find('.col1').hide()
}
});
\$('#test_notification').click(function () {
\$('form').submit()
})
/**
Testing functions
**/
function testNotification(buttonObj) {
// Confirm?
if(\$(buttonObj).attr('rel')) {
if(!confirm(\$(buttonObj).attr('rel'))) return false;
}
// Disable button and get the data
\$(buttonObj).attr("disabled", "disabled")
\$(buttonObj).find('span').toggleClass('glyphicon-comment glyphicon-refresh spin-glyphicon')
var data = { mode: buttonObj.id, apikey: '$session', output: 'json' };
\$(buttonObj).parents('.section').extractFormDataTo(data);
// Clear up the box
resultBox = \$(buttonObj).parents('.section').find('.result-box .alert');
// Get the request
\$.ajax({
type: "GET",
url: "../../tapi",
data: {mode: 'test_notif', apikey: '$session', output: 'json' },
beforeSend: function () {
\$('#test_notification').attr("disabled", "disabled");
\$('#testnotice-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_notification').removeAttr("disabled");
\$('#testnotice-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testnotice-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testnotice-result').addClass("failure").html(data.error);
}
data: data
}).then(function(data) {
// Remove disabled and make the box
\$(buttonObj).removeAttr("disabled")
\$(buttonObj).find('span').toggleClass('glyphicon-comment glyphicon-refresh spin-glyphicon')
resultBox.removeClass('alert-success alert-danger').show()
if(data.status) {
resultBox.addClass('alert-success')
resultBox.text('$T('smpl-notesent')')
resultBox.prepend('<span class="glyphicon glyphicon-ok-sign"></span> ')
} else {
resultBox.addClass('alert-danger')
resultBox.text(data.error)
resultBox.prepend('<span class="glyphicon glyphicon-exclamation-sign"></span> ')
}
});
});
\$('#test_prowl').click(function () {
var data = { mode: 'test_prowl', apikey: '$session', output: 'json' };
\$("#prowl").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: data,
beforeSend: function () {
\$('#test_prowl').attr("disabled", "disabled");
\$('#testprowl-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_prowl').removeAttr("disabled");
\$('#testprowl-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testprowl-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testprowl-result').addClass("failure").html(data.error);
}
}
});
});
\$('#test_growl').click(function () {
var data = { mode: 'test_growl', apikey: '$session', output: 'json' };
\$("#growl").extractFormDataTo(data);
\$.ajax({
type: "GET",
url: "../../tapi",
data: data,
beforeSend: function () {
\$('#test_growl').attr("disabled", "disabled");
\$('#testgrowl-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_growl').removeAttr("disabled");
\$('#testgrowl-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testgrowl-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testgrowl-result').addClass("failure").html(data.error);
}
}
});
});
\$('#test_osd').click(function () {
\$.ajax({
type: "GET",
url: "../../tapi",
data: {mode: 'test_osd', apikey: '$session', output: 'json' },
beforeSend: function () {
\$('#test_osd').attr("disabled", "disabled");
\$('#testosd-result').removeClass("success failure").addClass("loading").html('$T('post-Verifying')');
},
complete: function () {
\$('#test_osdl').removeAttr("disabled");
\$('#testosd-result').removeClass("loading");
},
success: function (data) {
if (data.status == true) {
\$('#testosd-result').addClass("success").html('$T('smpl-notesent')');
} else {
\$('#testosd-result').addClass("failure").html(data.error);
}
}
});
});
})
}
\$('#test_email, #test_notif, #test_windows, #test_pushbullet, #test_pushover, #test_prowl, #test_growl, #test_osd, #test_nscript').click(function () {
testNotification(this)
})
});
</script>

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
<!--#set global $pane="Scheduling"#-->
<!--#set global $help_uri="configure-scheduling-0-8"#-->
<!--#set global $help_uri="configuration/2.1/scheduling"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<%
@@ -15,13 +15,13 @@ else:
<div class="colmask">
<div class="section">
<div class="col2">
<h3>$T('addSchedule')</h3>
<h3>$T('addSchedule') <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 -->
<form action="addSchedule" method="post">
<form action="addSchedule" method="post" autocomplete="off">
<input type="hidden" id="session" name="session" value="$session" />
<div class="col1">
<fieldset>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="hour">$T('hour').capitalize() : $T('minute').capitalize()</label>
<select name="hour" id="hour">
<!--#for $i in range(24)#-->
@@ -34,36 +34,45 @@ else:
</select>
</div>
<div class="field-pair">
<label class="config" for="daysofweek">$T('sch-frequency')</label>
<label class="config">$T('sch-frequency')</label>
<div class="checkbox-days">
<p><input type="checkbox" name="daysofweek" value="1"><label>$T('monday')</label></p>
<p><input type="checkbox" name="daysofweek" value="2"><label>$T('tuesday')</label></p>
<p><input type="checkbox" name="daysofweek" value="3"><label>$T('wednesday')</label></p>
<p><input type="checkbox" name="daysofweek" value="4"><label>$T('thursday')</label></p>
<p><input type="checkbox" name="daysofweek" value="5"><label>$T('friday')</label></p>
<p><input type="checkbox" name="daysofweek" value="6"><label>$T('saturday')</label></p>
<p><input type="checkbox" name="daysofweek" value="7"><label>$T('sunday')</label></p>
<label><input type="checkbox" name="daysofweek" value="1"/> $T('monday')</label><br />
<label><input type="checkbox" name="daysofweek" value="2"/> $T('tuesday')</label><br />
<label><input type="checkbox" name="daysofweek" value="3"/> $T('wednesday')</label><br />
<label><input type="checkbox" name="daysofweek" value="4"/> $T('thursday')</label><br />
<label><input type="checkbox" name="daysofweek" value="5"/> $T('friday')</label><br />
<label><input type="checkbox" name="daysofweek" value="6"/> $T('saturday')</label><br />
<label><input type="checkbox" name="daysofweek" value="7"/> $T('sunday')</label>
</div>
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="action">$T('sch-action')</label>
<select name="action" id="action">
<!--#for $action in $actions#-->
<option value="$action">$actions_lng[$action]</option>
<!--#end for#-->
<optgroup label="$T('sch-action')">
<!--#for $action in $actions#-->
<option value="$action" data-action="" data-noarg="<!--#if $action is 'speedlimit' then 0 else 1#-->">$actions_lng[$action]</option>
<!--#end for#-->
</optgroup>
<optgroup label="$T('cmenu-servers')">
<!--#for $server in $actions_servers.keys()#-->
<option value="$server" data-action="1" data-noarg="1">$T('sch-enable_server') "$actions_servers[$server]"</option>
<option value="$server" data-action="0" data-noarg="1">$T('sch-disable_server') "$actions_servers[$server]"</option>
<!--#end for#-->
</optgroup>
</select>
</div>
<div class="field-pair">
<div class="field-pair" id="hidden_arguments" style="display: none">
<label class="config" for="arguments">$T('sch-arguments')</label>
<input type="text" name="arguments" id="arguments" value="" size="20" />
<input type="text" name="arguments" id="arguments" class="select_width" />
</div>
<div class="field-pair">
<input type="submit" value="$T('button-addSchedule')" />
<button class="btn btn-default"><span class="glyphicon glyphicon-plus"></span> $T('button-addSchedule')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</form>
</div><!-- /section -->
</form>
<div class="section">
<div class="col2">
<h3>$T('currentSchedules')</h3>
@@ -71,21 +80,20 @@ else:
<div class="col1">
<fieldset>
<!--#if $schedlines#-->
<!--#set $schednum = 0#-->
<!--#set $odd = True#-->
<!--#for $line in $schedlines#-->
<!--#for $schednum, $line in enumerate($schedlines)#-->
<!--#set $odd = not $odd#-->
<form action="delSchedule" method="post">
<input type="hidden" name="session" id="session" value="$session">
<input type="hidden" name="session" value="$session"/>
<input type="hidden" name="line" id="line" value="$line"/>
<div class="field-pair infoTableSeperator<!--#if $odd then "" else " alt"#-->">
<input class="float-left" type="submit" value="$T('button-x')" />
<div class="field-pair infoTableSeperator <!--#if $odd then "" else " alt"#-->">
<input type="checkbox" name="schedenabled" value="$line" <!--#if int($taskinfo[$schednum][5]) > 0 then 'checked="checked"' else ""#-->>
<button class="btn btn-default float-left"><span class="glyphicon glyphicon-trash"></span></button>
<div class="scheduleEntry">
<span class="time">$taskinfo[$schednum][1]:$taskinfo[$schednum][2]</span><span class="frequency">$taskinfo[$schednum][3]</span> <span class="darkred">$taskinfo[$schednum][4]</span>
</div>
</div>
</form>
<!--#set $schednum = $schednum+1#-->
<!--#end for#-->
<!--#else#-->
<div class="field-pair">
@@ -96,5 +104,39 @@ else:
</div><!-- /col1 -->
</div><!-- /section -->
</div><!-- /colmask -->
<script type="text/javascript">
\$('#action').on('change', function() {
// Set the action
\$('#arguments').val((\$(this).find('option:selected').data('action')))
// Is it speedlimit?
if(\$(this).find('option:selected').val() == 'speedlimit') {
\$('#hidden_arguments').show()
\$('#hidden_arguments input').attr('placeholder', 'Bytes/s, "1M" = 1 MB/s, "500K" = 500 KB/s')
} else {
\$('#hidden_arguments').hide()
\$('#hidden_arguments input').attr('placeholder', '')
}
/* Arguments - since we only have speedlimit with arguments, disabled for now
if(\$(this).find('option:selected').data('noarg')) {
\$('#hidden_arguments').hide()
} else {
\$('#hidden_arguments').show()
}*/
})
\$('[name="schedenabled"]').click(function() {
\$.ajax({
type: "POST",
url: "toggleSchedule",
data: {line: \$(this).val(), session: "$session" }
}).done(function() {
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
location.reload();
});
});
</script>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

View File

@@ -1,196 +1,245 @@
<!--#set global $pane="Servers"#-->
<!--#set global $help_uri="configure-servers-0-8"#-->
<!--#set global $help_uri="configuration/2.1/servers"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<form action="addServer" method="post" novalidate>
<form action="addServer" method="post" autocomplete="off" onsubmit="removeObfuscation();" novalidate>
<input type="hidden" name="session" value="$session" />
<div id="addServer">
<div class="padding alt">
<input type="button" value="$T('button-addServer')" id="addServerButton" />
<button type="button" class="btn btn-default" id="addServerButton"><span class="glyphicon glyphicon-plus"></span> $T('button-addServer')</button>
</div>
</div>
<div class="section" id="addServerContent" style="display: none;">
<div class="col2">
<h3>$T('addServer')</h3>
<h3>$T('addServer') <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 alt">
<div class="field-pair">
<label class="config" for="enable">$T('srv-enable')</label>
<input type="checkbox" name="enable" id="enable" value="1" checked="checked" />
<span class="desc">$T('srv-enable')</span>
</div>
<div class="field-pair">
<label class="config" for="host">$T('srv-host')</label>
<input type="text" name="host" id="host" size="40" />
</div>
<div class="field-pair alt">
<label class="config" for="port">$T('srv-port')</label>
<input type="text" name="port" id="port" size="8" />
<input type="text" name="host" id="host" />
</div>
<div class="field-pair">
<label class="config" for="username">$T('srv-username')</label>
<input type="text" name="username" id="username" size="30" />
<label class="config" for="port">$T('srv-port')</label>
<input type="number" name="port" id="port" size="8" value="119" />
</div>
<div class="field-pair alt">
<label class="config" for="password">$T('srv-password')</label>
<input type="password" name="password" id="password" size="30" />
<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>
<!-- Tricks to avoid browser auto-fill, fixed on-submit with javascript -->
<div class="field-pair">
<label class="config" for="${pid}_00">$T('srv-username')</label>
<input type="text" name="${pid}_00" id="${pid}_00" data-hide="username" />
</div>
<div class="field-pair">
<label class="config" for="${pid}_01">$T('srv-password')</label>
<input type="text" name="${pid}_01" id="${pid}_01" data-hide="password" />
</div>
<div class="field-pair">
<label class="config" for="connections">$T('srv-connections')</label>
<input type="number" name="connections" id="connections" size="8" min="0" max="100" />
<input type="number" name="connections" id="connections" min="0" max="100" value="8" />
</div>
<div class="field-pair alt">
<div class="field-pair">
<label class="config" for="priority">$T('srv-priority')</label>
<input type="number" name="priority" id="priority" size="8" min="0" max="100" /> $T('explain-svrprio')
<input type="number" name="priority" id="priority" min="0" max="100" /> <i>$T('explain-svrprio')</i>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="retention">$T('srv-retention')</label>
<input type="number" name="retention" id="retention" size="8" min="0" /> <i>$T('days')</i>
<input type="number" name="retention" id="retention" min="0" /> <i>$T('days')</i>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="timeout">$T('srv-timeout')</label>
<input type="number" name="timeout" id="timeout" size="8" min="30" /> <i>$T('seconds')</i>
<input type="number" name="timeout" id="timeout" min="30" /> <i>$T('seconds')</i>
</div>
<div class="field-pair alt" <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="ssl">$T('srv-ssl')</label>
<input type="checkbox" name="ssl" id="ssl" value="1" <!--#if int($have_ssl) == 0 then "disabled=\"disabled\"" else ""#--> />
<span class="desc">$T('srv-ssl')</span>
<div class="field-pair <!--#if int($have_ssl_context) == 0 then "disabled" else ""#--> advanced-settings">
<label class="config" for="ssl_verify">$T('opt-ssl_verify')</label>
<select name="ssl_verify" id="ssl_verify" <!--#if int($have_ssl_context) == 0 then "disabled=\"disabled\"" else ""#-->>
<option value="2" selected>$T('ssl_verify-strict')</option>
<option value="1">$T('ssl_verify-normal')</option>
<option value="0">$T('ssl_verify-disabled')</option>
</select>
<span class="desc">$T('explain-ssl_verify').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="ssl_type">$T('srv-ssl_type')</label>
<!--#if int($have_ssl) == 1#-->
<select name="ssl_type" id="ssl_type">
<option value="t1" selected="selected" class="selected">TLS1</option>
<option value="v23">V23</option>
<option value="v2" >V2</option>
<option value="v3" >V3</option>
</select>
<!--#end if#-->
<span class="desc">$T('srv-explain-ssl_type')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="send_group">$T('srv-send_group')</label>
<input type="checkbox" name="send_group" id="send_group" value="1" />
<span class="desc">$T('srv-explain-send_group')</span>
</div>
<div class="field-pair alt">
<div class="field-pair advanced-settings">
<label class="config" for="optional">$T('srv-optional')</label>
<input type="checkbox" name="optional" id="optional" value="1" />
<span class="desc">$T('srv-optional')</span>
<span class="desc">$T('explain-optional')</span>
</div>
<div class="field-pair 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 advanced-settings">
<label class="config" for="displayname">$T('srv-displayname')</label>
<input type="text" name="displayname" id="displayname" />
</div>
<div class="field-pair advanced-settings">
<label class="config" for="notes">$T('srv-notes')</label>
<textarea name="notes" id="notes" rows="3" cols="50"></textarea>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-addServer')" />
<input type="button" value="$T('button-testServer')" class="testServer" />
<button class="btn btn-default"><span class="glyphicon glyphicon-plus"></span> $T('button-addServer')</button>
<button class="btn btn-default advancedButton"><span class="glyphicon glyphicon-cog"></span> $T('button-advanced')</button>
<button class="btn btn-default testServer" type="button"><span class="glyphicon glyphicon-sort"></span> $T('button-testServer')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
</form>
</form>
<!--#set $slist = $servers.keys()#-->
<!--#$slist.sort()#-->
<!--#set $cur = 0#-->
<!--#for $server in $slist#-->
<!--#set $cur = $cur + 1#-->
<form action="saveServer" method="post" id="fullform" novalidate>
<!--#set $prio_colors = ["#59cc33", "#3366cc","#7f33cc", "#cc33a6", "#cc3333"] #-->
<!--#set $cur_prio_color = -1 #-->
<!--#set $last_prio = -1 #-->
<!--#for $cur, $server in enumerate($servers) #-->
<form action="saveServer" method="post" class="fullform" autocomplete="off" novalidate>
<input type="hidden" name="session" value="$session" />
<input type="hidden" name="server" value="$server" />
<input type="hidden" name="server" value="$server['name']" />
<div class="section">
<div class="col2">
<h3>$servers[$server]['name']</h3>
<p>
<div class="section <!--#if int($server['enable']) == 0 then 'server-disabled' else ""#-->">
<div class="col2 <!--#if int($server['enable']) == 0 then 'server-disabled' else ""#-->">
<h3>$server['displayname'] <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<!--#if int($server['enable']) != 0 #-->
<!--#if $last_prio != $server['priority'] and $cur_prio_color+1 < len($prio_colors) #-->
<!--#set $cur_prio_color = $cur_prio_color+1 #-->
<!--#set $last_prio = $server['priority'] #-->
<!--#end if#-->
<span class="label label-primary" style="background-color: $prio_colors[$cur_prio_color]">$server['priority']</span>
<span class="label label-primary" style="background-color: $prio_colors[$cur_prio_color]">$T('srv-priority'):</span>
<!--#end if#-->
<table><tr>
<td><input type="checkbox" class="toggleServerCheckbox" name="q_enable" value="1" <!--#if int($servers[$server]['enable']) != 0 then 'checked="checked"' else ""#--> rel="$server" /></td>
<td>&nbsp;$T('enabled')</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>
<td><label for="enable_$cur">$T('enabled')</label></td>
</tr></table>
</p>
<input type="button" value="$T('button-clrServer')" class="clrServer" />
<input type="button" value="$T('showDetails')" class="showserver" />
<button type="button" class="btn btn-default showserver"><span class="glyphicon glyphicon-pencil"></span> $T('showDetails')</button>
<button type="button" class="btn btn-default clrServer"><span class="glyphicon glyphicon-remove"></span> $T('button-clrServer')</button>
</div><!-- /col2 -->
<div class="col1" style="display:none;">
<div class="col1" style="display:none;">
<input type="hidden" name="enable" id="enable$cur" value="$int($server['enable'])" />
<fieldset>
<div class="field-pair alt">
<label class="config" for="enable$cur">$T('srv-enable')</label>
<input type="checkbox" name="enable" id="enable$cur" value="1" <!--#if int($servers[$server]['enable']) != 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('srv-enable')</span>
</div>
<div class="field-pair">
<label class="config" for="host$cur">$T('srv-host')</label>
<input type="text" name="host" id="host$cur" value="$servers[$server]['host']" size="40" />
</div>
<div class="field-pair alt">
<label class="config" for="port$cur">$T('srv-port')</label>
<input type="text" name="port" id="port$cur" value="$servers[$server]['port']" size="8" />
<input type="text" name="host" id="host$cur" value="$server['host']" />
</div>
<div class="field-pair">
<label class="config" for="username$cur">$T('srv-username')</label>
<input type="text" name="username" id="username$cur" value="$servers[$server]['username']" size="30" />
<label class="config" for="port$cur">$T('srv-port')</label>
<input type="number" name="port" id="port$cur" value="$server['port']" size="8" />
</div>
<div class="field-pair alt">
<label class="config" for="password$cur">$T('srv-password')</label>
<input type="password" name="password" id="password$cur" value="$servers[$server]['password']" size="30" />
<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 -->
<div class="field-pair">
<label class="config" for="${pid}_${cur}0">$T('srv-username')</label>
<input type="text" name="${pid}_${cur}0" id="${pid}_${cur}0" value="$server['username']" data-hide="username" />
</div>
<div class="field-pair">
<label class="config" for="${pid}_${cur}1">$T('srv-password')</label>
<input type="text" name="${pid}_${cur}1" id="${pid}_${cur}1" value="$server['password']" data-hide="password" />
</div>
<div class="field-pair">
<label class="config" for="connections$cur">$T('srv-connections')</label>
<input type="number" name="connections" id="connections$cur" value="$servers[$server]['connections']" size="8" min="0" max="100" />
<input type="number" name="connections" id="connections$cur" value="$server['connections']" min="0" max="100" />
</div>
<div class="field-pair">
<label class="config" for="priority$cur">$T('srv-priority')</label>
<input type="number" name="priority" id="priority$cur" value="$servers[$server]['priority']" size="8" min="0" max="100" /> $T('explain-svrprio')
<input type="number" name="priority" id="priority$cur" value="$server['priority']" min="0" max="100" /> <i>$T('explain-svrprio')</i>
</div>
<div class="field-pair alt">
<div class="field-pair advanced-settings">
<label class="config" for="retention$cur">$T('srv-retention')</label>
<input type="number" name="retention" id="retention$cur" value="$servers[$server]['retention']" size="8" 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 class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="timeout$cur">$T('srv-timeout')</label>
<input type="number" name="timeout" id="timeout$cur" value="$servers[$server]['timeout']" size="8" 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 class="field-pair alt <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="ssl$cur">$T('srv-ssl')</label>
<input type="checkbox" name="ssl" id="ssl$cur" value="1" <!--#if int($servers[$server]['ssl']) != 0 and int($have_ssl) == 1 then 'checked="checked"' else ""#--> <!--#if int($have_ssl) == 0 then "disabled=\"disabled\"" else ""#--> />
<span class="desc">$T('srv-ssl')</span>
<div class="field-pair <!--#if int($have_ssl_context) == 0 then "disabled" else ""#--> advanced-settings">
<label class="config" for="ssl_verify$cur">$T('opt-ssl_verify')</label>
<select name="ssl_verify" id="ssl_verify$cur" <!--#if int($have_ssl_context) == 0 then "disabled=\"disabled\"" else ""#-->>
<option value="2" <!--#if $server['ssl_verify'] == 2 then 'selected="selected"' else ""#--> >$T('ssl_verify-strict')</option>
<option value="1" <!--#if $server['ssl_verify'] == 1 then 'selected="selected"' else ""#--> >$T('ssl_verify-normal')</option>
<option value="0" <!--#if $server['ssl_verify'] == 0 then 'selected="selected"' else ""#--> >$T('ssl_verify-disabled')</option>
</select>
<span class="desc">$T('explain-ssl_verify').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair <!--#if int($have_ssl) == 0 then "disabled" else ""#-->">
<label class="config" for="ssl_type$cur">$T('srv-ssl_type')</label>
<!--#if int($have_ssl) == 1#-->
<select name="ssl_type" id="ssl_type$cur">
<option value="t1" <!--#if $servers[$server]['ssl_type'] == "t1" then 'selected="selected" class="selected"' else ""#--> >TLS1</option>
<option value="v23" <!--#if $servers[$server]['ssl_type'] == "v23" then 'selected="selected" class="selected"' else ""#--> >V23</option>
<option value="v2" <!--#if $servers[$server]['ssl_type'] == "v2" then 'selected="selected" class="selected"' else ""#--> >V2</option>
<option value="v3" <!--#if $servers[$server]['ssl_type'] == "v3" then 'selected="selected" class="selected"' else ""#--> >V3</option>
</select>
<!--#end if#-->
<span class="desc">$T('srv-explain-ssl_type')</span>
</div>
<div class="field-pair">
<div class="field-pair advanced-settings">
<label class="config" for="optional$cur">$T('srv-optional')</label>
<input type="checkbox" name="optional" id="optional$cur" value="1" <!--#if int($servers[$server]['optional']) != 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('srv-optional')</span>
<input type="checkbox" name="optional" id="optional$cur" value="1" <!--#if int($server['optional']) != 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-optional')</span>
</div>
<div class="field-pair alt">
<div class="field-pair advanced-settings">
<label class="config" for="send_group$cur">$T('srv-send_group')</label>
<input type="checkbox" name="send_group" id="send_group$cur" value="1" <!--#if int($servers[$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>
</div>
<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 advanced-settings">
<label class="config" for="displayname$cur">$T('srv-displayname')</label>
<input type="text" name="displayname" id="displayname$cur" value="$server['displayname']" />
</div>
<div class="field-pair advanced-settings">
<label class="config" for="notes$cur">$T('srv-notes')</label>
<textarea name="notes" id="notes$cur" rows="3" cols="50">$server['notes']</textarea>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-testServer')" class="testServer" />
<input type="button" value="$T('button-delServer')" class="delServer" />
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default advancedButton"><span class="glyphicon glyphicon-cog"></span> $T('button-advanced')</button>
<button class="btn btn-default testServer" type="button"><span class="glyphicon glyphicon-sort"></span> $T('button-testServer')</button>
<button class="btn btn-default delServer"><span class="glyphicon glyphicon-trash"></span> $T('button-delServer')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div><!-- /col1 -->
<div class="col2" style="display:block;">
<!--#if 'amounts' in $servers[$server]#-->
<!--#if 'amounts' in $server#-->
<b>$T('srv-bandwidth'):</b><br/>
$T('total'): $(servers[$server]['amounts'][0])B<br/>
$T('today'): $(servers[$server]['amounts'][3])B<br/>
$T('thisWeek'): $(servers[$server]['amounts'][2])B<br/>
$T('thisMonth'): $(servers[$server]['amounts'][1])B
$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><!-- /section -->
@@ -199,9 +248,50 @@
</div><!-- /colmask -->
<script>
<script type="text/javascript">
\$(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()
})
})
/**
Message on no Default category selected
**/
function checkServerCats() {
// Now we check all of them
var hasDefault = false;
// Only check the active servers, not the add-server one
\$('.section:not(#addServerContent) select[name="categories"]').each(function() {
// See if this server is enabled
if(!\$(this).parents('.section').find('.col2').hasClass('server-disabled') ) {
// Is there Default?
if(\$(this).val() && \$(this).val().indexOf('Default') > -1) {
// Hide
\$('.alert-no-category').hide()
hasDefault = true
// All good!
return true
}
}
})
// We found nothing.. Let's show a warning
if(!hasDefault) \$('.alert-no-category').show()
}
\$('select[name="categories"]').on('change', checkServerCats)
checkServerCats()
/**
Click events
**/
\$('.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')") {
@@ -210,40 +300,95 @@
\$(this).attr("value", "$T('showDetails')");
}
});
\$('#addServerButton').click(function(){
\$('#addServer').hide();
\$('#addServerContent').show();
});
\$('.testServer').click(function(event){
\$(this).attr("disabled", "disabled")
\$.ajax({
type: "POST",
url: "../../tapi",
data: "mode=config&amp;name=test_server&amp;" + \$(this).parents('form:first').serialize() + "&amp;apikey=" + \$('#apikey').val(),
success: function(msg){
alert(msg);
\$(event.target).removeAttr("disabled")
\$('[name="ssl"]').click(function() {
// Use CSS transitions to do some highlighting
var portBox = \$(this).parent().parent().find('[name="port"]')
if(this.checked) {
// Enabled SSL change port when not already a custom port
if(portBox.val() == '119') {
portBox.val('563')
portBox.addClass('port-highlight')
}
} else {
// Remove SSL port
if(portBox.val() == '563') {
portBox.val('119')
portBox.addClass('port-highlight')
}
}
});
});
\$('.delServer').click(function(){
if( confirm("$T('Plush-confirm')") )
\$(this).parents('form:first').attr('action','delServer').submit();
return false;
});
\$('.clrServer').click(function(){
if( confirm("$T('Plush-confirm')") )
\$(this).parents('form:first').attr('action','clrServer').submit();
return false;
});
\$('.toggleServerCheckbox').click(function(){
var whichServer = \$(this).attr("rel");
setTimeout(function() { portBox.removeClass('port-highlight') }, 2000)
})
\$('.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: "toggleServer",
data: {server: whichServer, session: "$session" }
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() {
location.reload();
// Let us leave!
formWasSubmitted = true;
formHasChanged = false;
setTimeout(function() { location.reload(); }, 100)
});
});
});

View File

@@ -1,436 +1,433 @@
<!--#set global $pane="Sorting"#-->
<!--#set global $help_uri="configure-sorting-0-7"#-->
<!--#set global $help_uri="configuration/2.1/sorting"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<form action="saveSorting" method="post" name="fullform" id="fullform">
<input type="hidden" id="session" name="session" value="$session" />
<input id="complete_dir" type="hidden" value="$complete_dir" />
<div class="section">
<div class="col2">
<h3>$T('seriesSorting')</h3>
<p>
<b>$T('affectedCat')</b><br/>
<select name="tv_cat" multiple="multiple" class="multiple_cats">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $ct in $tv_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
</p>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
</div>
<div class="field-pair alt">
<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 ""#--> />
</div>
<div class="field-pair">
<label class="config" for="tvfoldername">$T('sortString')</label>
<input type="text" id="tvfoldername" name="tv_sort_string" value="$tv_sort_string" size="50" />
<input type="button" value="$T('button-clear')" class="clearBtn" />
</div>
<div class="field-pair alt">
<label class="config">$T('presetSort')</label>
<div class="presets float-left">
<input type="button" onclick="tvSet('%sn/Season %s/%sn - %sx%0e - %en.%ext')" value="$T('button-Season1x05')" />
<input type="button" onclick="tvSet('%sn/Season %s/%sn - S%0sE%0e - %en.%ext')" value="$T('button-SeasonS01E05')" /><br/>
<input type="button" onclick="tvSet('%sn/%sx%0e - %en/%sn - %sx%0e - %en.%ext')" value="$T('button-Ep1x05')" />
<input type="button" onclick="tvSet('%sn/S%0sE%0e - %en/%sn - S%0sE%0e - %en.%ext')" value="$T('button-EpS01E05')" />
</div>
</div>
<div id="previewtv" class="example">
<form action="saveSorting" method="post" name="fullform" class="fullform" autocomplete="off">
<input type="hidden" id="session" name="session" value="$session" />
<input id="complete_dir" type="hidden" value="$complete_dir" />
<div class="section">
<div class="col2">
<h3>$T('seriesSorting') <a href="$helpuri$help_uri#toc0" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p>
<b>$T('affectedCat')</b><br/>
<select name="tv_cat" multiple="multiple" class="multiple_cats">
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct in $tv_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
</p>
</div>
<!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<label class="config" for="tvsamplename">Test Data</label>
<input type="text" id="tvsamplename" name="tvsamplename" placeholder="$T('show-name') S01E05 - $T('ep-name') [DTS]" size="50" />
<input type="button" value="$T('button-clear')" class="clearBtn" />
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
</div>
<div class="field-pair">
<label class="config">$T('sortResult')</label>
<span class="desc path" id="previewtv-result">&nbsp;</span>
</div>
</div>
<div class="field-pair">
<label class="config">$T('sort-legenda')</label>
<input type="button" value="$T('sort-legenda')" onclick="jQuery(this).hide(); jQuery('#Key1').show();" />
<table id="Key1" class="Key">
<thead>
<tr>
<th class="align-right">$T('sort-meaning')</th>
<th>$T('sort-pattern')</th>
<th>$T('sort-result')</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-right"><b>$T('show-name'):</b></td>
<td>%sn</td>
<td>$T('show-sp-name')</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%s.n</td>
<td>$T('show-dot-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%s_n</td>
<td>$T('show-us-name')</td>
</tr>
<tr class="even">
<td class="align-right"><b>$T('show-name'):</b></td>
<td>%sN</td>
<td>$T('show-sp-name') ($T('case-adjusted'))</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%s.N</td>
<td>$T('show-dot-name') ($T('case-adjusted'))</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%s_N</td>
<td>$T('show-us-name') ($T('case-adjusted'))</td>
</tr>
<tr>
<td class="align-right"><b>$T('show-seasonNum'):</b></td>
<td>%s</td>
<td>1</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%0s</td>
<td>01</td>
</tr>
<tr>
<td class="align-right"><b>$T('show-epNum'):</b></td>
<td>%e</td>
<td>5</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%0e</td>
<td>05</td>
</tr>
<tr>
<td class="align-right"><b>$T('ep-name'):</b></td>
<td>%en</td>
<td>$T('ep-sp-name')</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%e.n</td>
<td>$T('ep-dot-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%e_n</td>
<td>$T('ep-us-name')</td>
</tr>
<tr class="even">
<td class="align-right"><b>$T('fileExt'):</b></td>
<td>%ext</td>
<td>avi</td>
</tr>
<tr>
<td class="align-right"><b>$T('orgFilename'):</b></td>
<td>%fn</td>
<td>$T("sort-File")</td>
</tr>
<tr class="even">
<td class="align-right"><b>$T('orgDirname'):</b></td>
<td>%dn</td>
<td>$T("sort-Folder")</td>
</tr>
<tr>
<td class="align-right"><b>$T('lowercase'):</b></td>
<td>{$T('TEXT')}</td>
<td>$T('text')</td>
</tr>
</tbody>
</table>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('genericSort')</h3>
<p>
<b>$T('affectedCat')</b><br/>
<select name="movie_cat" multiple="multiple" class="multiple_cats">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $ct in $movie_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
</p>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
</div>
<div class="field-pair alt">
<label class="config wide" for="enable_movie_sorting">$T('opt-movieSort')</label>
<input type="checkbox" name="enable_movie_sorting" id="enable_movie_sorting" value="1" <!--#if int($enable_movie_sorting) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="movie_extra_folder">$T('opt-movieExtra')</label>
<input type="checkbox" name="movie_extra_folder" id="movie_extra_folder" value="1" <!--#if int($movie_extra_folder) > 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair alt">
<label class="config" for="moviefoldername">$T('sortString')</label>
<input type="text" name="movie_sort_string" id="moviefoldername" value="$movie_sort_string" size="50" />
<input type="button" value="$T('button-clear')" class="clearBtn" />
</div>
<div class="field-pair">
<label class="config" for="movieextra">$T('multiPartLabel')</label>
<input type="text" name="movie_sort_extra" id="movieextra" value="$movie_sort_extra" size="50" />
<input type="button" value="$T('button-clear')" class="clearBtn" />
</div>
<div class="field-pair alt">
<label class="config">$T('presetSort')</label>
<div class="presets float-left">
<input type="button" onclick="movieSet('%title (%y)/%title (%y).%ext',' CD%1');movieExtraFolder(false)" value="$T('button-inFolders')" />
<input type="button" onclick="movieSet('%title (%y).%ext',' CD%1');movieExtraFolder(true)" value="$T('button-noFolders')" />
<input type="button" onclick="movieSet('%0decade/%title (%y).%ext',' CD%1');movieExtraFolder(true)" value="Decades 1" />
</div>
</div>
<div id="previewmovie" class="example">
<div class="field-pair">
<label class="config" for="moviesamplename">Test Data</label>
<input type="text" id="moviesamplename" name="moviesamplename" placeholder="$T('movie-sp-name') (2009)" size="50" />
<input type="button" value="$T('button-clear')" class="clearBtn" />
<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 ""#--> />
</div>
<div class="field-pair">
<label class="config">$T('sortResult')</label>
<span class="desc path" id="previewmovie-result">&nbsp;</span>
</div>
</div>
<div class="field-pair">
<label class="config">$T('sort-legenda')</label>
<input type="button" value="$T('sort-legenda')" onclick="jQuery(this).hide(); jQuery('#Key2').show();" />
<table id="Key2" class="Key">
<thead>
<tr>
<th class="align-right">$T('sort-meaning')</th>
<th>$T('sort-pattern')</th>
<th>$T('sort-result')</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-right"><b>$T('sort-title'):</b></td>
<td>%title</td>
<td>$T('movie-sp-name')</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%.title</td>
<td>$T('movie-dot-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%_title</td>
<td>$T('movie-us-name')</td>
</tr>
<tr class="even">
<td class="align-right"><b>$T('year'):</b></td>
<td>%y</td>
<td>2009</td>
</tr>
<tr>
<td class="align-right"><b>$T('extension'):</b></td>
<td>%ext</td>
<td>avi</td>
</tr>
<tr class="even">
<td class="align-right"><b>$T('decade'):</b></td>
<td>%decade</td>
<td>00</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%0decade</td>
<td>2000</td>
</tr>
<tr class="even">
<td class="align-right"><b>$T('orgFilename'):</b></td>
<td>%fn</td>
<td>$T('sort-File')</td>
</tr>
<tr>
<td class="align-right"><b>$T('orgDirname'):</b></td>
<td>%dn</td>
<td>$T("sort-Folder")</td>
</tr>
<tr class="even">
<td class="align-right"><b>$T('lowercase'):</b></td>
<td>{$T('TEXT')}</td>
<td>$T('text')</td>
</tr>
</tbody>
<tbody>
<tr>
<th class="align-right"><b>$T('multiPartLabel')</b></th>
<th>$T('sort-pattern')</th>
<th>$T('sort-result')</th>
</tr>
</tbody>
<tbody>
<tr>
<td class="align-right"><b>$T('partNumber'):</b></td>
<td>%1</td>
<td>1</td>
</tr>
</tbody>
</table>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('dateSorting')</h3>
<p>
<b>$T('affectedCat')</b><br/>
<select name="date_cat" multiple="multiple" class="multiple_cats">
<!--#for $ct in $cat_list#-->
<option value="$ct" <!--#if $ct in $date_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
</p>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
</div>
<div class="field-pair alt">
<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 ""#--> />
</div>
<div class="field-pair">
<label class="config" for="datefoldername">$T('sortString')</label>
<input type="text" name="date_sort_string" id="datefoldername" value="$date_sort_string" size="50" />
<input type="button" value="$T('button-clear')" class="clearBtn" />
</div>
<div class="field-pair alt">
<label class="config">$T('presetSort')</label>
<div class="presets float-left">
<input type="button" onclick="dateSet('%t/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-ShowNameF')" />
<input type="button" onclick="dateSet('%y-%0m/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-YMF')" />
<input type="button" onclick="dateSet('%y-%0m-%0d/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-DailyF')" />
</div>
</div>
<div id="previewdate" class="example">
<div class="field-pair">
<label class="config" for="datesamplename">Test Data</label>
<input type="text" id="datesamplename" name="datesamplename" placeholder="$T('show-name') 2009-01-02" size="50" />
<input type="button" value="$T('button-clear')" class="clearBtn" />
<label class="config" for="tvfoldername">$T('sortString')</label>
<input type="text" id="tvfoldername" name="tv_sort_string" value="$tv_sort_string" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('sortResult')</label>
<span class="desc path" id="previewdate-result">&nbsp;</span>
<label class="config">$T('presetSort')</label>
<div class="presets float-left">
<input type="button" class="btn btn-default" onclick="tvSet('%sn/Season %s/%sn - %sx%0e - %en.%ext')" value="$T('button-Season1x05')" />
<input type="button" class="btn btn-default" onclick="tvSet('%sn/Season %s/%sn - S%0sE%0e - %en.%ext')" value="$T('button-SeasonS01E05')" />
<br/>
<input type="button" class="btn btn-default" onclick="tvSet('%sn/%sx%0e - %en/%sn - %sx%0e - %en.%ext')" value="$T('button-Ep1x05')" />
<input type="button" class="btn btn-default" onclick="tvSet('%sn/S%0sE%0e - %en/%sn - S%0sE%0e - %en.%ext')" value="$T('button-EpS01E05')" />
</div>
</div>
</div>
<div class="field-pair">
<label class="config">$T('sort-legenda')</label>
<input type="button" value="$T('sort-legenda')" onclick="jQuery(this).hide(); jQuery('#Key3').show();" />
<table id="Key3" class="Key">
<thead>
<tr>
<th class="align-right">$T('sort-meaning')</th>
<th>$T('sort-pattern')</th>
<th>$T('sort-result')</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-right"><b>$T('show-name'):</b></td>
<td>%t</td>
<td>$T('show-sp-name')</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%.t</td>
<td>$T('show-dot-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%_t</td>
<td>$T('show-us-name')</td>
</tr>
<tr class="even">
<td class="align-right"><b>$T('year'):</b></td>
<td>%y</td>
<td>2009</td>
</tr>
<tr>
<td class="align-right"><b>$T('month'):</b></td>
<td>%m</td>
<td>1</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%0m</td>
<td>01</td>
</tr>
<tr>
<td class="align-right"><b>$T('day-of-month'):</b></td>
<td>%d</td>
<td>2</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%0d</td>
<td>02</td>
</tr>
<tr>
<td class="align-right"><b>$T('decade'):</b></td>
<td>%decade</td>
<td>00</td>
</tr>
<tr class="even">
<td>&nbsp;</td>
<td>%0decade</td>
<td>2000</td>
</tr>
<tr>
<td class="align-right"><b>$T('orgFilename'):</b></td>
<td>%fn</td>
<td>$T('sort-File')</td>
</tr>
<tr class="even">
<td class="align-right"><b>$T('lowercase'):</b></td>
<td>{$T('TEXT')}</td>
<td>$T('text')</td>
</tr>
</tbody>
</table>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="padding alt">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
</div>
<div id="previewtv" class="example">
<div class="field-pair">
<label class="config" for="tvsamplename">Test Data</label>
<input type="text" id="tvsamplename" name="tvsamplename" placeholder="$T('show-name') S01E05 - $T('ep-name') [DTS]" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('sortResult')</label> <span class="desc path" id="previewtv-result">&nbsp;</span>
</div>
</div>
<div class="field-pair">
<label class="config">$T('sort-legenda')</label>
<button type="button" class="btn btn-default patternKey" onclick="jQuery(this).hide(); jQuery('#Key1').show();"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> $T('sort-legenda')</button>
<table id="Key1" class="Key">
<thead>
<tr>
<th class="align-right">$T('sort-meaning')</th>
<th>$T('sort-pattern')</th>
<th>$T('sort-result')</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-right"><b>$T('show-name'):</b></td>
<td>%sn</td>
<td>$T('show-sp-name') ($T('case-adjusted'))</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%s.n</td>
<td>$T('show-dot-name') ($T('case-adjusted'))</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%s_n</td>
<td>$T('show-us-name') ($T('case-adjusted'))</td>
</tr>
<tr>
<td class="align-right"><b>$T('show-name'):</b></td>
<td>%sN</td>
<td>$T('show-sp-name') </td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%s.N</td>
<td>$T('show-dot-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%s_N</td>
<td>$T('show-us-name')</td>
</tr>
<tr>
<td class="align-right"><b>$T('show-seasonNum'):</b></td>
<td>%s</td>
<td>1</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%0s</td>
<td>01</td>
</tr>
<tr>
<td class="align-right"><b>$T('show-epNum'):</b></td>
<td>%e</td>
<td>5</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%0e</td>
<td>05</td>
</tr>
<tr>
<td class="align-right"><b>$T('ep-name'):</b></td>
<td>%en</td>
<td>$T('ep-sp-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%e.n</td>
<td>$T('ep-dot-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%e_n</td>
<td>$T('ep-us-name')</td>
</tr>
<tr>
<td class="align-right"><b>$T('fileExt'):</b></td>
<td>%ext</td>
<td>avi</td>
</tr>
<tr>
<td class="align-right"><b>$T('orgFilename'):</b></td>
<td>%fn</td>
<td>$T("sort-File")</td>
</tr>
<tr>
<td class="align-right"><b>$T('orgDirname'):</b></td>
<td>%dn</td>
<td>$T("sort-Folder")</td>
</tr>
<tr>
<td class="align-right"><b>$T('lowercase'):</b></td>
<td>{$T('TEXT')}</td>
<td>$T('text')</td>
</tr>
</tbody>
</table>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div>
<!-- /col1 -->
</div>
<!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('movieSort') <a href="$helpuri$help_uri#toc6" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p>
<b>$T('affectedCat')</b><br/>
<select name="movie_cat" multiple="multiple" class="multiple_cats">
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct in $movie_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
</p>
</div>
<!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
</div>
<div class="field-pair">
<label class="config wide" for="enable_movie_sorting">$T('opt-movieSort')</label>
<input type="checkbox" name="enable_movie_sorting" id="enable_movie_sorting" value="1" <!--#if int($enable_movie_sorting)> 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config wide" for="movie_extra_folder">$T('opt-movieExtra')</label>
<input type="checkbox" name="movie_extra_folder" id="movie_extra_folder" value="1" <!--#if int($movie_extra_folder)> 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config" for="moviefoldername">$T('sortString')</label>
<input type="text" name="movie_sort_string" id="moviefoldername" value="$movie_sort_string" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config" for="movieextra">$T('multiPartLabel')</label>
<input type="text" name="movie_sort_extra" id="movieextra" value="$movie_sort_extra" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('presetSort')</label>
<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).%ext',' CD%1');movieExtraFolder(true)" value="$T('button-noFolders')" />
<input type="button" class="btn btn-default" onclick="movieSet('%0decade/%title (%y).%ext',' CD%1');movieExtraFolder(true)" value="Decades 1" />
</div>
</div>
<div id="previewmovie" class="example">
<div class="field-pair">
<label class="config" for="moviesamplename">Test Data</label>
<input type="text" id="moviesamplename" name="moviesamplename" placeholder="$T('movie-sp-name') (2009)" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('sortResult')</label> <span class="desc path" id="previewmovie-result">&nbsp;</span>
</div>
</div>
<div class="field-pair">
<label class="config">$T('sort-legenda')</label>
<button type="button" class="btn btn-default patternKey" onclick="jQuery(this).hide(); jQuery('#Key2').show();"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> $T('sort-legenda')</button>
<table id="Key2" class="Key">
<thead>
<tr>
<th class="align-right">$T('sort-meaning')</th>
<th>$T('sort-pattern')</th>
<th>$T('sort-result')</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-right"><b>$T('sort-title'):</b></td>
<td>%title</td>
<td>$T('movie-sp-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%.title</td>
<td>$T('movie-dot-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%_title</td>
<td>$T('movie-us-name')</td>
</tr>
<tr>
<td class="align-right"><b>$T('year'):</b></td>
<td>%y</td>
<td>2009</td>
</tr>
<tr>
<td class="align-right"><b>$T('extension'):</b></td>
<td>%ext</td>
<td>avi</td>
</tr>
<tr>
<td class="align-right"><b>$T('decade'):</b></td>
<td>%decade</td>
<td>00</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%0decade</td>
<td>2000</td>
</tr>
<tr>
<td class="align-right"><b>$T('orgFilename'):</b></td>
<td>%fn</td>
<td>$T('sort-File')</td>
</tr>
<tr>
<td class="align-right"><b>$T('orgDirname'):</b></td>
<td>%dn</td>
<td>$T("sort-Folder")</td>
</tr>
<tr>
<td class="align-right"><b>$T('lowercase'):</b></td>
<td>{$T('TEXT')}</td>
<td>$T('text')</td>
</tr>
</tbody>
<tbody>
<tr>
<th class="align-right"><b>$T('multiPartLabel')</b></th>
<th>$T('sort-pattern')</th>
<th>$T('sort-result')</th>
</tr>
</tbody>
<tbody>
<tr>
<td class="align-right"><b>$T('partNumber'):</b></td>
<td>%1</td>
<td>1</td>
</tr>
</tbody>
</table>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div>
<!-- /col1 -->
</div>
<!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('dateSorting') <a href="$helpuri$help_uri#toc9" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
<p>
<b>$T('affectedCat')</b><br/>
<select name="date_cat" multiple="multiple" class="multiple_cats">
<!--#for $ct in $categories#-->
<option value="$ct" <!--#if $ct in $date_categories then 'selected="selected"' else ""#--> >$Tspec($ct)</option>
<!--#end for#-->
</select>
</p>
</div>
<!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<h5 class="darkred nomargin">$T('ft-download'): <span class="path">$complete_dir</span></h5>
</div>
<div class="field-pair">
<label class="config wide" for="enable_date_sorting">$T('opt-dateSort')</label>
<input type="checkbox" name="enable_date_sorting" id="enable_date_sorting" value="1" <!--#if int($enable_date_sorting)> 0 then 'checked="checked"' else ""#--> />
</div>
<div class="field-pair">
<label class="config" for="datefoldername">$T('sortString')</label>
<input type="text" name="date_sort_string" id="datefoldername" value="$date_sort_string" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('presetSort')</label>
<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('%y-%0m/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-YMF')" />
<input type="button" class="btn btn-default" onclick="dateSet('%y-%0m-%0d/%t - %y-%0m-%0d - %desc.%ext')" value="$T('button-DailyF')" />
</div>
</div>
<div id="previewdate" class="example">
<div class="field-pair">
<label class="config" for="datesamplename">Test Data</label>
<input type="text" id="datesamplename" name="datesamplename" placeholder="$T('show-name') 2009-01-02" /><button class="btn btn-default clearBtn" type="button"><span class="glyphicon glyphicon-remove"></span> $T('button-clear')</button>
</div>
<div class="field-pair">
<label class="config">$T('sortResult')</label> <span class="desc path" id="previewdate-result">&nbsp;</span>
</div>
</div>
<div class="field-pair">
<label class="config">$T('sort-legenda')</label>
<button type="button" class="btn btn-default patternKey" onclick="jQuery(this).hide(); jQuery('#Key3').show();"><span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> $T('sort-legenda')</button>
<table id="Key3" class="Key">
<thead>
<tr>
<th class="align-right">$T('sort-meaning')</th>
<th>$T('sort-pattern')</th>
<th>$T('sort-result')</th>
</tr>
</thead>
<tbody>
<tr>
<td class="align-right"><b>$T('show-name'):</b></td>
<td>%t</td>
<td>$T('show-sp-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%.t</td>
<td>$T('show-dot-name')</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%_t</td>
<td>$T('show-us-name')</td>
</tr>
<tr>
<td class="align-right"><b>$T('year'):</b></td>
<td>%y</td>
<td>2009</td>
</tr>
<tr>
<td class="align-right"><b>$T('month'):</b></td>
<td>%m</td>
<td>1</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%0m</td>
<td>01</td>
</tr>
<tr>
<td class="align-right"><b>$T('day-of-month'):</b></td>
<td>%d</td>
<td>2</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%0d</td>
<td>02</td>
</tr>
<tr>
<td class="align-right"><b>$T('decade'):</b></td>
<td>%decade</td>
<td>00</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>%0decade</td>
<td>2000</td>
</tr>
<tr>
<td class="align-right"><b>$T('orgFilename'):</b></td>
<td>%fn</td>
<td>$T('sort-File')</td>
</tr>
<tr>
<td class="align-right"><b>$T('lowercase'):</b></td>
<td>{$T('TEXT')}</td>
<td>$T('text')</td>
</tr>
</tbody>
</table>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div>
<!-- /col1 -->
</div>
<!-- /section -->
</form>
</div><!-- /colmask -->
</div>
<!-- /colmask -->
<script>
<script type="text/javascript">
// http://stackoverflow.com/questions/2219924/idiomatic-jquery-delayed-event-only-after-a-short-pause-in-typing-e-g-timew
var typewatch = (function(){
var timer = 0;
@@ -523,21 +520,18 @@
\$('#previewdate').hide();
}
</script>
\$(document).ready(function(){
new_previewtv();
new_previewmovie();
new_previewdate();
<script>
\$(document).ready(function(){
new_previewtv();
new_previewmovie();
new_previewdate();
\$('#tvfoldername, #tvsamplename').bind("keyup focus", new_previewtv);
\$('#moviefoldername, #movieextra, #moviesamplename').bind("keyup focus", new_previewmovie);
\$('#datefoldername, #datesamplename').bind("keyup focus", new_previewdate);
\$('.clearBtn').click(function(){
\$(this).prev().val('').focus();
\$('#tvfoldername, #tvsamplename').bind("keyup focus", new_previewtv);
\$('#moviefoldername, #movieextra, #moviesamplename').bind("keyup focus", new_previewmovie);
\$('#datefoldername, #datesamplename').bind("keyup focus", new_previewdate);
\$('.clearBtn').click(function(){
\$(this).prev().val('').focus();
});
});
});
</script>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

View File

@@ -1,58 +1,56 @@
<!--#set global $pane="Special"#-->
<!--#set global $help_uri="configure+special-0-8"#-->
<!--#set global $help_uri="configuration/2.1/special"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<form action="saveSpecial" method="post" name="fullform" id="fullform">
<input type="hidden" id="session" name="session" value="$session" />
<div class="padTable">
<h3 class="darkred nomargin">$T('explain-special')</h3>
</div>
<div class="section">
<div class="col2">
<h3>$T('sptag-boolean')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<!--#set $odd = False#-->
<!--#for $option in $switches#-->
<!--#set $odd = not $odd#-->
<div class="field-pair <!--#if $odd then "alt" else ""#-->">
<label class="config wide" for="$option[0]">$option[0] ( <span class="path"><!--#if $option[2] then $T('on') else $T('off')#--></span> )</label>
<input type="checkbox" name="$option[0]" id="$option[0]" value="1" <!--#if int($option[1]) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">&nbsp;</span>
</div>
<!--#end for#-->
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('sptag-entries')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<!--#set $odd = False#-->
<!--#for $option in $entries#-->
<!--#set $odd = not $odd#-->
<div class="field-pair <!--#if $odd then "alt" else ""#-->">
<label class="config narrow" for="$option[0]">$option[0] (&nbsp;<span class="path">$option[2]</span>&nbsp;)</label>
<input type="text" name="$option[0]" id="$option[0]" value="$option[1]" />
</div>
<!--#end for#-->
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="padding alt">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
</div>
<form action="saveSpecial" method="post" autocomplete="off">
<input type="hidden" id="session" name="session" value="$session" />
<div class="padTable">
<h4 class="darkred nomargin">$T('explain-special')</h4>
</div>
<div class="section">
<div class="col2">
<h3>$T('sptag-boolean') <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<!--#for $option in $switches#-->
<div class="field-pair">
<label class="config wide" for="$option[0]">
$option[0] ( <span class="path"><!--#if $option[2] then $T('on') else $T('off')#--></span> )
<!--#if $option[1] != $option[2] then '<span class="glyphicon glyphicon-asterisk"></span>' else ''#-->
</label>
<input type="checkbox" name="$option[0]" id="$option[0]" value="1" <!--#if int($option[1]) > 0 then 'checked="checked"' else ""#--> />
<span class="desc"></span>
</div>
<!--#end for#-->
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default sabnzbd_restart"><span class="glyphicon glyphicon-refresh"></span> $T('button-restart') SABnzbd</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('sptag-entries') <a href="$helpuri$help_uri" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<!--#for $option in $entries#-->
<div class="field-pair">
<label class="config narrow" for="$option[0]">$option[0] (&nbsp;<span class="path">$option[2]</span>&nbsp;)</label>
<input type="text" name="$option[0]" id="$option[0]" value="$option[1]" />
<!--#if ($option[1] != $option[2]) and ($option[2] != "") then '<span class="glyphicon glyphicon-asterisk"></span>' else ''#-->
</div>
<!--#end for#-->
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default sabnzbd_restart"><span class="glyphicon glyphicon-refresh"></span> $T('button-restart') SABnzbd</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
</form>
</div><!-- /colmask -->

View File

@@ -1,358 +1,461 @@
<!--#set global $pane="Switches"#-->
<!--#set global $help_uri="configure-switches-0-8"#-->
<!--#set global $help_uri="configuration/2.1/switches"#-->
<!--#include $webdir + "/_inc_header_uc.tmpl"#-->
<div class="colmask">
<form action="saveSwitches" method="post" name="fullform" id="fullform">
<input type="hidden" id="session" name="session" value="$session" />
<div class="section">
<div class="col2">
<h3>$T('swtag-general')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair alt">
<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" class="selected"' else ""#--> >$T('off')</option>
<option value="1" <!--#if $check_new_rel == 1 then 'selected="selected" class="selected"' else ""#--> >$T('on')</option>
<option value="2" <!--#if $check_new_rel == 2 then 'selected="selected" class="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="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 alt <!--#if not $have_ampm then "disabled" else "" #-->">
<label class="config" for="ampm">$T('opt-ampm')</label>
<input type="checkbox" name="ampm" id="ampm" value="1" <!--#if int($ampm) > 0 then 'checked="checked"' else ""#--> <!--#if not $have_ampm then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-ampm')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-server')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair alt">
<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" size="8" min="2" />
<span class="desc">$T('explain-max_art_tries')</span>
</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 alt">
<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 ""#--> />
<span class="desc">$T('explain-auto_disconnect')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-queue')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<label class="config" for="fail_hopeless">$T('opt-fail_hopeless')</label>
<input type="checkbox" name="fail_hopeless" id="fail_hopeless" value="1" <!--#if int($fail_hopeless) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-fail_hopeless')</span>
</div>
<div class="field-pair alt">
<label class="config" for="pre_check">$T('opt-pre_check')</label>
<input type="checkbox" name="pre_check" id="pre_check" value="1" <!--#if int($pre_check) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-pre_check')</span>
</div>
<div class="field-pair">
<label class="config" for="no_dupes">$T('opt-no_dupes')</label>
<select name="no_dupes" id="no_dupes">
<option value="0" <!--#if int($no_dupes) == 0 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="1" <!--#if int($no_dupes) == 1 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-ignore')</option>
<option value="2" <!--#if int($no_dupes) == 2 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-pause')</option>
</select>
<span class="desc">$T('explain-no_dupes')</span>
</div>
<div class="field-pair alt">
<label class="config" for="no_series_dupes">$T('opt-no_series_dupes')</label>
<select name="no_series_dupes" id="no_series_dupes">
<option value="0" <!--#if int($no_series_dupes) == 0 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="1" <!--#if int($no_series_dupes) == 1 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-ignore')</option>
<option value="2" <!--#if int($no_series_dupes) == 2 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-pause')</option>
</select>
<span class="desc">$T('explain-no_series_dupes')</span>
</div>
<div class="field-pair alt">
<label class="config" for="pause_on_post_processing">$T('opt-pause_on_post_processing')</label>
<input type="checkbox" name="pause_on_post_processing" id="pause_on_post_processing" value="1" <!--#if int($pause_on_post_processing) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-pause_on_post_processing')</span>
</div>
<div class="field-pair">
<label class="config" for="pause_on_pwrar">$T('opt-pause_on_pwrar')</label>
<select name="pause_on_pwrar" id="pause_on_pwrar">
<option value="0" <!--#if int($pause_on_pwrar) == 0 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="1" <!--#if int($pause_on_pwrar) == 1 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="2" <!--#if int($pause_on_pwrar) == 2 then 'selected="selected" class="selected"' else ""#--> >$T('abort')</option>
</select>
<span class="desc">$T('explain-pause_on_pwrar')</span>
</div>
<div class="field-pair alt">
<label class="config" for="action_on_unwanted_extensions">$T('opt-action_on_unwanted_extensions')</label>
<select name="action_on_unwanted_extensions" id="action_on_unwanted_extensions">
<option value="0" <!--#if int($action_on_unwanted_extensions) == 0 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="1" <!--#if int($action_on_unwanted_extensions) == 1 then 'selected="selected" class="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="2" <!--#if int($action_on_unwanted_extensions) == 2 then 'selected="selected" class="selected"' else ""#--> >$T('abort')</option>
</select>
<span class="desc">$T('explain-action_on_unwanted_extensions')</span>
</div>
<div class="field-pair">
<label class="config" for="unwanted_extensions">$T('opt-unwanted_extensions')</label>
<input type="text" name="unwanted_extensions" id="unwanted_extensions" value="$unwanted_extensions" size="50"/>
<span class="desc">$T('explain-unwanted_extensions')</span>
</div>
<div class="field-pair alt">
<label class="config" for="top_only">$T('opt-top_only')</label>
<input type="checkbox" name="top_only" id="top_only" value="1" <!--#if int($top_only) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-top_only')</span>
</div>
<div class="field-pair">
<label class="config" for="auto_sort">$T('opt-auto_sort')</label>
<input type="checkbox" name="auto_sort" id="auto_sort" value="1" <!--#if int($auto_sort) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-auto_sort')</span>
</div>
<div class="field-pair alt">
<label class="config" for="pre_script">$T('opt-pre_script')</label>
<select name="pre_script" id="pre_script">
<!--#for $sc in $script_list#-->
<!--#if $sc.lower() == $pre_script.lower()#-->
<option value="$sc" selected="selected" class="selected">$Tspec($sc)</option>
<!--#else#-->
<option value="$sc">$Tspec($sc)</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-pre_script')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-pp')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair alt">
<label class="config" for="ignore_samples">$T('opt-ignore_samples')</label>
<select name="ignore_samples" id="ignore_samples">
<option value="0" <!--#if int($ignore_samples) == 0 then 'selected="selected" class="selected"' else ""#--> >$T('igsam-off')</option>
<option value="1" <!--#if int($ignore_samples) == 1 then 'selected="selected" class="selected"' else ""#--> >$T('igsam-del')</option>
</select>
<span class="desc">$T('explain-ignore_samples')</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 alt">
<label class="config" for="enable_all_par">$T('opt-enable_all_par')</label>
<input type="checkbox" name="enable_all_par" id="enable_all_par" value="1" <!--#if int($enable_all_par) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_all_par')</span>
</div>
<div class="field-pair <!--#if not $have_unrar then "disabled" else "" #-->">
<label class="config" for="enable_unrar">$T('opt-enable_unrar')</label>
<input type="checkbox" name="enable_unrar" id="enable_unrar" value="1" <!--#if not $have_unrar then 'readonly="readonly" disabled="disabled"' else "" #--> <!--#if int($enable_unrar) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_unrar')</span>
</div>
<div class="field-pair alt <!--#if not $have_unzip then "disabled" else "" #-->">
<label class="config" for="enable_unzip">$T('opt-enable_unzip')</label>
<input type="checkbox" name="enable_unzip" id="enable_unzip" value="1" <!--#if not $have_unzip then 'readonly="readonly" disabled="disabled"' else "" #--> <!--#if int($enable_unzip) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_unzip')</span>
</div>
<div class="field-pair <!--#if not $have_7zip then "disabled" else "" #-->">
<label class="config" for="enable_7zip">$T('opt-enable_7zip')</label>
<input type="checkbox" name="enable_7zip" id="enable_7zip" value="1" <!--#if not $have_7zip then 'readonly="readonly" disabled="disabled"' else "" #--> <!--#if int($enable_7zip) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_7zip')</span>
</div>
<div class="field-pair alt">
<label class="config" for="enable_recursive">$T('opt-enable_recursive')</label>
<input type="checkbox" name="enable_recursive" id="enable_recursive" value="1" <!--#if int($enable_recursive) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_recursive')</span>
</div>
<div class="field-pair">
<label class="config" for="flat_unpack">$T('opt-flat_unpack')</label>
<input type="checkbox" name="flat_unpack" id="flat_unpack" value="1" <!--#if int($flat_unpack) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-flat_unpack')</span>
</div>
<div class="field-pair alt">
<label class="config" for="overwrite_files">$T('opt-overwrite_files')</label>
<input type="checkbox" name="overwrite_files" id="overwrite_files" value="1" <!--#if int($overwrite_files) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-overwrite_files')</span>
</div>
<div class="field-pair">
<label class="config" for="enable_filejoin">$T('opt-enable_filejoin')</label>
<input type="checkbox" name="enable_filejoin" id="enable_filejoin" value="1" <!--#if int($enable_filejoin) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_filejoin')</span>
</div>
<div class="field-pair alt">
<label class="config" for="enable_tsjoin">$T('opt-enable_tsjoin')</label>
<input type="checkbox" name="enable_tsjoin" id="enable_tsjoin" value="1" <!--#if int($enable_tsjoin) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-ts_join')</span>
</div>
<div class="field-pair">
<label class="config" for="safe_postproc">$T('opt-safe_postproc')</label>
<input type="checkbox" name="safe_postproc" id="safe_postproc" value="1" <!--#if int($safe_postproc) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-safe_postproc')</span>
</div>
<div class="field-pair alt">
<label class="config" for="sfv_check">$T('opt-sfv_check')</label>
<input type="checkbox" name="sfv_check" id="sfv_check" value="1" <!--#if int($sfv_check) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-sfv_check')</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 alt">
<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 ""#--> />
<span class="desc">$T('explain-script_can_fail')</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 class="field-pair alt">
<label class="config" for="par_option">$T('opt-par_option')</label>
<input type="text" name="par_option" id="par_option" value="$par_option" size="20" />
<span class="desc">$T('explain-par_option')</span>
</div>
<div class="field-pair <!--#if not $have_nice then "disabled" else "" #-->">
<label class="config" for="nice">$T('opt-nice')</label>
<input type="text" name="nice" id="nice" value="$nice" size="20" <!--#if not $have_nice then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-nice')</span>
</div>
<div class="field-pair alt <!--#if not $have_ionice then "disabled" else "" #-->">
<label class="config" for="ionice">$T('opt-ionice')</label>
<input type="text" name="ionice" id="ionice" value="$ionice" size="20" <!--#if not $have_ionice then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-ionice')</span>
</div>
<div class="field-pair">
<label class="config" for="cleanup_list">$T('opt-cleanup_list')</label>
<input type="text" name="cleanup_list" id="cleanup_list" value="$cleanup_list" size="50"/>
<span class="desc">$T('explain-cleanup_list')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-naming')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<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 ""#--> />
<span class="desc">$T('explain-folder_rename')</span>
</div>
<div class="field-pair">
<label class="config" for="replace_spaces">$T('opt-replace_spaces')</label>
<input type="checkbox" name="replace_spaces" id="replace_spaces" value="1" <!--#if int($replace_spaces) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-replace_spaces')</span>
</div>
<div class="field-pair alt">
<label class="config" for="replace_dots">$T('opt-replace_dots')</label>
<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>
</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>
<div class="field-pair alt">
<label class="config" for="sanitize_safe">$T('opt-sanitize_safe')</label>
<input type="checkbox" name="sanitize_safe" id="sanitize_safe" value="1" <!--#if int($sanitize_safe) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-sanitize_safe')</span>
</div>
<div class="field-pair">
<label class="config" for="enable_meta">$T('opt-enable_meta')</label>
<input type="checkbox" name="enable_meta" id="enable_meta" value="1" <!--#if int($enable_meta) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_meta')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-quota')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair alt">
<label class="config" for="quota_size">$T('opt-quota_size')</label>
<input type="text" name="quota_size" id="quota_size" value="$quota_size" size="8" />
<span class="desc">$T('explain-quota_size')</span>
</div>
<div class="field-pair">
<label class="config" for="quota_period">$T('opt-quota_period')</label>
<select name="quota_period" id="quota_period">
<option value="d" <!--#if $quota_period == "d" then 'selected="selected" class="selected"' else ""#--> >$T('day').capitalize()</option>
<option value="w" <!--#if $quota_period == "w" then 'selected="selected" class="selected"' else ""#--> >$T('week').capitalize()</option>
<option value="m" <!--#if $quota_period == "m" then 'selected="selected" class="selected"' else ""#--> >$T('month').capitalize()</option>
<option value="x" <!--#if $quota_period == "x" then 'selected="selected" class="selected"' else ""#--> >$T('manual').capitalize()</option>
</select>
<span class="desc">$T('explain-quota_period')</span>
</div>
<div class="field-pair alt">
<label class="config" for="quota_day">$T('opt-quota_day')</label>
<input type="text" name="quota_day" id="quota_day" value="$quota_day" size="20" />
<span class="desc">$T('explain-quota_day')</span>
</div>
<div class="field-pair">
<label class="config" for="quota_resume">$T('opt-quota_resume')</label>
<input type="checkbox" name="quota_resume" id="quota_resume" value="1" <!--#if int($quota_resume) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-quota_resume')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="padding alt">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />
</div>
<form action="saveSwitches" method="post" name="fullform" class="fullform" autocomplete="off">
<input type="hidden" id="session" name="session" value="$session" />
<div class="section">
<div class="col2">
<h3>$T('swtag-server') <a href="$helpuri$help_uri#toc1" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<label class="config" for="load_balancing">$T('opt-load_balancing')</label>
<select name="load_balancing" id="load_balancing">
<option value="0" <!--#if $load_balancing == 0 then 'selected="selected"' else ""#--> >$T('no-load-balancing')</option>
<option value="1" <!--#if $load_balancing == 1 then 'selected="selected"' else ""#--> >$T('load-balancing')</option>
<option value="2" <!--#if $load_balancing == 2 then 'selected="selected"' else ""#--> >$T('load-balancing-happy-eyeballs')</option>
</select>
<span class="desc">$T('explain-load_balancing')</span>
</div>
<div class="field-pair">
<label class="config" for="ssl_ciphers">$T('opt-ssl_ciphers')</label>
<input type="text" name="ssl_ciphers" id="ssl_ciphers" value="$ssl_ciphers" />
<span class="desc">$T('explain-ssl_ciphers') <br>$T('readwiki')
<a href="${helpuri}advanced/ssl-ciphers" target="_blank">${helpuri}advanced/ssl-ciphers</a></span>
</div>
<div class="field-pair">
<label class="config" for="max_art_tries">$T('opt-max_art_tries')</label>
<input type="number" name="max_art_tries" id="max_art_tries" value="$max_art_tries" min="2" max="2000" />
<span class="desc">$T('explain-max_art_tries')</span>
</div>
<div class="field-pair">
<label class="config" for="auto_disconnect">$T('opt-auto_disconnect')</label>
<input type="checkbox" name="auto_disconnect" id="auto_disconnect" value="1" <!--#if int($auto_disconnect) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-auto_disconnect')</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="col2">
<h3>$T('swtag-queue') <a href="$helpuri$help_uri#toc2" 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="pre_script">$T('opt-pre_script')</label>
<select name="pre_script" id="pre_script">
<!--#for $sc in $scripts#-->
<!--#if $sc.lower() == $pre_script.lower()#-->
<option value="$sc" selected="selected">$Tspec($sc)</option>
<!--#else#-->
<option value="$sc">$Tspec($sc)</option>
<!--#end if#-->
<!--#end for#-->
</select>
<span class="desc">$T('explain-pre_script')</span>
</div>
<div class="field-pair">
<label class="config" for="propagation_delay">$T('opt-propagation_delay')</label>
<input type="number" name="propagation_delay" id="propagation_delay" value="$propagation_delay" /> <i>$T('minutes')</i>
<span class="desc">$T('explain-propagation_delay')</span>
</div>
<div class="field-pair">
<label class="config" for="top_only">$T('opt-top_only')</label>
<input type="checkbox" name="top_only" id="top_only" value="1" <!--#if int($top_only) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-top_only')</span>
</div>
<div class="field-pair">
<label class="config" for="pre_check">$T('opt-pre_check')</label>
<input type="checkbox" name="pre_check" id="pre_check" value="1" <!--#if int($pre_check) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-pre_check')</span>
</div>
<div class="field-pair">
<label class="config" for="fail_hopeless_jobs">$T('opt-fail_hopeless_jobs')</label>
<input type="checkbox" name="fail_hopeless_jobs" id="fail_hopeless_jobs" value="1" <!--#if int($fail_hopeless_jobs) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-fail_hopeless_jobs')</span>
</div>
<div class="field-pair">
<label class="config" for="no_dupes">$T('opt-no_dupes')</label>
<select name="no_dupes" id="no_dupes">
<option value="0" <!--#if int($no_dupes) == 0 then 'selected="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="2" <!--#if int($no_dupes) == 2 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="3" <!--#if int($no_dupes) == 3 then 'selected="selected"' else ""#--> >$T('nodupes-fail')</option>
<option value="1" <!--#if int($no_dupes) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-ignore')</option>
</select>
<span class="desc">$T('explain-no_dupes')</span>
</div>
<div class="field-pair">
<label class="config" for="no_series_dupes">$T('opt-no_series_dupes')</label>
<select name="no_series_dupes" id="no_series_dupes">
<option value="0" <!--#if int($no_series_dupes) == 0 then 'selected="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="2" <!--#if int($no_series_dupes) == 2 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="3" <!--#if int($no_series_dupes) == 3 then 'selected="selected"' else ""#--> >$T('nodupes-fail')</option>
<option value="1" <!--#if int($no_series_dupes) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-ignore')</option>
</select>
<span class="desc">$T('explain-no_series_dupes')</span>
</div>
<div class="field-pair">
<label class="config" for="pause_on_pwrar">$T('opt-pause_on_pwrar')</label>
<select name="pause_on_pwrar" id="pause_on_pwrar">
<option value="0" <!--#if int($pause_on_pwrar) == 0 then 'selected="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="1" <!--#if int($pause_on_pwrar) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="2" <!--#if int($pause_on_pwrar) == 2 then 'selected="selected"' else ""#--> >$T('abort')</option>
</select>
<span class="desc">$T('explain-pause_on_pwrar')</span>
</div>
<div class="field-pair">
<label class="config" for="action_on_unwanted_extensions">$T('opt-action_on_unwanted_extensions')</label>
<select name="action_on_unwanted_extensions" id="action_on_unwanted_extensions">
<option value="0" <!--#if int($action_on_unwanted_extensions) == 0 then 'selected="selected"' else ""#--> >$T('nodupes-off')</option>
<option value="1" <!--#if int($action_on_unwanted_extensions) == 1 then 'selected="selected"' else ""#--> >$T('nodupes-pause')</option>
<option value="2" <!--#if int($action_on_unwanted_extensions) == 2 then 'selected="selected"' else ""#--> >$T('abort')</option>
</select>
<span class="desc">$T('explain-action_on_unwanted_extensions')</span>
</div>
<div class="field-pair">
<label class="config" for="unwanted_extensions">$T('opt-unwanted_extensions')</label>
<input type="text" name="unwanted_extensions" id="unwanted_extensions" value="$unwanted_extensions"/>
<span class="desc">$T('explain-unwanted_extensions')</span>
</div>
<div class="field-pair">
<label class="config" for="auto_sort">$T('opt-auto_sort')</label>
<input type="checkbox" name="auto_sort" id="auto_sort" value="1" <!--#if int($auto_sort) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-auto_sort')</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="col2">
<h3>$T('swtag-pp') <a href="$helpuri$help_uri#toc3" 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="pause_on_post_processing">$T('opt-pause_on_post_processing')</label>
<input type="checkbox" name="pause_on_post_processing" id="pause_on_post_processing" value="1" <!--#if int($pause_on_post_processing) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-pause_on_post_processing')</span>
</div>
<div class="field-pair">
<label class="config" for="enable_all_par">$T('opt-enable_all_par')</label>
<input type="checkbox" name="enable_all_par" id="enable_all_par" value="1" <!--#if int($enable_all_par) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_all_par').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair">
<label class="config" for="par_option">$T('opt-par_option')</label>
<input type="text" name="par_option" id="par_option" value="$par_option" />
<span class="desc">$T('explain-par_option')</span>
</div>
<div class="field-pair">
<label class="config" for="sfv_check">$T('opt-sfv_check')</label>
<input type="checkbox" name="sfv_check" id="sfv_check" value="1" <!--#if int($sfv_check) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-sfv_check')</span>
</div>
<div class="field-pair">
<label class="config" for="safe_postproc">$T('opt-safe_postproc')</label>
<input type="checkbox" name="safe_postproc" id="safe_postproc" value="1" <!--#if int($safe_postproc) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-safe_postproc')</span>
</div>
<div class="field-pair">
<label class="config" for="enable_recursive">$T('opt-enable_recursive')</label>
<input type="checkbox" name="enable_recursive" id="enable_recursive" value="1" <!--#if int($enable_recursive) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-enable_recursive')</span>
</div>
<div class="field-pair">
<label class="config" for="flat_unpack">$T('opt-flat_unpack')</label>
<input type="checkbox" name="flat_unpack" id="flat_unpack" value="1" <!--#if int($flat_unpack) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-flat_unpack')</span>
</div>
<div class="field-pair">
<label class="config" for="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 ""#--> />
<span class="desc">$T('explain-script_can_fail')</span>
</div>
<div class="field-pair">
<label class="config" for="new_nzb_on_failure">$T('opt-new_nzb_on_failure')</label>
<input type="checkbox" name="new_nzb_on_failure" id="new_nzb_on_failure" value="1" <!--#if int($new_nzb_on_failure) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-new_nzb_on_failure')</span>
</div>
<!--#if not $nt#-->
<div class="field-pair <!--#if not $have_nice then "disabled" else "" #-->">
<label class="config" for="nice">$T('opt-nice')</label>
<input type="text" name="nice" id="nice" value="$nice" <!--#if not $have_nice then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-nice')</span>
</div>
<div class="field-pair <!--#if not $have_ionice then "disabled" else "" #-->">
<label class="config" for="ionice">$T('opt-ionice')</label>
<input type="text" name="ionice" id="ionice" value="$ionice" <!--#if not $have_ionice then 'readonly="readonly" disabled="disabled"' else "" #--> />
<span class="desc">$T('explain-ionice')</span>
</div>
<!--#end if#-->
<div class="field-pair">
<label class="config" for="ignore_samples">$T('opt-ignore_samples')</label>
<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>
</div>
<div class="field-pair">
<label class="config" for="cleanup_list">$T('opt-cleanup_list')</label>
<input type="text" name="cleanup_list" id="cleanup_list" value="$cleanup_list"/>
<span class="desc">$T('explain-cleanup_list')</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="col2">
<h3>$T('swtag-naming') <a href="$helpuri$help_uri#toc4" 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="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 ""#--> />
<span class="desc">$T('explain-folder_rename')</span>
</div>
<div class="field-pair">
<label class="config" for="replace_spaces">$T('opt-replace_spaces')</label>
<input type="checkbox" name="replace_spaces" id="replace_spaces" value="1" <!--#if int($replace_spaces) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-replace_spaces')</span>
</div>
<div class="field-pair">
<label class="config" for="replace_dots">$T('opt-replace_dots')</label>
<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>
</div>
<!--#if not $nt#-->
<div class="field-pair">
<label class="config" for="sanitize_safe">$T('opt-sanitize_safe')</label>
<input type="checkbox" name="sanitize_safe" id="sanitize_safe" value="1" <!--#if int($sanitize_safe) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-sanitize_safe')</span>
</div>
<!--#end if#-->
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default restoreDefaults"><span class="glyphicon glyphicon-asterisk"></span> $T('button-restoreDefaults')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-quota') <a href="$helpuri$help_uri#toc5" 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="quota_size">$T('opt-quota_size')</label>
<input type="text" name="quota_size" id="quota_size" value="$quota_size" class="smaller_input" />
<span class="desc">$T('explain-quota_size')</span>
</div>
<div class="field-pair">
<label class="config" for="quota_period">$T('opt-quota_period')</label>
<select name="quota_period" id="quota_period">
<option value="d" <!--#if $quota_period == "d" then 'selected="selected"' else ""#--> >$T('day').capitalize()</option>
<option value="w" <!--#if $quota_period == "w" then 'selected="selected"' else ""#--> >$T('week').capitalize()</option>
<option value="m" <!--#if $quota_period == "m" then 'selected="selected"' else ""#--> >$T('month').capitalize()</option>
<option value="x" <!--#if $quota_period == "x" then 'selected="selected"' else ""#--> >$T('manual').capitalize()</option>
</select>
<span class="desc">$T('explain-quota_period')</span>
</div>
<div class="field-pair">
<label class="config" for="quota_day">$T('opt-quota_day')</label>
<input type="text" name="quota_day" id="quota_day" value="$quota_day" class="smaller_input" />
<span class="desc">$T('explain-quota_day')</span>
</div>
<div class="field-pair">
<label class="config" for="quota_resume">$T('opt-quota_resume')</label>
<input type="checkbox" name="quota_resume" id="quota_resume" value="1" <!--#if int($quota_resume) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-quota_resume')</span>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-indexing') <a href="$helpuri$help_uri#toc6" 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="rating_enable">$T('opt-rating_enable')</label>
<input type="checkbox" name="rating_enable" id="rating_enable" value="1" <!--#if int($rating_enable) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-rating_enable').replace('. ', '.<br/>')</span>
</div>
<div class="field-pair">
<label class="config" for="rating_api_key">$T('opt-rating_api_key')</label>
<input type="text" name="rating_api_key" id="rating_api_key" value="$rating_api_key" />
<span class="desc">$T('explain-rating_api_key')</span>
</div>
<div class="field-pair">
<label class="config" for="rating_filter_enable">$T('opt-rating_filter_enable')</label>
<input type="checkbox" name="rating_filter_enable" id="rating_filter_enable" value="1" <!--#if int($rating_filter_enable) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-rating_filter_enable')</span>
</div>
<div class="field-pair" id="rating_filter_abort">
<label class="config">$T('opt-rating_filter_abort_if')</label>
<div class="rating-filter">
<p>
<label for="rating_filter_abort_video">$T('opt-rating_filter_video')</label>
<select name="rating_filter_abort_video" id="rating_filter_abort_video">
<option value="0" <!--#if $rating_filter_abort_video == 0 then 'selected="selected"' else ""#--> >$T('notUsed')</option>
<!--#for $val in $range(1, 10)#--><option value="$val" <!--#if $rating_filter_abort_video == $val then 'selected="selected"' else ""#--> >$val $T('orLess')</option><!--#end for#-->
</select>
</p>
<p>
<label for="rating_filter_abort_audio">$T('opt-rating_filter_audio')</label>
<select name="rating_filter_abort_audio" id="rating_filter_abort_audio">
<option value="0" <!--#if $rating_filter_abort_audio == 0 then 'selected="selected"' else ""#--> >$T('notUsed')</option>
<!--#for $val in $range(1, 10)#--><option value="$val" <!--#if $rating_filter_abort_audio == $val then 'selected="selected"' else ""#--> >$val $T('orLess')</option><!--#end for#-->
</select>
</p>
<p>
<span>
<input type="checkbox" value="1" id="rating_filter_abort_encrypted" name="rating_filter_abort_encrypted" <!--#if int($rating_filter_abort_encrypted) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_encrypted">$T('opt-rating_filter_passworded')</label>
</span>
<span>
<input type="checkbox" value="1" id="rating_filter_abort_encrypted_confirm" name="rating_filter_abort_encrypted_confirm" <!--#if int($rating_filter_abort_encrypted_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_encrypted_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
<p>
<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 ""#--> />
<label for="rating_filter_abort_spam">$T('opt-rating_filter_spam')</label>
</span>
<span>
<input type="checkbox" value="1" id="rating_filter_abort_spam_confirm" name="rating_filter_abort_spam_confirm" <!--#if int($rating_filter_abort_spam_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_abort_spam_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
<p>
<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>
</p>
<p>
<label for="rating_filter_abort_keywords">$T('opt-rating_filter_keywords')</label>
<input type="text" name="rating_filter_abort_keywords" id="rating_filter_abort_keywords" value="$rating_filter_abort_keywords"/>
<span class="desc">$T('explain-rating_filter_keywords')</span>
</p>
</div>
</div>
<div class="field-pair" id="rating_filter_pause">
<label class="config">$T('opt-rating_filter_pause_if')</label>
<div class="rating-filter">
<p>
<label for="rating_filter_pause_video">$T('opt-rating_filter_video')</label>
<select name="rating_filter_pause_video" id="rating_filter_pause_video">
<option value="0" <!--#if $rating_filter_pause_video == 0 then 'selected="selected"' else ""#--> >$T('notUsed')</option>
<!--#for $val in $range(1, 10)#--><option value="$val" <!--#if $rating_filter_pause_video == $val then 'selected="selected"' else ""#--> >$val $T('orLess')</option><!--#end for#-->
</select>
</p>
<p>
<label for="rating_filter_pause_audio">$T('opt-rating_filter_audio')</label>
<select name="rating_filter_pause_audio" id="rating_filter_pause_audio">
<option value="0" <!--#if $rating_filter_pause_audio == 0 then 'selected="selected"' else ""#--> >$T('notUsed')</option>
<!--#for $val in $range(1, 10)#--><option value="$val" <!--#if $rating_filter_pause_audio == $val then 'selected="selected"' else ""#--> >$val $T('orLess') </option><!--#end for#-->
</select>
</p>
<p>
<span>
<input type="checkbox" value="1" id="rating_filter_pause_encrypted" name="rating_filter_pause_encrypted" <!--#if int($rating_filter_pause_encrypted) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_encrypted">$T('opt-rating_filter_passworded')</label>
</span>
<span>
<input type="checkbox" value="1" id="rating_filter_pause_encrypted_confirm" name="rating_filter_pause_encrypted_confirm" <!--#if int($rating_filter_pause_encrypted_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_encrypted_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
<p>
<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 ""#--> />
<label for="rating_filter_pause_spam">$T('opt-rating_filter_spam')</label>
</span>
<span>
<input type="checkbox" value="1" id="rating_filter_pause_spam_confirm" name="rating_filter_pause_spam_confirm" <!--#if int($rating_filter_pause_spam_confirm) > 0 then 'checked="checked"' else ""#--> />
<label for="rating_filter_pause_spam_confirm">$T('opt-rating_filter_confirmed')</label>
</span>
</p>
<p>
<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>
</p>
<p>
<label for="rating_filter_pause_keywords">$T('opt-rating_filter_keywords')</label>
<input type="text" name="rating_filter_pause_keywords" id="rating_filter_pause_keywords" value="$rating_filter_pause_keywords"/>
<span class="desc">$T('explain-rating_filter_keywords')</span>
</p>
</div>
</div>
<div class="field-pair">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
</form>
</div><!-- /colmask -->
<script type="text/javascript">
\$(document).ready(function() {
if (!\$('#rating_filter_enable').is(":checked")) {
\$("#rating_filter_abort").hide();
\$("#rating_filter_pause").hide();
}
\$('#rating_filter_enable').change(function () {
if (\$(this).is(":checked")) {
\$("#rating_filter_abort").show();
\$("#rating_filter_pause").show();
}
else {
\$("#rating_filter_abort").hide();
\$("#rating_filter_pause").hide();
}
});
\$('.restoreDefaults').click(function(e) {
// Get section name
var sectionName = \$(this).parents('.section').find('.col2 h3').text().trim()
// Confirm?
if(!confirm("$T('explain-restoreDefaults') \""+sectionName+"\"\n$T('confirm')")) return false
e.preventDefault()
// Need to get all the input values, so same way as saving normally
var key_container = {}
\$(this).parents('.section').extractFormDataTo(key_container);
key_container = Object.keys(key_container)
// Send request
\$.ajax({
type: "GET",
url: "../../tapi",
data: "mode=set_config_default&session=${session}&output=json&keyword=" + key_container.join('&keyword=')
}).then(function(data) {
// Reload page
document.location = document.location
})
})
});
</script>
<!--#include $webdir + "/_inc_footer_uc.tmpl"#-->

View File

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

View File

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

View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,288 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata></metadata>
<defs>
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
<font-face units-per-em="1200" ascent="960" descent="-240" />
<missing-glyph horiz-adv-x="500" />
<glyph horiz-adv-x="0" />
<glyph horiz-adv-x="400" />
<glyph unicode=" " />
<glyph unicode="*" d="M600 1100q15 0 34 -1.5t30 -3.5l11 -1q10 -2 17.5 -10.5t7.5 -18.5v-224l158 158q7 7 18 8t19 -6l106 -106q7 -8 6 -19t-8 -18l-158 -158h224q10 0 18.5 -7.5t10.5 -17.5q6 -41 6 -75q0 -15 -1.5 -34t-3.5 -30l-1 -11q-2 -10 -10.5 -17.5t-18.5 -7.5h-224l158 -158 q7 -7 8 -18t-6 -19l-106 -106q-8 -7 -19 -6t-18 8l-158 158v-224q0 -10 -7.5 -18.5t-17.5 -10.5q-41 -6 -75 -6q-15 0 -34 1.5t-30 3.5l-11 1q-10 2 -17.5 10.5t-7.5 18.5v224l-158 -158q-7 -7 -18 -8t-19 6l-106 106q-7 8 -6 19t8 18l158 158h-224q-10 0 -18.5 7.5 t-10.5 17.5q-6 41 -6 75q0 15 1.5 34t3.5 30l1 11q2 10 10.5 17.5t18.5 7.5h224l-158 158q-7 7 -8 18t6 19l106 106q8 7 19 6t18 -8l158 -158v224q0 10 7.5 18.5t17.5 10.5q41 6 75 6z" />
<glyph unicode="+" d="M450 1100h200q21 0 35.5 -14.5t14.5 -35.5v-350h350q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-350v-350q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v350h-350q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5 h350v350q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xa0;" />
<glyph unicode="&#xa5;" d="M825 1100h250q10 0 12.5 -5t-5.5 -13l-364 -364q-6 -6 -11 -18h268q10 0 13 -6t-3 -14l-120 -160q-6 -8 -18 -14t-22 -6h-125v-100h275q10 0 13 -6t-3 -14l-120 -160q-6 -8 -18 -14t-22 -6h-125v-174q0 -11 -7.5 -18.5t-18.5 -7.5h-148q-11 0 -18.5 7.5t-7.5 18.5v174 h-275q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h125v100h-275q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h118q-5 12 -11 18l-364 364q-8 8 -5.5 13t12.5 5h250q25 0 43 -18l164 -164q8 -8 18 -8t18 8l164 164q18 18 43 18z" />
<glyph unicode="&#x2000;" horiz-adv-x="650" />
<glyph unicode="&#x2001;" horiz-adv-x="1300" />
<glyph unicode="&#x2002;" horiz-adv-x="650" />
<glyph unicode="&#x2003;" horiz-adv-x="1300" />
<glyph unicode="&#x2004;" horiz-adv-x="433" />
<glyph unicode="&#x2005;" horiz-adv-x="325" />
<glyph unicode="&#x2006;" horiz-adv-x="216" />
<glyph unicode="&#x2007;" horiz-adv-x="216" />
<glyph unicode="&#x2008;" horiz-adv-x="162" />
<glyph unicode="&#x2009;" horiz-adv-x="260" />
<glyph unicode="&#x200a;" horiz-adv-x="72" />
<glyph unicode="&#x202f;" horiz-adv-x="260" />
<glyph unicode="&#x205f;" horiz-adv-x="325" />
<glyph unicode="&#x20ac;" d="M744 1198q242 0 354 -189q60 -104 66 -209h-181q0 45 -17.5 82.5t-43.5 61.5t-58 40.5t-60.5 24t-51.5 7.5q-19 0 -40.5 -5.5t-49.5 -20.5t-53 -38t-49 -62.5t-39 -89.5h379l-100 -100h-300q-6 -50 -6 -100h406l-100 -100h-300q9 -74 33 -132t52.5 -91t61.5 -54.5t59 -29 t47 -7.5q22 0 50.5 7.5t60.5 24.5t58 41t43.5 61t17.5 80h174q-30 -171 -128 -278q-107 -117 -274 -117q-206 0 -324 158q-36 48 -69 133t-45 204h-217l100 100h112q1 47 6 100h-218l100 100h134q20 87 51 153.5t62 103.5q117 141 297 141z" />
<glyph unicode="&#x20bd;" d="M428 1200h350q67 0 120 -13t86 -31t57 -49.5t35 -56.5t17 -64.5t6.5 -60.5t0.5 -57v-16.5v-16.5q0 -36 -0.5 -57t-6.5 -61t-17 -65t-35 -57t-57 -50.5t-86 -31.5t-120 -13h-178l-2 -100h288q10 0 13 -6t-3 -14l-120 -160q-6 -8 -18 -14t-22 -6h-138v-175q0 -11 -5.5 -18 t-15.5 -7h-149q-10 0 -17.5 7.5t-7.5 17.5v175h-267q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h117v100h-267q-10 0 -13 6t3 14l120 160q6 8 18 14t22 6h117v475q0 10 7.5 17.5t17.5 7.5zM600 1000v-300h203q64 0 86.5 33t22.5 119q0 84 -22.5 116t-86.5 32h-203z" />
<glyph unicode="&#x2212;" d="M250 700h800q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#x231b;" d="M1000 1200v-150q0 -21 -14.5 -35.5t-35.5 -14.5h-50v-100q0 -91 -49.5 -165.5t-130.5 -109.5q81 -35 130.5 -109.5t49.5 -165.5v-150h50q21 0 35.5 -14.5t14.5 -35.5v-150h-800v150q0 21 14.5 35.5t35.5 14.5h50v150q0 91 49.5 165.5t130.5 109.5q-81 35 -130.5 109.5 t-49.5 165.5v100h-50q-21 0 -35.5 14.5t-14.5 35.5v150h800zM400 1000v-100q0 -60 32.5 -109.5t87.5 -73.5q28 -12 44 -37t16 -55t-16 -55t-44 -37q-55 -24 -87.5 -73.5t-32.5 -109.5v-150h400v150q0 60 -32.5 109.5t-87.5 73.5q-28 12 -44 37t-16 55t16 55t44 37 q55 24 87.5 73.5t32.5 109.5v100h-400z" />
<glyph unicode="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
<glyph unicode="&#x2601;" d="M503 1089q110 0 200.5 -59.5t134.5 -156.5q44 14 90 14q120 0 205 -86.5t85 -206.5q0 -121 -85 -207.5t-205 -86.5h-750q-79 0 -135.5 57t-56.5 137q0 69 42.5 122.5t108.5 67.5q-2 12 -2 37q0 153 108 260.5t260 107.5z" />
<glyph unicode="&#x26fa;" d="M774 1193.5q16 -9.5 20.5 -27t-5.5 -33.5l-136 -187l467 -746h30q20 0 35 -18.5t15 -39.5v-42h-1200v42q0 21 15 39.5t35 18.5h30l468 746l-135 183q-10 16 -5.5 34t20.5 28t34 5.5t28 -20.5l111 -148l112 150q9 16 27 20.5t34 -5zM600 200h377l-182 112l-195 534v-646z " />
<glyph unicode="&#x2709;" d="M25 1100h1150q10 0 12.5 -5t-5.5 -13l-564 -567q-8 -8 -18 -8t-18 8l-564 567q-8 8 -5.5 13t12.5 5zM18 882l264 -264q8 -8 8 -18t-8 -18l-264 -264q-8 -8 -13 -5.5t-5 12.5v550q0 10 5 12.5t13 -5.5zM918 618l264 264q8 8 13 5.5t5 -12.5v-550q0 -10 -5 -12.5t-13 5.5 l-264 264q-8 8 -8 18t8 18zM818 482l364 -364q8 -8 5.5 -13t-12.5 -5h-1150q-10 0 -12.5 5t5.5 13l364 364q8 8 18 8t18 -8l164 -164q8 -8 18 -8t18 8l164 164q8 8 18 8t18 -8z" />
<glyph unicode="&#x270f;" d="M1011 1210q19 0 33 -13l153 -153q13 -14 13 -33t-13 -33l-99 -92l-214 214l95 96q13 14 32 14zM1013 800l-615 -614l-214 214l614 614zM317 96l-333 -112l110 335z" />
<glyph unicode="&#xe001;" d="M700 650v-550h250q21 0 35.5 -14.5t14.5 -35.5v-50h-800v50q0 21 14.5 35.5t35.5 14.5h250v550l-500 550h1200z" />
<glyph unicode="&#xe002;" d="M368 1017l645 163q39 15 63 0t24 -49v-831q0 -55 -41.5 -95.5t-111.5 -63.5q-79 -25 -147 -4.5t-86 75t25.5 111.5t122.5 82q72 24 138 8v521l-600 -155v-606q0 -42 -44 -90t-109 -69q-79 -26 -147 -5.5t-86 75.5t25.5 111.5t122.5 82.5q72 24 138 7v639q0 38 14.5 59 t53.5 34z" />
<glyph unicode="&#xe003;" d="M500 1191q100 0 191 -39t156.5 -104.5t104.5 -156.5t39 -191l-1 -2l1 -5q0 -141 -78 -262l275 -274q23 -26 22.5 -44.5t-22.5 -42.5l-59 -58q-26 -20 -46.5 -20t-39.5 20l-275 274q-119 -77 -261 -77l-5 1l-2 -1q-100 0 -191 39t-156.5 104.5t-104.5 156.5t-39 191 t39 191t104.5 156.5t156.5 104.5t191 39zM500 1022q-88 0 -162 -43t-117 -117t-43 -162t43 -162t117 -117t162 -43t162 43t117 117t43 162t-43 162t-117 117t-162 43z" />
<glyph unicode="&#xe005;" d="M649 949q48 68 109.5 104t121.5 38.5t118.5 -20t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-150 152.5t-126.5 127.5t-93.5 124.5t-33.5 117.5q0 64 28 123t73 100.5t104 64t119 20 t120.5 -38.5t104.5 -104z" />
<glyph unicode="&#xe006;" d="M407 800l131 353q7 19 17.5 19t17.5 -19l129 -353h421q21 0 24 -8.5t-14 -20.5l-342 -249l130 -401q7 -20 -0.5 -25.5t-24.5 6.5l-343 246l-342 -247q-17 -12 -24.5 -6.5t-0.5 25.5l130 400l-347 251q-17 12 -14 20.5t23 8.5h429z" />
<glyph unicode="&#xe007;" d="M407 800l131 353q7 19 17.5 19t17.5 -19l129 -353h421q21 0 24 -8.5t-14 -20.5l-342 -249l130 -401q7 -20 -0.5 -25.5t-24.5 6.5l-343 246l-342 -247q-17 -12 -24.5 -6.5t-0.5 25.5l130 400l-347 251q-17 12 -14 20.5t23 8.5h429zM477 700h-240l197 -142l-74 -226 l193 139l195 -140l-74 229l192 140h-234l-78 211z" />
<glyph unicode="&#xe008;" d="M600 1200q124 0 212 -88t88 -212v-250q0 -46 -31 -98t-69 -52v-75q0 -10 6 -21.5t15 -17.5l358 -230q9 -5 15 -16.5t6 -21.5v-93q0 -10 -7.5 -17.5t-17.5 -7.5h-1150q-10 0 -17.5 7.5t-7.5 17.5v93q0 10 6 21.5t15 16.5l358 230q9 6 15 17.5t6 21.5v75q-38 0 -69 52 t-31 98v250q0 124 88 212t212 88z" />
<glyph unicode="&#xe009;" d="M25 1100h1150q10 0 17.5 -7.5t7.5 -17.5v-1050q0 -10 -7.5 -17.5t-17.5 -7.5h-1150q-10 0 -17.5 7.5t-7.5 17.5v1050q0 10 7.5 17.5t17.5 7.5zM100 1000v-100h100v100h-100zM875 1000h-550q-10 0 -17.5 -7.5t-7.5 -17.5v-350q0 -10 7.5 -17.5t17.5 -7.5h550 q10 0 17.5 7.5t7.5 17.5v350q0 10 -7.5 17.5t-17.5 7.5zM1000 1000v-100h100v100h-100zM100 800v-100h100v100h-100zM1000 800v-100h100v100h-100zM100 600v-100h100v100h-100zM1000 600v-100h100v100h-100zM875 500h-550q-10 0 -17.5 -7.5t-7.5 -17.5v-350q0 -10 7.5 -17.5 t17.5 -7.5h550q10 0 17.5 7.5t7.5 17.5v350q0 10 -7.5 17.5t-17.5 7.5zM100 400v-100h100v100h-100zM1000 400v-100h100v100h-100zM100 200v-100h100v100h-100zM1000 200v-100h100v100h-100z" />
<glyph unicode="&#xe010;" d="M50 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM650 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400 q0 21 14.5 35.5t35.5 14.5zM50 500h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM650 500h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe011;" d="M50 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5zM850 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM50 700h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 700h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM850 700h200q21 0 35.5 -14.5t14.5 -35.5v-200 q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM50 300h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 300h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM850 300h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5 t35.5 14.5z" />
<glyph unicode="&#xe012;" d="M50 1100h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 1100h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200 q0 21 14.5 35.5t35.5 14.5zM50 700h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 700h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700 q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM50 300h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5zM450 300h700q21 0 35.5 -14.5t14.5 -35.5v-200 q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe013;" d="M465 477l571 571q8 8 18 8t17 -8l177 -177q8 -7 8 -17t-8 -18l-783 -784q-7 -8 -17.5 -8t-17.5 8l-384 384q-8 8 -8 18t8 17l177 177q7 8 17 8t18 -8l171 -171q7 -7 18 -7t18 7z" />
<glyph unicode="&#xe014;" d="M904 1083l178 -179q8 -8 8 -18.5t-8 -17.5l-267 -268l267 -268q8 -7 8 -17.5t-8 -18.5l-178 -178q-8 -8 -18.5 -8t-17.5 8l-268 267l-268 -267q-7 -8 -17.5 -8t-18.5 8l-178 178q-8 8 -8 18.5t8 17.5l267 268l-267 268q-8 7 -8 17.5t8 18.5l178 178q8 8 18.5 8t17.5 -8 l268 -267l268 268q7 7 17.5 7t18.5 -7z" />
<glyph unicode="&#xe015;" d="M507 1177q98 0 187.5 -38.5t154.5 -103.5t103.5 -154.5t38.5 -187.5q0 -141 -78 -262l300 -299q8 -8 8 -18.5t-8 -18.5l-109 -108q-7 -8 -17.5 -8t-18.5 8l-300 299q-119 -77 -261 -77q-98 0 -188 38.5t-154.5 103t-103 154.5t-38.5 188t38.5 187.5t103 154.5 t154.5 103.5t188 38.5zM506.5 1023q-89.5 0 -165.5 -44t-120 -120.5t-44 -166t44 -165.5t120 -120t165.5 -44t166 44t120.5 120t44 165.5t-44 166t-120.5 120.5t-166 44zM425 900h150q10 0 17.5 -7.5t7.5 -17.5v-75h75q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5 t-17.5 -7.5h-75v-75q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v75h-75q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h75v75q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe016;" d="M507 1177q98 0 187.5 -38.5t154.5 -103.5t103.5 -154.5t38.5 -187.5q0 -141 -78 -262l300 -299q8 -8 8 -18.5t-8 -18.5l-109 -108q-7 -8 -17.5 -8t-18.5 8l-300 299q-119 -77 -261 -77q-98 0 -188 38.5t-154.5 103t-103 154.5t-38.5 188t38.5 187.5t103 154.5 t154.5 103.5t188 38.5zM506.5 1023q-89.5 0 -165.5 -44t-120 -120.5t-44 -166t44 -165.5t120 -120t165.5 -44t166 44t120.5 120t44 165.5t-44 166t-120.5 120.5t-166 44zM325 800h350q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-350q-10 0 -17.5 7.5 t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe017;" d="M550 1200h100q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM800 975v166q167 -62 272 -209.5t105 -331.5q0 -117 -45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5 t-184.5 123t-123 184.5t-45.5 224q0 184 105 331.5t272 209.5v-166q-103 -55 -165 -155t-62 -220q0 -116 57 -214.5t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5q0 120 -62 220t-165 155z" />
<glyph unicode="&#xe018;" d="M1025 1200h150q10 0 17.5 -7.5t7.5 -17.5v-1150q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v1150q0 10 7.5 17.5t17.5 7.5zM725 800h150q10 0 17.5 -7.5t7.5 -17.5v-750q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v750 q0 10 7.5 17.5t17.5 7.5zM425 500h150q10 0 17.5 -7.5t7.5 -17.5v-450q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v450q0 10 7.5 17.5t17.5 7.5zM125 300h150q10 0 17.5 -7.5t7.5 -17.5v-250q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5 v250q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe019;" d="M600 1174q33 0 74 -5l38 -152l5 -1q49 -14 94 -39l5 -2l134 80q61 -48 104 -105l-80 -134l3 -5q25 -44 39 -93l1 -6l152 -38q5 -43 5 -73q0 -34 -5 -74l-152 -38l-1 -6q-15 -49 -39 -93l-3 -5l80 -134q-48 -61 -104 -105l-134 81l-5 -3q-44 -25 -94 -39l-5 -2l-38 -151 q-43 -5 -74 -5q-33 0 -74 5l-38 151l-5 2q-49 14 -94 39l-5 3l-134 -81q-60 48 -104 105l80 134l-3 5q-25 45 -38 93l-2 6l-151 38q-6 42 -6 74q0 33 6 73l151 38l2 6q13 48 38 93l3 5l-80 134q47 61 105 105l133 -80l5 2q45 25 94 39l5 1l38 152q43 5 74 5zM600 815 q-89 0 -152 -63t-63 -151.5t63 -151.5t152 -63t152 63t63 151.5t-63 151.5t-152 63z" />
<glyph unicode="&#xe020;" d="M500 1300h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-75h-1100v75q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5zM500 1200v-100h300v100h-300zM1100 900v-800q0 -41 -29.5 -70.5t-70.5 -29.5h-700q-41 0 -70.5 29.5t-29.5 70.5 v800h900zM300 800v-700h100v700h-100zM500 800v-700h100v700h-100zM700 800v-700h100v700h-100zM900 800v-700h100v700h-100z" />
<glyph unicode="&#xe021;" d="M18 618l620 608q8 7 18.5 7t17.5 -7l608 -608q8 -8 5.5 -13t-12.5 -5h-175v-575q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v375h-300v-375q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v575h-175q-10 0 -12.5 5t5.5 13z" />
<glyph unicode="&#xe022;" d="M600 1200v-400q0 -41 29.5 -70.5t70.5 -29.5h300v-650q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v1100q0 21 14.5 35.5t35.5 14.5h450zM1000 800h-250q-21 0 -35.5 14.5t-14.5 35.5v250z" />
<glyph unicode="&#xe023;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM525 900h50q10 0 17.5 -7.5t7.5 -17.5v-275h175q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe024;" d="M1300 0h-538l-41 400h-242l-41 -400h-538l431 1200h209l-21 -300h162l-20 300h208zM515 800l-27 -300h224l-27 300h-170z" />
<glyph unicode="&#xe025;" d="M550 1200h200q21 0 35.5 -14.5t14.5 -35.5v-450h191q20 0 25.5 -11.5t-7.5 -27.5l-327 -400q-13 -16 -32 -16t-32 16l-327 400q-13 16 -7.5 27.5t25.5 11.5h191v450q0 21 14.5 35.5t35.5 14.5zM1125 400h50q10 0 17.5 -7.5t7.5 -17.5v-350q0 -10 -7.5 -17.5t-17.5 -7.5 h-1050q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h50q10 0 17.5 -7.5t7.5 -17.5v-175h900v175q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe026;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM525 900h150q10 0 17.5 -7.5t7.5 -17.5v-275h137q21 0 26 -11.5t-8 -27.5l-223 -275q-13 -16 -32 -16t-32 16l-223 275q-13 16 -8 27.5t26 11.5h137v275q0 10 7.5 17.5t17.5 7.5z " />
<glyph unicode="&#xe027;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM632 914l223 -275q13 -16 8 -27.5t-26 -11.5h-137v-275q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v275h-137q-21 0 -26 11.5t8 27.5l223 275q13 16 32 16 t32 -16z" />
<glyph unicode="&#xe028;" d="M225 1200h750q10 0 19.5 -7t12.5 -17l186 -652q7 -24 7 -49v-425q0 -12 -4 -27t-9 -17q-12 -6 -37 -6h-1100q-12 0 -27 4t-17 8q-6 13 -6 38l1 425q0 25 7 49l185 652q3 10 12.5 17t19.5 7zM878 1000h-556q-10 0 -19 -7t-11 -18l-87 -450q-2 -11 4 -18t16 -7h150 q10 0 19.5 -7t11.5 -17l38 -152q2 -10 11.5 -17t19.5 -7h250q10 0 19.5 7t11.5 17l38 152q2 10 11.5 17t19.5 7h150q10 0 16 7t4 18l-87 450q-2 11 -11 18t-19 7z" />
<glyph unicode="&#xe029;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM540 820l253 -190q17 -12 17 -30t-17 -30l-253 -190q-16 -12 -28 -6.5t-12 26.5v400q0 21 12 26.5t28 -6.5z" />
<glyph unicode="&#xe030;" d="M947 1060l135 135q7 7 12.5 5t5.5 -13v-362q0 -10 -7.5 -17.5t-17.5 -7.5h-362q-11 0 -13 5.5t5 12.5l133 133q-109 76 -238 76q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5h150q0 -117 -45.5 -224 t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5q192 0 347 -117z" />
<glyph unicode="&#xe031;" d="M947 1060l135 135q7 7 12.5 5t5.5 -13v-361q0 -11 -7.5 -18.5t-18.5 -7.5h-361q-11 0 -13 5.5t5 12.5l134 134q-110 75 -239 75q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5h-150q0 117 45.5 224t123 184.5t184.5 123t224 45.5q192 0 347 -117zM1027 600h150 q0 -117 -45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5q-192 0 -348 118l-134 -134q-7 -8 -12.5 -5.5t-5.5 12.5v360q0 11 7.5 18.5t18.5 7.5h360q10 0 12.5 -5.5t-5.5 -12.5l-133 -133q110 -76 240 -76q116 0 214.5 57t155.5 155.5t57 214.5z" />
<glyph unicode="&#xe032;" d="M125 1200h1050q10 0 17.5 -7.5t7.5 -17.5v-1150q0 -10 -7.5 -17.5t-17.5 -7.5h-1050q-10 0 -17.5 7.5t-7.5 17.5v1150q0 10 7.5 17.5t17.5 7.5zM1075 1000h-850q-10 0 -17.5 -7.5t-7.5 -17.5v-850q0 -10 7.5 -17.5t17.5 -7.5h850q10 0 17.5 7.5t7.5 17.5v850 q0 10 -7.5 17.5t-17.5 7.5zM325 900h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 900h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5zM325 700h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 700h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5zM325 500h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 500h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5zM325 300h50q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM525 300h450q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-450q-10 0 -17.5 7.5t-7.5 17.5v50 q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe033;" d="M900 800v200q0 83 -58.5 141.5t-141.5 58.5h-300q-82 0 -141 -59t-59 -141v-200h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-600q0 -41 29.5 -70.5t70.5 -29.5h900q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-100zM400 800v150q0 21 15 35.5t35 14.5h200 q20 0 35 -14.5t15 -35.5v-150h-300z" />
<glyph unicode="&#xe034;" d="M125 1100h50q10 0 17.5 -7.5t7.5 -17.5v-1075h-100v1075q0 10 7.5 17.5t17.5 7.5zM1075 1052q4 0 9 -2q16 -6 16 -23v-421q0 -6 -3 -12q-33 -59 -66.5 -99t-65.5 -58t-56.5 -24.5t-52.5 -6.5q-26 0 -57.5 6.5t-52.5 13.5t-60 21q-41 15 -63 22.5t-57.5 15t-65.5 7.5 q-85 0 -160 -57q-7 -5 -15 -5q-6 0 -11 3q-14 7 -14 22v438q22 55 82 98.5t119 46.5q23 2 43 0.5t43 -7t32.5 -8.5t38 -13t32.5 -11q41 -14 63.5 -21t57 -14t63.5 -7q103 0 183 87q7 8 18 8z" />
<glyph unicode="&#xe035;" d="M600 1175q116 0 227 -49.5t192.5 -131t131 -192.5t49.5 -227v-300q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v300q0 127 -70.5 231.5t-184.5 161.5t-245 57t-245 -57t-184.5 -161.5t-70.5 -231.5v-300q0 -10 -7.5 -17.5t-17.5 -7.5h-50 q-10 0 -17.5 7.5t-7.5 17.5v300q0 116 49.5 227t131 192.5t192.5 131t227 49.5zM220 500h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460q0 8 6 14t14 6zM820 500h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14v460 q0 8 6 14t14 6z" />
<glyph unicode="&#xe036;" d="M321 814l258 172q9 6 15 2.5t6 -13.5v-750q0 -10 -6 -13.5t-15 2.5l-258 172q-21 14 -46 14h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h250q25 0 46 14zM900 668l120 120q7 7 17 7t17 -7l34 -34q7 -7 7 -17t-7 -17l-120 -120l120 -120q7 -7 7 -17 t-7 -17l-34 -34q-7 -7 -17 -7t-17 7l-120 119l-120 -119q-7 -7 -17 -7t-17 7l-34 34q-7 7 -7 17t7 17l119 120l-119 120q-7 7 -7 17t7 17l34 34q7 8 17 8t17 -8z" />
<glyph unicode="&#xe037;" d="M321 814l258 172q9 6 15 2.5t6 -13.5v-750q0 -10 -6 -13.5t-15 2.5l-258 172q-21 14 -46 14h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h250q25 0 46 14zM766 900h4q10 -1 16 -10q96 -129 96 -290q0 -154 -90 -281q-6 -9 -17 -10l-3 -1q-9 0 -16 6 l-29 23q-7 7 -8.5 16.5t4.5 17.5q72 103 72 229q0 132 -78 238q-6 8 -4.5 18t9.5 17l29 22q7 5 15 5z" />
<glyph unicode="&#xe038;" d="M967 1004h3q11 -1 17 -10q135 -179 135 -396q0 -105 -34 -206.5t-98 -185.5q-7 -9 -17 -10h-3q-9 0 -16 6l-42 34q-8 6 -9 16t5 18q111 150 111 328q0 90 -29.5 176t-84.5 157q-6 9 -5 19t10 16l42 33q7 5 15 5zM321 814l258 172q9 6 15 2.5t6 -13.5v-750q0 -10 -6 -13.5 t-15 2.5l-258 172q-21 14 -46 14h-250q-10 0 -17.5 7.5t-7.5 17.5v350q0 10 7.5 17.5t17.5 7.5h250q25 0 46 14zM766 900h4q10 -1 16 -10q96 -129 96 -290q0 -154 -90 -281q-6 -9 -17 -10l-3 -1q-9 0 -16 6l-29 23q-7 7 -8.5 16.5t4.5 17.5q72 103 72 229q0 132 -78 238 q-6 8 -4.5 18.5t9.5 16.5l29 22q7 5 15 5z" />
<glyph unicode="&#xe039;" d="M500 900h100v-100h-100v-100h-400v-100h-100v600h500v-300zM1200 700h-200v-100h200v-200h-300v300h-200v300h-100v200h600v-500zM100 1100v-300h300v300h-300zM800 1100v-300h300v300h-300zM300 900h-100v100h100v-100zM1000 900h-100v100h100v-100zM300 500h200v-500 h-500v500h200v100h100v-100zM800 300h200v-100h-100v-100h-200v100h-100v100h100v200h-200v100h300v-300zM100 400v-300h300v300h-300zM300 200h-100v100h100v-100zM1200 200h-100v100h100v-100zM700 0h-100v100h100v-100zM1200 0h-300v100h300v-100z" />
<glyph unicode="&#xe040;" d="M100 200h-100v1000h100v-1000zM300 200h-100v1000h100v-1000zM700 200h-200v1000h200v-1000zM900 200h-100v1000h100v-1000zM1200 200h-200v1000h200v-1000zM400 0h-300v100h300v-100zM600 0h-100v91h100v-91zM800 0h-100v91h100v-91zM1100 0h-200v91h200v-91z" />
<glyph unicode="&#xe041;" d="M500 1200l682 -682q8 -8 8 -18t-8 -18l-464 -464q-8 -8 -18 -8t-18 8l-682 682l1 475q0 10 7.5 17.5t17.5 7.5h474zM319.5 1024.5q-29.5 29.5 -71 29.5t-71 -29.5t-29.5 -71.5t29.5 -71.5t71 -29.5t71 29.5t29.5 71.5t-29.5 71.5z" />
<glyph unicode="&#xe042;" d="M500 1200l682 -682q8 -8 8 -18t-8 -18l-464 -464q-8 -8 -18 -8t-18 8l-682 682l1 475q0 10 7.5 17.5t17.5 7.5h474zM800 1200l682 -682q8 -8 8 -18t-8 -18l-464 -464q-8 -8 -18 -8t-18 8l-56 56l424 426l-700 700h150zM319.5 1024.5q-29.5 29.5 -71 29.5t-71 -29.5 t-29.5 -71.5t29.5 -71.5t71 -29.5t71 29.5t29.5 71.5t-29.5 71.5z" />
<glyph unicode="&#xe043;" d="M300 1200h825q75 0 75 -75v-900q0 -25 -18 -43l-64 -64q-8 -8 -13 -5.5t-5 12.5v950q0 10 -7.5 17.5t-17.5 7.5h-700q-25 0 -43 -18l-64 -64q-8 -8 -5.5 -13t12.5 -5h700q10 0 17.5 -7.5t7.5 -17.5v-950q0 -10 -7.5 -17.5t-17.5 -7.5h-850q-10 0 -17.5 7.5t-7.5 17.5v975 q0 25 18 43l139 139q18 18 43 18z" />
<glyph unicode="&#xe044;" d="M250 1200h800q21 0 35.5 -14.5t14.5 -35.5v-1150l-450 444l-450 -445v1151q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe045;" d="M822 1200h-444q-11 0 -19 -7.5t-9 -17.5l-78 -301q-7 -24 7 -45l57 -108q6 -9 17.5 -15t21.5 -6h450q10 0 21.5 6t17.5 15l62 108q14 21 7 45l-83 301q-1 10 -9 17.5t-19 7.5zM1175 800h-150q-10 0 -21 -6.5t-15 -15.5l-78 -156q-4 -9 -15 -15.5t-21 -6.5h-550 q-10 0 -21 6.5t-15 15.5l-78 156q-4 9 -15 15.5t-21 6.5h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-650q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h750q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5 t7.5 17.5v650q0 10 -7.5 17.5t-17.5 7.5zM850 200h-500q-10 0 -19.5 -7t-11.5 -17l-38 -152q-2 -10 3.5 -17t15.5 -7h600q10 0 15.5 7t3.5 17l-38 152q-2 10 -11.5 17t-19.5 7z" />
<glyph unicode="&#xe046;" d="M500 1100h200q56 0 102.5 -20.5t72.5 -50t44 -59t25 -50.5l6 -20h150q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5h150q2 8 6.5 21.5t24 48t45 61t72 48t102.5 21.5zM900 800v-100 h100v100h-100zM600 730q-95 0 -162.5 -67.5t-67.5 -162.5t67.5 -162.5t162.5 -67.5t162.5 67.5t67.5 162.5t-67.5 162.5t-162.5 67.5zM600 603q43 0 73 -30t30 -73t-30 -73t-73 -30t-73 30t-30 73t30 73t73 30z" />
<glyph unicode="&#xe047;" d="M681 1199l385 -998q20 -50 60 -92q18 -19 36.5 -29.5t27.5 -11.5l10 -2v-66h-417v66q53 0 75 43.5t5 88.5l-82 222h-391q-58 -145 -92 -234q-11 -34 -6.5 -57t25.5 -37t46 -20t55 -6v-66h-365v66q56 24 84 52q12 12 25 30.5t20 31.5l7 13l399 1006h93zM416 521h340 l-162 457z" />
<glyph unicode="&#xe048;" d="M753 641q5 -1 14.5 -4.5t36 -15.5t50.5 -26.5t53.5 -40t50.5 -54.5t35.5 -70t14.5 -87q0 -67 -27.5 -125.5t-71.5 -97.5t-98.5 -66.5t-108.5 -40.5t-102 -13h-500v89q41 7 70.5 32.5t29.5 65.5v827q0 24 -0.5 34t-3.5 24t-8.5 19.5t-17 13.5t-28 12.5t-42.5 11.5v71 l471 -1q57 0 115.5 -20.5t108 -57t80.5 -94t31 -124.5q0 -51 -15.5 -96.5t-38 -74.5t-45 -50.5t-38.5 -30.5zM400 700h139q78 0 130.5 48.5t52.5 122.5q0 41 -8.5 70.5t-29.5 55.5t-62.5 39.5t-103.5 13.5h-118v-350zM400 200h216q80 0 121 50.5t41 130.5q0 90 -62.5 154.5 t-156.5 64.5h-159v-400z" />
<glyph unicode="&#xe049;" d="M877 1200l2 -57q-83 -19 -116 -45.5t-40 -66.5l-132 -839q-9 -49 13 -69t96 -26v-97h-500v97q186 16 200 98l173 832q3 17 3 30t-1.5 22.5t-9 17.5t-13.5 12.5t-21.5 10t-26 8.5t-33.5 10q-13 3 -19 5v57h425z" />
<glyph unicode="&#xe050;" d="M1300 900h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-200v-850q0 -22 25 -34.5t50 -13.5l25 -2v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v850h-200q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h1000v-300zM175 1000h-75v-800h75l-125 -167l-125 167h75v800h-75l125 167z" />
<glyph unicode="&#xe051;" d="M1100 900h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-200v-650q0 -22 25 -34.5t50 -13.5l25 -2v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v650h-200q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h1000v-300zM1167 50l-167 -125v75h-800v-75l-167 125l167 125v-75h800v75z" />
<glyph unicode="&#xe052;" d="M50 1100h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 800h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM50 500h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe053;" d="M250 1100h700q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 800h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM250 500h700q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe054;" d="M500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000 q-21 0 -35.5 14.5t-14.5 35.5zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5zM0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100 q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5z" />
<glyph unicode="&#xe055;" d="M50 1100h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 800h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM50 500h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe056;" d="M50 1100h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 1100h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM50 800h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 800h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 500h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 500h800q21 0 35.5 -14.5t14.5 -35.5v-100 q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM350 200h800 q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe057;" d="M400 0h-100v1100h100v-1100zM550 1100h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM550 800h500q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-500 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM267 550l-167 -125v75h-200v100h200v75zM550 500h300q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM550 200h600 q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe058;" d="M50 1100h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM900 0h-100v1100h100v-1100zM50 800h500q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-500 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM1100 600h200v-100h-200v-75l-167 125l167 125v-75zM50 500h300q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5zM50 200h600 q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-600q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe059;" d="M75 1000h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53v650q0 31 22 53t53 22zM1200 300l-300 300l300 300v-600z" />
<glyph unicode="&#xe060;" d="M44 1100h1112q18 0 31 -13t13 -31v-1012q0 -18 -13 -31t-31 -13h-1112q-18 0 -31 13t-13 31v1012q0 18 13 31t31 13zM100 1000v-737l247 182l298 -131l-74 156l293 318l236 -288v500h-1000zM342 884q56 0 95 -39t39 -94.5t-39 -95t-95 -39.5t-95 39.5t-39 95t39 94.5 t95 39z" />
<glyph unicode="&#xe062;" d="M648 1169q117 0 216 -60t156.5 -161t57.5 -218q0 -115 -70 -258q-69 -109 -158 -225.5t-143 -179.5l-54 -62q-9 8 -25.5 24.5t-63.5 67.5t-91 103t-98.5 128t-95.5 148q-60 132 -60 249q0 88 34 169.5t91.5 142t137 96.5t166.5 36zM652.5 974q-91.5 0 -156.5 -65 t-65 -157t65 -156.5t156.5 -64.5t156.5 64.5t65 156.5t-65 157t-156.5 65z" />
<glyph unicode="&#xe063;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 173v854q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57z" />
<glyph unicode="&#xe064;" d="M554 1295q21 -72 57.5 -143.5t76 -130t83 -118t82.5 -117t70 -116t49.5 -126t18.5 -136.5q0 -71 -25.5 -135t-68.5 -111t-99 -82t-118.5 -54t-125.5 -23q-84 5 -161.5 34t-139.5 78.5t-99 125t-37 164.5q0 69 18 136.5t49.5 126.5t69.5 116.5t81.5 117.5t83.5 119 t76.5 131t58.5 143zM344 710q-23 -33 -43.5 -70.5t-40.5 -102.5t-17 -123q1 -37 14.5 -69.5t30 -52t41 -37t38.5 -24.5t33 -15q21 -7 32 -1t13 22l6 34q2 10 -2.5 22t-13.5 19q-5 4 -14 12t-29.5 40.5t-32.5 73.5q-26 89 6 271q2 11 -6 11q-8 1 -15 -10z" />
<glyph unicode="&#xe065;" d="M1000 1013l108 115q2 1 5 2t13 2t20.5 -1t25 -9.5t28.5 -21.5q22 -22 27 -43t0 -32l-6 -10l-108 -115zM350 1100h400q50 0 105 -13l-187 -187h-368q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v182l200 200v-332 q0 -165 -93.5 -257.5t-256.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5zM1009 803l-362 -362l-161 -50l55 170l355 355z" />
<glyph unicode="&#xe066;" d="M350 1100h361q-164 -146 -216 -200h-195q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-103q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5z M824 1073l339 -301q8 -7 8 -17.5t-8 -17.5l-340 -306q-7 -6 -12.5 -4t-6.5 11v203q-26 1 -54.5 0t-78.5 -7.5t-92 -17.5t-86 -35t-70 -57q10 59 33 108t51.5 81.5t65 58.5t68.5 40.5t67 24.5t56 13.5t40 4.5v210q1 10 6.5 12.5t13.5 -4.5z" />
<glyph unicode="&#xe067;" d="M350 1100h350q60 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-219q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5z M643 639l395 395q7 7 17.5 7t17.5 -7l101 -101q7 -7 7 -17.5t-7 -17.5l-531 -532q-7 -7 -17.5 -7t-17.5 7l-248 248q-7 7 -7 17.5t7 17.5l101 101q7 7 17.5 7t17.5 -7l111 -111q8 -7 18 -7t18 7z" />
<glyph unicode="&#xe068;" d="M318 918l264 264q8 8 18 8t18 -8l260 -264q7 -8 4.5 -13t-12.5 -5h-170v-200h200v173q0 10 5 12t13 -5l264 -260q8 -7 8 -17.5t-8 -17.5l-264 -265q-8 -7 -13 -5t-5 12v173h-200v-200h170q10 0 12.5 -5t-4.5 -13l-260 -264q-8 -8 -18 -8t-18 8l-264 264q-8 8 -5.5 13 t12.5 5h175v200h-200v-173q0 -10 -5 -12t-13 5l-264 265q-8 7 -8 17.5t8 17.5l264 260q8 7 13 5t5 -12v-173h200v200h-175q-10 0 -12.5 5t5.5 13z" />
<glyph unicode="&#xe069;" d="M250 1100h100q21 0 35.5 -14.5t14.5 -35.5v-438l464 453q15 14 25.5 10t10.5 -25v-1000q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe070;" d="M50 1100h100q21 0 35.5 -14.5t14.5 -35.5v-438l464 453q15 14 25.5 10t10.5 -25v-438l464 453q15 14 25.5 10t10.5 -25v-1000q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5 t-14.5 35.5v1000q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe071;" d="M1200 1050v-1000q0 -21 -10.5 -25t-25.5 10l-464 453v-438q0 -21 -10.5 -25t-25.5 10l-492 480q-15 14 -15 35t15 35l492 480q15 14 25.5 10t10.5 -25v-438l464 453q15 14 25.5 10t10.5 -25z" />
<glyph unicode="&#xe072;" d="M243 1074l814 -498q18 -11 18 -26t-18 -26l-814 -498q-18 -11 -30.5 -4t-12.5 28v1000q0 21 12.5 28t30.5 -4z" />
<glyph unicode="&#xe073;" d="M250 1000h200q21 0 35.5 -14.5t14.5 -35.5v-800q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v800q0 21 14.5 35.5t35.5 14.5zM650 1000h200q21 0 35.5 -14.5t14.5 -35.5v-800q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v800 q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe074;" d="M1100 950v-800q0 -21 -14.5 -35.5t-35.5 -14.5h-800q-21 0 -35.5 14.5t-14.5 35.5v800q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5z" />
<glyph unicode="&#xe075;" d="M500 612v438q0 21 10.5 25t25.5 -10l492 -480q15 -14 15 -35t-15 -35l-492 -480q-15 -14 -25.5 -10t-10.5 25v438l-464 -453q-15 -14 -25.5 -10t-10.5 25v1000q0 21 10.5 25t25.5 -10z" />
<glyph unicode="&#xe076;" d="M1048 1102l100 1q20 0 35 -14.5t15 -35.5l5 -1000q0 -21 -14.5 -35.5t-35.5 -14.5l-100 -1q-21 0 -35.5 14.5t-14.5 35.5l-2 437l-463 -454q-14 -15 -24.5 -10.5t-10.5 25.5l-2 437l-462 -455q-15 -14 -25.5 -9.5t-10.5 24.5l-5 1000q0 21 10.5 25.5t25.5 -10.5l466 -450 l-2 438q0 20 10.5 24.5t25.5 -9.5l466 -451l-2 438q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe077;" d="M850 1100h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-464 -453q-15 -14 -25.5 -10t-10.5 25v1000q0 21 10.5 25t25.5 -10l464 -453v438q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe078;" d="M686 1081l501 -540q15 -15 10.5 -26t-26.5 -11h-1042q-22 0 -26.5 11t10.5 26l501 540q15 15 36 15t36 -15zM150 400h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe079;" d="M885 900l-352 -353l352 -353l-197 -198l-552 552l552 550z" />
<glyph unicode="&#xe080;" d="M1064 547l-551 -551l-198 198l353 353l-353 353l198 198z" />
<glyph unicode="&#xe081;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM650 900h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-150h-150 q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -21 14.5 -35.5t35.5 -14.5h150v-150q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v150h150q21 0 35.5 14.5t14.5 35.5v100q0 21 -14.5 35.5t-35.5 14.5h-150v150q0 21 -14.5 35.5t-35.5 14.5z" />
<glyph unicode="&#xe082;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM850 700h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -21 14.5 -35.5 t35.5 -14.5h500q21 0 35.5 14.5t14.5 35.5v100q0 21 -14.5 35.5t-35.5 14.5z" />
<glyph unicode="&#xe083;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM741.5 913q-12.5 0 -21.5 -9l-120 -120l-120 120q-9 9 -21.5 9 t-21.5 -9l-141 -141q-9 -9 -9 -21.5t9 -21.5l120 -120l-120 -120q-9 -9 -9 -21.5t9 -21.5l141 -141q9 -9 21.5 -9t21.5 9l120 120l120 -120q9 -9 21.5 -9t21.5 9l141 141q9 9 9 21.5t-9 21.5l-120 120l120 120q9 9 9 21.5t-9 21.5l-141 141q-9 9 -21.5 9z" />
<glyph unicode="&#xe084;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM546 623l-84 85q-7 7 -17.5 7t-18.5 -7l-139 -139q-7 -8 -7 -18t7 -18 l242 -241q7 -8 17.5 -8t17.5 8l375 375q7 7 7 17.5t-7 18.5l-139 139q-7 7 -17.5 7t-17.5 -7z" />
<glyph unicode="&#xe085;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM588 941q-29 0 -59 -5.5t-63 -20.5t-58 -38.5t-41.5 -63t-16.5 -89.5 q0 -25 20 -25h131q30 -5 35 11q6 20 20.5 28t45.5 8q20 0 31.5 -10.5t11.5 -28.5q0 -23 -7 -34t-26 -18q-1 0 -13.5 -4t-19.5 -7.5t-20 -10.5t-22 -17t-18.5 -24t-15.5 -35t-8 -46q-1 -8 5.5 -16.5t20.5 -8.5h173q7 0 22 8t35 28t37.5 48t29.5 74t12 100q0 47 -17 83 t-42.5 57t-59.5 34.5t-64 18t-59 4.5zM675 400h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe086;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM675 1000h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5 t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5zM675 700h-250q-10 0 -17.5 -7.5t-7.5 -17.5v-50q0 -10 7.5 -17.5t17.5 -7.5h75v-200h-75q-10 0 -17.5 -7.5t-7.5 -17.5v-50q0 -10 7.5 -17.5t17.5 -7.5h350q10 0 17.5 7.5t7.5 17.5v50q0 10 -7.5 17.5 t-17.5 7.5h-75v275q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe087;" d="M525 1200h150q10 0 17.5 -7.5t7.5 -17.5v-194q103 -27 178.5 -102.5t102.5 -178.5h194q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-194q-27 -103 -102.5 -178.5t-178.5 -102.5v-194q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v194 q-103 27 -178.5 102.5t-102.5 178.5h-194q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h194q27 103 102.5 178.5t178.5 102.5v194q0 10 7.5 17.5t17.5 7.5zM700 893v-168q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v168q-68 -23 -119 -74 t-74 -119h168q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-168q23 -68 74 -119t119 -74v168q0 10 7.5 17.5t17.5 7.5h150q10 0 17.5 -7.5t7.5 -17.5v-168q68 23 119 74t74 119h-168q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h168 q-23 68 -74 119t-119 74z" />
<glyph unicode="&#xe088;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM759 823l64 -64q7 -7 7 -17.5t-7 -17.5l-124 -124l124 -124q7 -7 7 -17.5t-7 -17.5l-64 -64q-7 -7 -17.5 -7t-17.5 7l-124 124l-124 -124q-7 -7 -17.5 -7t-17.5 7l-64 64 q-7 7 -7 17.5t7 17.5l124 124l-124 124q-7 7 -7 17.5t7 17.5l64 64q7 7 17.5 7t17.5 -7l124 -124l124 124q7 7 17.5 7t17.5 -7z" />
<glyph unicode="&#xe089;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5t57 -214.5 t155.5 -155.5t214.5 -57t214.5 57t155.5 155.5t57 214.5t-57 214.5t-155.5 155.5t-214.5 57zM782 788l106 -106q7 -7 7 -17.5t-7 -17.5l-320 -321q-8 -7 -18 -7t-18 7l-202 203q-8 7 -8 17.5t8 17.5l106 106q7 8 17.5 8t17.5 -8l79 -79l197 197q7 7 17.5 7t17.5 -7z" />
<glyph unicode="&#xe090;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM600 1027q-116 0 -214.5 -57t-155.5 -155.5t-57 -214.5q0 -120 65 -225 l587 587q-105 65 -225 65zM965 819l-584 -584q104 -62 219 -62q116 0 214.5 57t155.5 155.5t57 214.5q0 115 -62 219z" />
<glyph unicode="&#xe091;" d="M39 582l522 427q16 13 27.5 8t11.5 -26v-291h550q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-550v-291q0 -21 -11.5 -26t-27.5 8l-522 427q-16 13 -16 32t16 32z" />
<glyph unicode="&#xe092;" d="M639 1009l522 -427q16 -13 16 -32t-16 -32l-522 -427q-16 -13 -27.5 -8t-11.5 26v291h-550q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h550v291q0 21 11.5 26t27.5 -8z" />
<glyph unicode="&#xe093;" d="M682 1161l427 -522q13 -16 8 -27.5t-26 -11.5h-291v-550q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v550h-291q-21 0 -26 11.5t8 27.5l427 522q13 16 32 16t32 -16z" />
<glyph unicode="&#xe094;" d="M550 1200h200q21 0 35.5 -14.5t14.5 -35.5v-550h291q21 0 26 -11.5t-8 -27.5l-427 -522q-13 -16 -32 -16t-32 16l-427 522q-13 16 -8 27.5t26 11.5h291v550q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe095;" d="M639 1109l522 -427q16 -13 16 -32t-16 -32l-522 -427q-16 -13 -27.5 -8t-11.5 26v291q-94 -2 -182 -20t-170.5 -52t-147 -92.5t-100.5 -135.5q5 105 27 193.5t67.5 167t113 135t167 91.5t225.5 42v262q0 21 11.5 26t27.5 -8z" />
<glyph unicode="&#xe096;" d="M850 1200h300q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -10.5 -25t-24.5 10l-94 94l-249 -249q-8 -7 -18 -7t-18 7l-106 106q-7 8 -7 18t7 18l249 249l-94 94q-14 14 -10 24.5t25 10.5zM350 0h-300q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 10.5 25t24.5 -10l94 -94l249 249 q8 7 18 7t18 -7l106 -106q7 -8 7 -18t-7 -18l-249 -249l94 -94q14 -14 10 -24.5t-25 -10.5z" />
<glyph unicode="&#xe097;" d="M1014 1120l106 -106q7 -8 7 -18t-7 -18l-249 -249l94 -94q14 -14 10 -24.5t-25 -10.5h-300q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 10.5 25t24.5 -10l94 -94l249 249q8 7 18 7t18 -7zM250 600h300q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -10.5 -25t-24.5 10l-94 94 l-249 -249q-8 -7 -18 -7t-18 7l-106 106q-7 8 -7 18t7 18l249 249l-94 94q-14 14 -10 24.5t25 10.5z" />
<glyph unicode="&#xe101;" d="M600 1177q117 0 224 -45.5t184.5 -123t123 -184.5t45.5 -224t-45.5 -224t-123 -184.5t-184.5 -123t-224 -45.5t-224 45.5t-184.5 123t-123 184.5t-45.5 224t45.5 224t123 184.5t184.5 123t224 45.5zM704 900h-208q-20 0 -32 -14.5t-8 -34.5l58 -302q4 -20 21.5 -34.5 t37.5 -14.5h54q20 0 37.5 14.5t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5zM675 400h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe102;" d="M260 1200q9 0 19 -2t15 -4l5 -2q22 -10 44 -23l196 -118q21 -13 36 -24q29 -21 37 -12q11 13 49 35l196 118q22 13 45 23q17 7 38 7q23 0 47 -16.5t37 -33.5l13 -16q14 -21 18 -45l25 -123l8 -44q1 -9 8.5 -14.5t17.5 -5.5h61q10 0 17.5 -7.5t7.5 -17.5v-50 q0 -10 -7.5 -17.5t-17.5 -7.5h-50q-10 0 -17.5 -7.5t-7.5 -17.5v-175h-400v300h-200v-300h-400v175q0 10 -7.5 17.5t-17.5 7.5h-50q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5h61q11 0 18 3t7 8q0 4 9 52l25 128q5 25 19 45q2 3 5 7t13.5 15t21.5 19.5t26.5 15.5 t29.5 7zM915 1079l-166 -162q-7 -7 -5 -12t12 -5h219q10 0 15 7t2 17l-51 149q-3 10 -11 12t-15 -6zM463 917l-177 157q-8 7 -16 5t-11 -12l-51 -143q-3 -10 2 -17t15 -7h231q11 0 12.5 5t-5.5 12zM500 0h-375q-10 0 -17.5 7.5t-7.5 17.5v375h400v-400zM1100 400v-375 q0 -10 -7.5 -17.5t-17.5 -7.5h-375v400h400z" />
<glyph unicode="&#xe103;" d="M1165 1190q8 3 21 -6.5t13 -17.5q-2 -178 -24.5 -323.5t-55.5 -245.5t-87 -174.5t-102.5 -118.5t-118 -68.5t-118.5 -33t-120 -4.5t-105 9.5t-90 16.5q-61 12 -78 11q-4 1 -12.5 0t-34 -14.5t-52.5 -40.5l-153 -153q-26 -24 -37 -14.5t-11 43.5q0 64 42 102q8 8 50.5 45 t66.5 58q19 17 35 47t13 61q-9 55 -10 102.5t7 111t37 130t78 129.5q39 51 80 88t89.5 63.5t94.5 45t113.5 36t129 31t157.5 37t182 47.5zM1116 1098q-8 9 -22.5 -3t-45.5 -50q-38 -47 -119 -103.5t-142 -89.5l-62 -33q-56 -30 -102 -57t-104 -68t-102.5 -80.5t-85.5 -91 t-64 -104.5q-24 -56 -31 -86t2 -32t31.5 17.5t55.5 59.5q25 30 94 75.5t125.5 77.5t147.5 81q70 37 118.5 69t102 79.5t99 111t86.5 148.5q22 50 24 60t-6 19z" />
<glyph unicode="&#xe104;" d="M653 1231q-39 -67 -54.5 -131t-10.5 -114.5t24.5 -96.5t47.5 -80t63.5 -62.5t68.5 -46.5t65 -30q-4 7 -17.5 35t-18.5 39.5t-17 39.5t-17 43t-13 42t-9.5 44.5t-2 42t4 43t13.5 39t23 38.5q96 -42 165 -107.5t105 -138t52 -156t13 -159t-19 -149.5q-13 -55 -44 -106.5 t-68 -87t-78.5 -64.5t-72.5 -45t-53 -22q-72 -22 -127 -11q-31 6 -13 19q6 3 17 7q13 5 32.5 21t41 44t38.5 63.5t21.5 81.5t-6.5 94.5t-50 107t-104 115.5q10 -104 -0.5 -189t-37 -140.5t-65 -93t-84 -52t-93.5 -11t-95 24.5q-80 36 -131.5 114t-53.5 171q-2 23 0 49.5 t4.5 52.5t13.5 56t27.5 60t46 64.5t69.5 68.5q-8 -53 -5 -102.5t17.5 -90t34 -68.5t44.5 -39t49 -2q31 13 38.5 36t-4.5 55t-29 64.5t-36 75t-26 75.5q-15 85 2 161.5t53.5 128.5t85.5 92.5t93.5 61t81.5 25.5z" />
<glyph unicode="&#xe105;" d="M600 1094q82 0 160.5 -22.5t140 -59t116.5 -82.5t94.5 -95t68 -95t42.5 -82.5t14 -57.5t-14 -57.5t-43 -82.5t-68.5 -95t-94.5 -95t-116.5 -82.5t-140 -59t-159.5 -22.5t-159.5 22.5t-140 59t-116.5 82.5t-94.5 95t-68.5 95t-43 82.5t-14 57.5t14 57.5t42.5 82.5t68 95 t94.5 95t116.5 82.5t140 59t160.5 22.5zM888 829q-15 15 -18 12t5 -22q25 -57 25 -119q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 59 23 114q8 19 4.5 22t-17.5 -12q-70 -69 -160 -184q-13 -16 -15 -40.5t9 -42.5q22 -36 47 -71t70 -82t92.5 -81t113 -58.5t133.5 -24.5 t133.5 24t113 58.5t92.5 81.5t70 81.5t47 70.5q11 18 9 42.5t-14 41.5q-90 117 -163 189zM448 727l-35 -36q-15 -15 -19.5 -38.5t4.5 -41.5q37 -68 93 -116q16 -13 38.5 -11t36.5 17l35 34q14 15 12.5 33.5t-16.5 33.5q-44 44 -89 117q-11 18 -28 20t-32 -12z" />
<glyph unicode="&#xe106;" d="M592 0h-148l31 120q-91 20 -175.5 68.5t-143.5 106.5t-103.5 119t-66.5 110t-22 76q0 21 14 57.5t42.5 82.5t68 95t94.5 95t116.5 82.5t140 59t160.5 22.5q61 0 126 -15l32 121h148zM944 770l47 181q108 -85 176.5 -192t68.5 -159q0 -26 -19.5 -71t-59.5 -102t-93 -112 t-129 -104.5t-158 -75.5l46 173q77 49 136 117t97 131q11 18 9 42.5t-14 41.5q-54 70 -107 130zM310 824q-70 -69 -160 -184q-13 -16 -15 -40.5t9 -42.5q18 -30 39 -60t57 -70.5t74 -73t90 -61t105 -41.5l41 154q-107 18 -178.5 101.5t-71.5 193.5q0 59 23 114q8 19 4.5 22 t-17.5 -12zM448 727l-35 -36q-15 -15 -19.5 -38.5t4.5 -41.5q37 -68 93 -116q16 -13 38.5 -11t36.5 17l12 11l22 86l-3 4q-44 44 -89 117q-11 18 -28 20t-32 -12z" />
<glyph unicode="&#xe107;" d="M-90 100l642 1066q20 31 48 28.5t48 -35.5l642 -1056q21 -32 7.5 -67.5t-50.5 -35.5h-1294q-37 0 -50.5 34t7.5 66zM155 200h345v75q0 10 7.5 17.5t17.5 7.5h150q10 0 17.5 -7.5t7.5 -17.5v-75h345l-445 723zM496 700h208q20 0 32 -14.5t8 -34.5l-58 -252 q-4 -20 -21.5 -34.5t-37.5 -14.5h-54q-20 0 -37.5 14.5t-21.5 34.5l-58 252q-4 20 8 34.5t32 14.5z" />
<glyph unicode="&#xe108;" d="M650 1200q62 0 106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -93 100 -113v-64q0 -21 -13 -29t-32 1l-205 128l-205 -128q-19 -9 -32 -1t-13 29v64q0 20 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5v41 q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44z" />
<glyph unicode="&#xe109;" d="M850 1200h100q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-150h-1100v150q0 21 14.5 35.5t35.5 14.5h50v50q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-50h500v50q0 21 14.5 35.5t35.5 14.5zM1100 800v-750q0 -21 -14.5 -35.5 t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v750h1100zM100 600v-100h100v100h-100zM300 600v-100h100v100h-100zM500 600v-100h100v100h-100zM700 600v-100h100v100h-100zM900 600v-100h100v100h-100zM100 400v-100h100v100h-100zM300 400v-100h100v100h-100zM500 400 v-100h100v100h-100zM700 400v-100h100v100h-100zM900 400v-100h100v100h-100zM100 200v-100h100v100h-100zM300 200v-100h100v100h-100zM500 200v-100h100v100h-100zM700 200v-100h100v100h-100zM900 200v-100h100v100h-100z" />
<glyph unicode="&#xe110;" d="M1135 1165l249 -230q15 -14 15 -35t-15 -35l-249 -230q-14 -14 -24.5 -10t-10.5 25v150h-159l-600 -600h-291q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h209l600 600h241v150q0 21 10.5 25t24.5 -10zM522 819l-141 -141l-122 122h-209q-21 0 -35.5 14.5 t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h291zM1135 565l249 -230q15 -14 15 -35t-15 -35l-249 -230q-14 -14 -24.5 -10t-10.5 25v150h-241l-181 181l141 141l122 -122h159v150q0 21 10.5 25t24.5 -10z" />
<glyph unicode="&#xe111;" d="M100 1100h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5v600q0 41 29.5 70.5t70.5 29.5z" />
<glyph unicode="&#xe112;" d="M150 1200h200q21 0 35.5 -14.5t14.5 -35.5v-250h-300v250q0 21 14.5 35.5t35.5 14.5zM850 1200h200q21 0 35.5 -14.5t14.5 -35.5v-250h-300v250q0 21 14.5 35.5t35.5 14.5zM1100 800v-300q0 -41 -3 -77.5t-15 -89.5t-32 -96t-58 -89t-89 -77t-129 -51t-174 -20t-174 20 t-129 51t-89 77t-58 89t-32 96t-15 89.5t-3 77.5v300h300v-250v-27v-42.5t1.5 -41t5 -38t10 -35t16.5 -30t25.5 -24.5t35 -19t46.5 -12t60 -4t60 4.5t46.5 12.5t35 19.5t25 25.5t17 30.5t10 35t5 38t2 40.5t-0.5 42v25v250h300z" />
<glyph unicode="&#xe113;" d="M1100 411l-198 -199l-353 353l-353 -353l-197 199l551 551z" />
<glyph unicode="&#xe114;" d="M1101 789l-550 -551l-551 551l198 199l353 -353l353 353z" />
<glyph unicode="&#xe115;" d="M404 1000h746q21 0 35.5 -14.5t14.5 -35.5v-551h150q21 0 25 -10.5t-10 -24.5l-230 -249q-14 -15 -35 -15t-35 15l-230 249q-14 14 -10 24.5t25 10.5h150v401h-381zM135 984l230 -249q14 -14 10 -24.5t-25 -10.5h-150v-400h385l215 -200h-750q-21 0 -35.5 14.5 t-14.5 35.5v550h-150q-21 0 -25 10.5t10 24.5l230 249q14 15 35 15t35 -15z" />
<glyph unicode="&#xe116;" d="M56 1200h94q17 0 31 -11t18 -27l38 -162h896q24 0 39 -18.5t10 -42.5l-100 -475q-5 -21 -27 -42.5t-55 -21.5h-633l48 -200h535q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-50q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v50h-300v-50 q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v50h-31q-18 0 -32.5 10t-20.5 19l-5 10l-201 961h-54q-20 0 -35 14.5t-15 35.5t15 35.5t35 14.5z" />
<glyph unicode="&#xe117;" d="M1200 1000v-100h-1200v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500zM0 800h1200v-800h-1200v800z" />
<glyph unicode="&#xe118;" d="M200 800l-200 -400v600h200q0 41 29.5 70.5t70.5 29.5h300q42 0 71 -29.5t29 -70.5h500v-200h-1000zM1500 700l-300 -700h-1200l300 700h1200z" />
<glyph unicode="&#xe119;" d="M635 1184l230 -249q14 -14 10 -24.5t-25 -10.5h-150v-601h150q21 0 25 -10.5t-10 -24.5l-230 -249q-14 -15 -35 -15t-35 15l-230 249q-14 14 -10 24.5t25 10.5h150v601h-150q-21 0 -25 10.5t10 24.5l230 249q14 15 35 15t35 -15z" />
<glyph unicode="&#xe120;" d="M936 864l249 -229q14 -15 14 -35.5t-14 -35.5l-249 -229q-15 -15 -25.5 -10.5t-10.5 24.5v151h-600v-151q0 -20 -10.5 -24.5t-25.5 10.5l-249 229q-14 15 -14 35.5t14 35.5l249 229q15 15 25.5 10.5t10.5 -25.5v-149h600v149q0 21 10.5 25.5t25.5 -10.5z" />
<glyph unicode="&#xe121;" d="M1169 400l-172 732q-5 23 -23 45.5t-38 22.5h-672q-20 0 -38 -20t-23 -41l-172 -739h1138zM1100 300h-1000q-41 0 -70.5 -29.5t-29.5 -70.5v-100q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v100q0 41 -29.5 70.5t-70.5 29.5zM800 100v100h100v-100h-100 zM1000 100v100h100v-100h-100z" />
<glyph unicode="&#xe122;" d="M1150 1100q21 0 35.5 -14.5t14.5 -35.5v-850q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v850q0 21 14.5 35.5t35.5 14.5zM1000 200l-675 200h-38l47 -276q3 -16 -5.5 -20t-29.5 -4h-7h-84q-20 0 -34.5 14t-18.5 35q-55 337 -55 351v250v6q0 16 1 23.5t6.5 14 t17.5 6.5h200l675 250v-850zM0 750v-250q-4 0 -11 0.5t-24 6t-30 15t-24 30t-11 48.5v50q0 26 10.5 46t25 30t29 16t25.5 7z" />
<glyph unicode="&#xe123;" d="M553 1200h94q20 0 29 -10.5t3 -29.5l-18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q19 0 33 -14.5t14 -35t-13 -40.5t-31 -27q-8 -4 -23 -9.5t-65 -19.5t-103 -25t-132.5 -20t-158.5 -9q-57 0 -115 5t-104 12t-88.5 15.5t-73.5 17.5t-54.5 16t-35.5 12l-11 4 q-18 8 -31 28t-13 40.5t14 35t33 14.5h17l118 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3.5 32t28.5 13zM498 110q50 -6 102 -6q53 0 102 6q-12 -49 -39.5 -79.5t-62.5 -30.5t-63 30.5t-39 79.5z" />
<glyph unicode="&#xe124;" d="M800 946l224 78l-78 -224l234 -45l-180 -155l180 -155l-234 -45l78 -224l-224 78l-45 -234l-155 180l-155 -180l-45 234l-224 -78l78 224l-234 45l180 155l-180 155l234 45l-78 224l224 -78l45 234l155 -180l155 180z" />
<glyph unicode="&#xe125;" d="M650 1200h50q40 0 70 -40.5t30 -84.5v-150l-28 -125h328q40 0 70 -40.5t30 -84.5v-100q0 -45 -29 -74l-238 -344q-16 -24 -38 -40.5t-45 -16.5h-250q-7 0 -42 25t-66 50l-31 25h-61q-45 0 -72.5 18t-27.5 57v400q0 36 20 63l145 196l96 198q13 28 37.5 48t51.5 20z M650 1100l-100 -212l-150 -213v-375h100l136 -100h214l250 375v125h-450l50 225v175h-50zM50 800h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v500q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe126;" d="M600 1100h250q23 0 45 -16.5t38 -40.5l238 -344q29 -29 29 -74v-100q0 -44 -30 -84.5t-70 -40.5h-328q28 -118 28 -125v-150q0 -44 -30 -84.5t-70 -40.5h-50q-27 0 -51.5 20t-37.5 48l-96 198l-145 196q-20 27 -20 63v400q0 39 27.5 57t72.5 18h61q124 100 139 100z M50 1000h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v500q0 21 14.5 35.5t35.5 14.5zM636 1000l-136 -100h-100v-375l150 -213l100 -212h50v175l-50 225h450v125l-250 375h-214z" />
<glyph unicode="&#xe127;" d="M356 873l363 230q31 16 53 -6l110 -112q13 -13 13.5 -32t-11.5 -34l-84 -121h302q84 0 138 -38t54 -110t-55 -111t-139 -39h-106l-131 -339q-6 -21 -19.5 -41t-28.5 -20h-342q-7 0 -90 81t-83 94v525q0 17 14 35.5t28 28.5zM400 792v-503l100 -89h293l131 339 q6 21 19.5 41t28.5 20h203q21 0 30.5 25t0.5 50t-31 25h-456h-7h-6h-5.5t-6 0.5t-5 1.5t-5 2t-4 2.5t-4 4t-2.5 4.5q-12 25 5 47l146 183l-86 83zM50 800h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v500 q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe128;" d="M475 1103l366 -230q2 -1 6 -3.5t14 -10.5t18 -16.5t14.5 -20t6.5 -22.5v-525q0 -13 -86 -94t-93 -81h-342q-15 0 -28.5 20t-19.5 41l-131 339h-106q-85 0 -139.5 39t-54.5 111t54 110t138 38h302l-85 121q-11 15 -10.5 34t13.5 32l110 112q22 22 53 6zM370 945l146 -183 q17 -22 5 -47q-2 -2 -3.5 -4.5t-4 -4t-4 -2.5t-5 -2t-5 -1.5t-6 -0.5h-6h-6.5h-6h-475v-100h221q15 0 29 -20t20 -41l130 -339h294l106 89v503l-342 236zM1050 800h100q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5 v500q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe129;" d="M550 1294q72 0 111 -55t39 -139v-106l339 -131q21 -6 41 -19.5t20 -28.5v-342q0 -7 -81 -90t-94 -83h-525q-17 0 -35.5 14t-28.5 28l-9 14l-230 363q-16 31 6 53l112 110q13 13 32 13.5t34 -11.5l121 -84v302q0 84 38 138t110 54zM600 972v203q0 21 -25 30.5t-50 0.5 t-25 -31v-456v-7v-6v-5.5t-0.5 -6t-1.5 -5t-2 -5t-2.5 -4t-4 -4t-4.5 -2.5q-25 -12 -47 5l-183 146l-83 -86l236 -339h503l89 100v293l-339 131q-21 6 -41 19.5t-20 28.5zM450 200h500q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-500 q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe130;" d="M350 1100h500q21 0 35.5 14.5t14.5 35.5v100q0 21 -14.5 35.5t-35.5 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100q0 -21 14.5 -35.5t35.5 -14.5zM600 306v-106q0 -84 -39 -139t-111 -55t-110 54t-38 138v302l-121 -84q-15 -12 -34 -11.5t-32 13.5l-112 110 q-22 22 -6 53l230 363q1 2 3.5 6t10.5 13.5t16.5 17t20 13.5t22.5 6h525q13 0 94 -83t81 -90v-342q0 -15 -20 -28.5t-41 -19.5zM308 900l-236 -339l83 -86l183 146q22 17 47 5q2 -1 4.5 -2.5t4 -4t2.5 -4t2 -5t1.5 -5t0.5 -6v-5.5v-6v-7v-456q0 -22 25 -31t50 0.5t25 30.5 v203q0 15 20 28.5t41 19.5l339 131v293l-89 100h-503z" />
<glyph unicode="&#xe131;" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM914 632l-275 223q-16 13 -27.5 8t-11.5 -26v-137h-275 q-10 0 -17.5 -7.5t-7.5 -17.5v-150q0 -10 7.5 -17.5t17.5 -7.5h275v-137q0 -21 11.5 -26t27.5 8l275 223q16 13 16 32t-16 32z" />
<glyph unicode="&#xe132;" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM561 855l-275 -223q-16 -13 -16 -32t16 -32l275 -223q16 -13 27.5 -8 t11.5 26v137h275q10 0 17.5 7.5t7.5 17.5v150q0 10 -7.5 17.5t-17.5 7.5h-275v137q0 21 -11.5 26t-27.5 -8z" />
<glyph unicode="&#xe133;" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM855 639l-223 275q-13 16 -32 16t-32 -16l-223 -275q-13 -16 -8 -27.5 t26 -11.5h137v-275q0 -10 7.5 -17.5t17.5 -7.5h150q10 0 17.5 7.5t7.5 17.5v275h137q21 0 26 11.5t-8 27.5z" />
<glyph unicode="&#xe134;" d="M600 1178q118 0 225 -45.5t184.5 -123t123 -184.5t45.5 -225t-45.5 -225t-123 -184.5t-184.5 -123t-225 -45.5t-225 45.5t-184.5 123t-123 184.5t-45.5 225t45.5 225t123 184.5t184.5 123t225 45.5zM675 900h-150q-10 0 -17.5 -7.5t-7.5 -17.5v-275h-137q-21 0 -26 -11.5 t8 -27.5l223 -275q13 -16 32 -16t32 16l223 275q13 16 8 27.5t-26 11.5h-137v275q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe135;" d="M600 1176q116 0 222.5 -46t184 -123.5t123.5 -184t46 -222.5t-46 -222.5t-123.5 -184t-184 -123.5t-222.5 -46t-222.5 46t-184 123.5t-123.5 184t-46 222.5t46 222.5t123.5 184t184 123.5t222.5 46zM627 1101q-15 -12 -36.5 -20.5t-35.5 -12t-43 -8t-39 -6.5 q-15 -3 -45.5 0t-45.5 -2q-20 -7 -51.5 -26.5t-34.5 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -91t-29.5 -79q-9 -34 5 -93t8 -87q0 -9 17 -44.5t16 -59.5q12 0 23 -5t23.5 -15t19.5 -14q16 -8 33 -15t40.5 -15t34.5 -12q21 -9 52.5 -32t60 -38t57.5 -11 q7 -15 -3 -34t-22.5 -40t-9.5 -38q13 -21 23 -34.5t27.5 -27.5t36.5 -18q0 -7 -3.5 -16t-3.5 -14t5 -17q104 -2 221 112q30 29 46.5 47t34.5 49t21 63q-13 8 -37 8.5t-36 7.5q-15 7 -49.5 15t-51.5 19q-18 0 -41 -0.5t-43 -1.5t-42 -6.5t-38 -16.5q-51 -35 -66 -12 q-4 1 -3.5 25.5t0.5 25.5q-6 13 -26.5 17.5t-24.5 6.5q1 15 -0.5 30.5t-7 28t-18.5 11.5t-31 -21q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q7 -12 18 -24t21.5 -20.5t20 -15t15.5 -10.5l5 -3q2 12 7.5 30.5t8 34.5t-0.5 32q-3 18 3.5 29 t18 22.5t15.5 24.5q6 14 10.5 35t8 31t15.5 22.5t34 22.5q-6 18 10 36q8 0 24 -1.5t24.5 -1.5t20 4.5t20.5 15.5q-10 23 -31 42.5t-37.5 29.5t-49 27t-43.5 23q0 1 2 8t3 11.5t1.5 10.5t-1 9.5t-4.5 4.5q31 -13 58.5 -14.5t38.5 2.5l12 5q5 28 -9.5 46t-36.5 24t-50 15 t-41 20q-18 -4 -37 0zM613 994q0 -17 8 -42t17 -45t9 -23q-8 1 -39.5 5.5t-52.5 10t-37 16.5q3 11 16 29.5t16 25.5q10 -10 19 -10t14 6t13.5 14.5t16.5 12.5z" />
<glyph unicode="&#xe136;" d="M756 1157q164 92 306 -9l-259 -138l145 -232l251 126q6 -89 -34 -156.5t-117 -110.5q-60 -34 -127 -39.5t-126 16.5l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5t15 37.5l600 599q-34 101 5.5 201.5t135.5 154.5z" />
<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M100 1196h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 1096h-200v-100h200v100zM100 796h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 696h-500v-100h500v100zM100 396h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5v100q0 41 29.5 70.5t70.5 29.5zM1100 296h-300v-100h300v100z " />
<glyph unicode="&#xe138;" d="M150 1200h900q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM700 500v-300l-200 -200v500l-350 500h900z" />
<glyph unicode="&#xe139;" d="M500 1200h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5zM500 1100v-100h200v100h-200zM1200 400v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5v200h1200z" />
<glyph unicode="&#xe140;" d="M50 1200h300q21 0 25 -10.5t-10 -24.5l-94 -94l199 -199q7 -8 7 -18t-7 -18l-106 -106q-8 -7 -18 -7t-18 7l-199 199l-94 -94q-14 -14 -24.5 -10t-10.5 25v300q0 21 14.5 35.5t35.5 14.5zM850 1200h300q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -10.5 -25t-24.5 10l-94 94 l-199 -199q-8 -7 -18 -7t-18 7l-106 106q-7 8 -7 18t7 18l199 199l-94 94q-14 14 -10 24.5t25 10.5zM364 470l106 -106q7 -8 7 -18t-7 -18l-199 -199l94 -94q14 -14 10 -24.5t-25 -10.5h-300q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 10.5 25t24.5 -10l94 -94l199 199 q8 7 18 7t18 -7zM1071 271l94 94q14 14 24.5 10t10.5 -25v-300q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -25 10.5t10 24.5l94 94l-199 199q-7 8 -7 18t7 18l106 106q8 7 18 7t18 -7z" />
<glyph unicode="&#xe141;" d="M596 1192q121 0 231.5 -47.5t190 -127t127 -190t47.5 -231.5t-47.5 -231.5t-127 -190.5t-190 -127t-231.5 -47t-231.5 47t-190.5 127t-127 190.5t-47 231.5t47 231.5t127 190t190.5 127t231.5 47.5zM596 1010q-112 0 -207.5 -55.5t-151 -151t-55.5 -207.5t55.5 -207.5 t151 -151t207.5 -55.5t207.5 55.5t151 151t55.5 207.5t-55.5 207.5t-151 151t-207.5 55.5zM454.5 905q22.5 0 38.5 -16t16 -38.5t-16 -39t-38.5 -16.5t-38.5 16.5t-16 39t16 38.5t38.5 16zM754.5 905q22.5 0 38.5 -16t16 -38.5t-16 -39t-38 -16.5q-14 0 -29 10l-55 -145 q17 -23 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 23 16 39t38.5 16zM345.5 709q22.5 0 38.5 -16t16 -38.5t-16 -38.5t-38.5 -16t-38.5 16t-16 38.5t16 38.5t38.5 16zM854.5 709q22.5 0 38.5 -16 t16 -38.5t-16 -38.5t-38.5 -16t-38.5 16t-16 38.5t16 38.5t38.5 16z" />
<glyph unicode="&#xe142;" d="M546 173l469 470q91 91 99 192q7 98 -52 175.5t-154 94.5q-22 4 -47 4q-34 0 -66.5 -10t-56.5 -23t-55.5 -38t-48 -41.5t-48.5 -47.5q-376 -375 -391 -390q-30 -27 -45 -41.5t-37.5 -41t-32 -46.5t-16 -47.5t-1.5 -56.5q9 -62 53.5 -95t99.5 -33q74 0 125 51l548 548 q36 36 20 75q-7 16 -21.5 26t-32.5 10q-26 0 -50 -23q-13 -12 -39 -38l-341 -338q-15 -15 -35.5 -15.5t-34.5 13.5t-14 34.5t14 34.5q327 333 361 367q35 35 67.5 51.5t78.5 16.5q14 0 29 -1q44 -8 74.5 -35.5t43.5 -68.5q14 -47 2 -96.5t-47 -84.5q-12 -11 -32 -32 t-79.5 -81t-114.5 -115t-124.5 -123.5t-123 -119.5t-96.5 -89t-57 -45q-56 -27 -120 -27q-70 0 -129 32t-93 89q-48 78 -35 173t81 163l511 511q71 72 111 96q91 55 198 55q80 0 152 -33q78 -36 129.5 -103t66.5 -154q17 -93 -11 -183.5t-94 -156.5l-482 -476 q-15 -15 -36 -16t-37 14t-17.5 34t14.5 35z" />
<glyph unicode="&#xe143;" d="M649 949q48 68 109.5 104t121.5 38.5t118.5 -20t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-150 152.5t-126.5 127.5t-93.5 124.5t-33.5 117.5q0 64 28 123t73 100.5t104 64t119 20 t120.5 -38.5t104.5 -104zM896 972q-33 0 -64.5 -19t-56.5 -46t-47.5 -53.5t-43.5 -45.5t-37.5 -19t-36 19t-40 45.5t-43 53.5t-54 46t-65.5 19q-67 0 -122.5 -55.5t-55.5 -132.5q0 -23 13.5 -51t46 -65t57.5 -63t76 -75l22 -22q15 -14 44 -44t50.5 -51t46 -44t41 -35t23 -12 t23.5 12t42.5 36t46 44t52.5 52t44 43q4 4 12 13q43 41 63.5 62t52 55t46 55t26 46t11.5 44q0 79 -53 133.5t-120 54.5z" />
<glyph unicode="&#xe144;" d="M776.5 1214q93.5 0 159.5 -66l141 -141q66 -66 66 -160q0 -42 -28 -95.5t-62 -87.5l-29 -29q-31 53 -77 99l-18 18l95 95l-247 248l-389 -389l212 -212l-105 -106l-19 18l-141 141q-66 66 -66 159t66 159l283 283q65 66 158.5 66zM600 706l105 105q10 -8 19 -17l141 -141 q66 -66 66 -159t-66 -159l-283 -283q-66 -66 -159 -66t-159 66l-141 141q-66 66 -66 159.5t66 159.5l55 55q29 -55 75 -102l18 -17l-95 -95l247 -248l389 389z" />
<glyph unicode="&#xe145;" d="M603 1200q85 0 162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5v953q0 21 30 46.5t81 48t129 37.5t163 15zM300 1000v-700h600v700h-600zM600 254q-43 0 -73.5 -30.5t-30.5 -73.5t30.5 -73.5t73.5 -30.5t73.5 30.5 t30.5 73.5t-30.5 73.5t-73.5 30.5z" />
<glyph unicode="&#xe146;" d="M902 1185l283 -282q15 -15 15 -36t-14.5 -35.5t-35.5 -14.5t-35 15l-36 35l-279 -267v-300l-212 210l-308 -307l-280 -203l203 280l307 308l-210 212h300l267 279l-35 36q-15 14 -15 35t14.5 35.5t35.5 14.5t35 -15z" />
<glyph unicode="&#xe148;" d="M700 1248v-78q38 -5 72.5 -14.5t75.5 -31.5t71 -53.5t52 -84t24 -118.5h-159q-4 36 -10.5 59t-21 45t-40 35.5t-64.5 20.5v-307l64 -13q34 -7 64 -16.5t70 -32t67.5 -52.5t47.5 -80t20 -112q0 -139 -89 -224t-244 -97v-77h-100v79q-150 16 -237 103q-40 40 -52.5 93.5 t-15.5 139.5h139q5 -77 48.5 -126t117.5 -65v335l-27 8q-46 14 -79 26.5t-72 36t-63 52t-40 72.5t-16 98q0 70 25 126t67.5 92t94.5 57t110 27v77h100zM600 754v274q-29 -4 -50 -11t-42 -21.5t-31.5 -41.5t-10.5 -65q0 -29 7 -50.5t16.5 -34t28.5 -22.5t31.5 -14t37.5 -10 q9 -3 13 -4zM700 547v-310q22 2 42.5 6.5t45 15.5t41.5 27t29 42t12 59.5t-12.5 59.5t-38 44.5t-53 31t-66.5 24.5z" />
<glyph unicode="&#xe149;" d="M561 1197q84 0 160.5 -40t123.5 -109.5t47 -147.5h-153q0 40 -19.5 71.5t-49.5 48.5t-59.5 26t-55.5 9q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -26 13.5 -63t26.5 -61t37 -66q6 -9 9 -14h241v-100h-197q8 -50 -2.5 -115t-31.5 -95q-45 -62 -99 -112 q34 10 83 17.5t71 7.5q32 1 102 -16t104 -17q83 0 136 30l50 -147q-31 -19 -58 -30.5t-55 -15.5t-42 -4.5t-46 -0.5q-23 0 -76 17t-111 32.5t-96 11.5q-39 -3 -82 -16t-67 -25l-23 -11l-55 145q4 3 16 11t15.5 10.5t13 9t15.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221v100h166q-23 47 -44 104q-7 20 -12 41.5t-6 55.5t6 66.5t29.5 70.5t58.5 71q97 88 263 88z" />
<glyph unicode="&#xe150;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM935 1184l230 -249q14 -14 10 -24.5t-25 -10.5h-150v-900h-200v900h-150q-21 0 -25 10.5t10 24.5l230 249q14 15 35 15t35 -15z" />
<glyph unicode="&#xe151;" d="M1000 700h-100v100h-100v-100h-100v500h300v-500zM400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM801 1100v-200h100v200h-100zM1000 350l-200 -250h200v-100h-300v150l200 250h-200v100h300v-150z " />
<glyph unicode="&#xe152;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1000 1050l-200 -250h200v-100h-300v150l200 250h-200v100h300v-150zM1000 0h-100v100h-100v-100h-100v500h300v-500zM801 400v-200h100v200h-100z " />
<glyph unicode="&#xe153;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1000 700h-100v400h-100v100h200v-500zM1100 0h-100v100h-200v400h300v-500zM901 400v-200h100v200h-100z" />
<glyph unicode="&#xe154;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1100 700h-100v100h-200v400h300v-500zM901 1100v-200h100v200h-100zM1000 0h-100v400h-100v100h200v-500z" />
<glyph unicode="&#xe155;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM900 1000h-200v200h200v-200zM1000 700h-300v200h300v-200zM1100 400h-400v200h400v-200zM1200 100h-500v200h500v-200z" />
<glyph unicode="&#xe156;" d="M400 300h150q21 0 25 -11t-10 -25l-230 -250q-14 -15 -35 -15t-35 15l-230 250q-14 14 -10 25t25 11h150v900h200v-900zM1200 1000h-500v200h500v-200zM1100 700h-400v200h400v-200zM1000 400h-300v200h300v-200zM900 100h-200v200h200v-200z" />
<glyph unicode="&#xe157;" d="M350 1100h400q162 0 256 -93.5t94 -256.5v-400q0 -165 -93.5 -257.5t-256.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5z" />
<glyph unicode="&#xe158;" d="M350 1100h400q165 0 257.5 -92.5t92.5 -257.5v-400q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-163 0 -256.5 92.5t-93.5 257.5v400q0 163 94 256.5t256 93.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5zM440 770l253 -190q17 -12 17 -30t-17 -30l-253 -190q-16 -12 -28 -6.5t-12 26.5v400q0 21 12 26.5t28 -6.5z" />
<glyph unicode="&#xe159;" d="M350 1100h400q163 0 256.5 -94t93.5 -256v-400q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 163 92.5 256.5t257.5 93.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5zM350 700h400q21 0 26.5 -12t-6.5 -28l-190 -253q-12 -17 -30 -17t-30 17l-190 253q-12 16 -6.5 28t26.5 12z" />
<glyph unicode="&#xe160;" d="M350 1100h400q165 0 257.5 -92.5t92.5 -257.5v-400q0 -163 -92.5 -256.5t-257.5 -93.5h-400q-163 0 -256.5 94t-93.5 256v400q0 165 92.5 257.5t257.5 92.5zM800 900h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5 v500q0 41 -29.5 70.5t-70.5 29.5zM580 693l190 -253q12 -16 6.5 -28t-26.5 -12h-400q-21 0 -26.5 12t6.5 28l190 253q12 17 30 17t30 -17z" />
<glyph unicode="&#xe161;" d="M550 1100h400q165 0 257.5 -92.5t92.5 -257.5v-400q0 -165 -92.5 -257.5t-257.5 -92.5h-400q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h450q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-450q-21 0 -35.5 14.5t-14.5 35.5v100 q0 21 14.5 35.5t35.5 14.5zM338 867l324 -284q16 -14 16 -33t-16 -33l-324 -284q-16 -14 -27 -9t-11 26v150h-250q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h250v150q0 21 11 26t27 -9z" />
<glyph unicode="&#xe162;" d="M793 1182l9 -9q8 -10 5 -27q-3 -11 -79 -225.5t-78 -221.5l300 1q24 0 32.5 -17.5t-5.5 -35.5q-1 0 -133.5 -155t-267 -312.5t-138.5 -162.5q-12 -15 -26 -15h-9l-9 8q-9 11 -4 32q2 9 42 123.5t79 224.5l39 110h-302q-23 0 -31 19q-10 21 6 41q75 86 209.5 237.5 t228 257t98.5 111.5q9 16 25 16h9z" />
<glyph unicode="&#xe163;" d="M350 1100h400q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-450q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h450q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400 q0 165 92.5 257.5t257.5 92.5zM938 867l324 -284q16 -14 16 -33t-16 -33l-324 -284q-16 -14 -27 -9t-11 26v150h-250q-21 0 -35.5 14.5t-14.5 35.5v200q0 21 14.5 35.5t35.5 14.5h250v150q0 21 11 26t27 -9z" />
<glyph unicode="&#xe164;" d="M750 1200h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -10.5 -25t-24.5 10l-109 109l-312 -312q-15 -15 -35.5 -15t-35.5 15l-141 141q-15 15 -15 35.5t15 35.5l312 312l-109 109q-14 14 -10 24.5t25 10.5zM456 900h-156q-41 0 -70.5 -29.5t-29.5 -70.5v-500 q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v148l200 200v-298q0 -165 -93.5 -257.5t-256.5 -92.5h-400q-165 0 -257.5 92.5t-92.5 257.5v400q0 165 92.5 257.5t257.5 92.5h300z" />
<glyph unicode="&#xe165;" d="M600 1186q119 0 227.5 -46.5t187 -125t125 -187t46.5 -227.5t-46.5 -227.5t-125 -187t-187 -125t-227.5 -46.5t-227.5 46.5t-187 125t-125 187t-46.5 227.5t46.5 227.5t125 187t187 125t227.5 46.5zM600 1022q-115 0 -212 -56.5t-153.5 -153.5t-56.5 -212t56.5 -212 t153.5 -153.5t212 -56.5t212 56.5t153.5 153.5t56.5 212t-56.5 212t-153.5 153.5t-212 56.5zM600 794q80 0 137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137t57 137t137 57z" />
<glyph unicode="&#xe166;" d="M450 1200h200q21 0 35.5 -14.5t14.5 -35.5v-350h245q20 0 25 -11t-9 -26l-383 -426q-14 -15 -33.5 -15t-32.5 15l-379 426q-13 15 -8.5 26t25.5 11h250v350q0 21 14.5 35.5t35.5 14.5zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5z M900 200v-50h100v50h-100z" />
<glyph unicode="&#xe167;" d="M583 1182l378 -435q14 -15 9 -31t-26 -16h-244v-250q0 -20 -17 -35t-39 -15h-200q-20 0 -32 14.5t-12 35.5v250h-250q-20 0 -25.5 16.5t8.5 31.5l383 431q14 16 33.5 17t33.5 -14zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5z M900 200v-50h100v50h-100z" />
<glyph unicode="&#xe168;" d="M396 723l369 369q7 7 17.5 7t17.5 -7l139 -139q7 -8 7 -18.5t-7 -17.5l-525 -525q-7 -8 -17.5 -8t-17.5 8l-292 291q-7 8 -7 18t7 18l139 139q8 7 18.5 7t17.5 -7zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5zM900 200v-50h100v50 h-100z" />
<glyph unicode="&#xe169;" d="M135 1023l142 142q14 14 35 14t35 -14l77 -77l-212 -212l-77 76q-14 15 -14 36t14 35zM655 855l210 210q14 14 24.5 10t10.5 -25l-2 -599q-1 -20 -15.5 -35t-35.5 -15l-597 -1q-21 0 -25 10.5t10 24.5l208 208l-154 155l212 212zM50 300h1000q21 0 35.5 -14.5t14.5 -35.5 v-250h-1100v250q0 21 14.5 35.5t35.5 14.5zM900 200v-50h100v50h-100z" />
<glyph unicode="&#xe170;" d="M350 1200l599 -2q20 -1 35 -15.5t15 -35.5l1 -597q0 -21 -10.5 -25t-24.5 10l-208 208l-155 -154l-212 212l155 154l-210 210q-14 14 -10 24.5t25 10.5zM524 512l-76 -77q-15 -14 -36 -14t-35 14l-142 142q-14 14 -14 35t14 35l77 77zM50 300h1000q21 0 35.5 -14.5 t14.5 -35.5v-250h-1100v250q0 21 14.5 35.5t35.5 14.5zM900 200v-50h100v50h-100z" />
<glyph unicode="&#xe171;" d="M1200 103l-483 276l-314 -399v423h-399l1196 796v-1096zM483 424v-230l683 953z" />
<glyph unicode="&#xe172;" d="M1100 1000v-850q0 -21 -14.5 -35.5t-35.5 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200z" />
<glyph unicode="&#xe173;" d="M1100 1000l-2 -149l-299 -299l-95 95q-9 9 -21.5 9t-21.5 -9l-149 -147h-312v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM1132 638l106 -106q7 -7 7 -17.5t-7 -17.5l-420 -421q-8 -7 -18 -7 t-18 7l-202 203q-8 7 -8 17.5t8 17.5l106 106q7 8 17.5 8t17.5 -8l79 -79l297 297q7 7 17.5 7t17.5 -7z" />
<glyph unicode="&#xe174;" d="M1100 1000v-269l-103 -103l-134 134q-15 15 -33.5 16.5t-34.5 -12.5l-266 -266h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM1202 572l70 -70q15 -15 15 -35.5t-15 -35.5l-131 -131 l131 -131q15 -15 15 -35.5t-15 -35.5l-70 -70q-15 -15 -35.5 -15t-35.5 15l-131 131l-131 -131q-15 -15 -35.5 -15t-35.5 15l-70 70q-15 15 -15 35.5t15 35.5l131 131l-131 131q-15 15 -15 35.5t15 35.5l70 70q15 15 35.5 15t35.5 -15l131 -131l131 131q15 15 35.5 15 t35.5 -15z" />
<glyph unicode="&#xe175;" d="M1100 1000v-300h-350q-21 0 -35.5 -14.5t-14.5 -35.5v-150h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM850 600h100q21 0 35.5 -14.5t14.5 -35.5v-250h150q21 0 25 -10.5t-10 -24.5 l-230 -230q-14 -14 -35 -14t-35 14l-230 230q-14 14 -10 24.5t25 10.5h150v250q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe176;" d="M1100 1000v-400l-165 165q-14 15 -35 15t-35 -15l-263 -265h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100zM700 1000h-100v200h100v-200zM935 565l230 -229q14 -15 10 -25.5t-25 -10.5h-150v-250q0 -20 -14.5 -35 t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35v250h-150q-21 0 -25 10.5t10 25.5l230 229q14 15 35 15t35 -15z" />
<glyph unicode="&#xe177;" d="M50 1100h1100q21 0 35.5 -14.5t14.5 -35.5v-150h-1200v150q0 21 14.5 35.5t35.5 14.5zM1200 800v-550q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v550h1200zM100 500v-200h400v200h-400z" />
<glyph unicode="&#xe178;" d="M935 1165l248 -230q14 -14 14 -35t-14 -35l-248 -230q-14 -14 -24.5 -10t-10.5 25v150h-400v200h400v150q0 21 10.5 25t24.5 -10zM200 800h-50q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h50v-200zM400 800h-100v200h100v-200zM18 435l247 230 q14 14 24.5 10t10.5 -25v-150h400v-200h-400v-150q0 -21 -10.5 -25t-24.5 10l-247 230q-15 14 -15 35t15 35zM900 300h-100v200h100v-200zM1000 500h51q20 0 34.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-34.5 -14.5h-51v200z" />
<glyph unicode="&#xe179;" d="M862 1073l276 116q25 18 43.5 8t18.5 -41v-1106q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v397q-4 1 -11 5t-24 17.5t-30 29t-24 42t-11 56.5v359q0 31 18.5 65t43.5 52zM550 1200q22 0 34.5 -12.5t14.5 -24.5l1 -13v-450q0 -28 -10.5 -59.5 t-25 -56t-29 -45t-25.5 -31.5l-10 -11v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447q-4 4 -11 11.5t-24 30.5t-30 46t-24 55t-11 60v450q0 2 0.5 5.5t4 12t8.5 15t14.5 12t22.5 5.5q20 0 32.5 -12.5t14.5 -24.5l3 -13v-350h100v350v5.5t2.5 12 t7 15t15 12t25.5 5.5q23 0 35.5 -12.5t13.5 -24.5l1 -13v-350h100v350q0 2 0.5 5.5t3 12t7 15t15 12t24.5 5.5z" />
<glyph unicode="&#xe180;" d="M1200 1100v-56q-4 0 -11 -0.5t-24 -3t-30 -7.5t-24 -15t-11 -24v-888q0 -22 25 -34.5t50 -13.5l25 -2v-56h-400v56q75 0 87.5 6.5t12.5 43.5v394h-500v-394q0 -37 12.5 -43.5t87.5 -6.5v-56h-400v56q4 0 11 0.5t24 3t30 7.5t24 15t11 24v888q0 22 -25 34.5t-50 13.5 l-25 2v56h400v-56q-75 0 -87.5 -6.5t-12.5 -43.5v-394h500v394q0 37 -12.5 43.5t-87.5 6.5v56h400z" />
<glyph unicode="&#xe181;" d="M675 1000h375q21 0 35.5 -14.5t14.5 -35.5v-150h-105l-295 -98v98l-200 200h-400l100 100h375zM100 900h300q41 0 70.5 -29.5t29.5 -70.5v-500q0 -41 -29.5 -70.5t-70.5 -29.5h-300q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5zM100 800v-200h300v200 h-300zM1100 535l-400 -133v163l400 133v-163zM100 500v-200h300v200h-300zM1100 398v-248q0 -21 -14.5 -35.5t-35.5 -14.5h-375l-100 -100h-375l-100 100h400l200 200h105z" />
<glyph unicode="&#xe182;" d="M17 1007l162 162q17 17 40 14t37 -22l139 -194q14 -20 11 -44.5t-20 -41.5l-119 -118q102 -142 228 -268t267 -227l119 118q17 17 42.5 19t44.5 -12l192 -136q19 -14 22.5 -37.5t-13.5 -40.5l-163 -162q-3 -1 -9.5 -1t-29.5 2t-47.5 6t-62.5 14.5t-77.5 26.5t-90 42.5 t-101.5 60t-111 83t-119 108.5q-74 74 -133.5 150.5t-94.5 138.5t-60 119.5t-34.5 100t-15 74.5t-4.5 48z" />
<glyph unicode="&#xe183;" d="M600 1100q92 0 175 -10.5t141.5 -27t108.5 -36.5t81.5 -40t53.5 -37t31 -27l9 -10v-200q0 -21 -14.5 -33t-34.5 -9l-202 34q-20 3 -34.5 20t-14.5 38v146q-141 24 -300 24t-300 -24v-146q0 -21 -14.5 -38t-34.5 -20l-202 -34q-20 -3 -34.5 9t-14.5 33v200q3 4 9.5 10.5 t31 26t54 37.5t80.5 39.5t109 37.5t141 26.5t175 10.5zM600 795q56 0 97 -9.5t60 -23.5t30 -28t12 -24l1 -10v-50l365 -303q14 -15 24.5 -40t10.5 -45v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v212q0 20 10.5 45t24.5 40l365 303v50 q0 4 1 10.5t12 23t30 29t60 22.5t97 10z" />
<glyph unicode="&#xe184;" d="M1100 700l-200 -200h-600l-200 200v500h200v-200h200v200h200v-200h200v200h200v-500zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-12l137 -100h-950l137 100h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5 t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe185;" d="M700 1100h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-1000h300v1000q0 41 -29.5 70.5t-70.5 29.5zM1100 800h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-700h300v700q0 41 -29.5 70.5t-70.5 29.5zM400 0h-300v400q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-400z " />
<glyph unicode="&#xe186;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 700h-200v-100h200v-300h-300v100h200v100h-200v300h300v-100zM900 700v-300l-100 -100h-200v500h200z M700 700v-300h100v300h-100z" />
<glyph unicode="&#xe187;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 300h-100v200h-100v-200h-100v500h100v-200h100v200h100v-500zM900 700v-300l-100 -100h-200v500h200z M700 700v-300h100v300h-100z" />
<glyph unicode="&#xe188;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 700h-200v-300h200v-100h-300v500h300v-100zM900 700h-200v-300h200v-100h-300v500h300v-100z" />
<glyph unicode="&#xe189;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 400l-300 150l300 150v-300zM900 550l-300 -150v300z" />
<glyph unicode="&#xe190;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM900 300h-700v500h700v-500zM800 700h-130q-38 0 -66.5 -43t-28.5 -108t27 -107t68 -42h130v300zM300 700v-300 h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130z" />
<glyph unicode="&#xe191;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 700h-200v-100h200v-300h-300v100h200v100h-200v300h300v-100zM900 300h-100v400h-100v100h200v-500z M700 300h-100v100h100v-100z" />
<glyph unicode="&#xe192;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM300 700h200v-400h-300v500h100v-100zM900 300h-100v400h-100v100h200v-500zM300 600v-200h100v200h-100z M700 300h-100v100h100v-100z" />
<glyph unicode="&#xe193;" d="M200 1100h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212v500q0 124 88 212t212 88zM100 900v-700h900v700h-900zM500 500l-199 -200h-100v50l199 200v150h-200v100h300v-300zM900 300h-100v400h-100v100h200v-500zM701 300h-100 v100h100v-100z" />
<glyph unicode="&#xe194;" d="M600 1191q120 0 229.5 -47t188.5 -126t126 -188.5t47 -229.5t-47 -229.5t-126 -188.5t-188.5 -126t-229.5 -47t-229.5 47t-188.5 126t-126 188.5t-47 229.5t47 229.5t126 188.5t188.5 126t229.5 47zM600 1021q-114 0 -211 -56.5t-153.5 -153.5t-56.5 -211t56.5 -211 t153.5 -153.5t211 -56.5t211 56.5t153.5 153.5t56.5 211t-56.5 211t-153.5 153.5t-211 56.5zM800 700h-300v-200h300v-100h-300l-100 100v200l100 100h300v-100z" />
<glyph unicode="&#xe195;" d="M600 1191q120 0 229.5 -47t188.5 -126t126 -188.5t47 -229.5t-47 -229.5t-126 -188.5t-188.5 -126t-229.5 -47t-229.5 47t-188.5 126t-126 188.5t-47 229.5t47 229.5t126 188.5t188.5 126t229.5 47zM600 1021q-114 0 -211 -56.5t-153.5 -153.5t-56.5 -211t56.5 -211 t153.5 -153.5t211 -56.5t211 56.5t153.5 153.5t56.5 211t-56.5 211t-153.5 153.5t-211 56.5zM800 700v-100l-50 -50l100 -100v-50h-100l-100 100h-150v-100h-100v400h300zM500 700v-100h200v100h-200z" />
<glyph unicode="&#xe197;" d="M503 1089q110 0 200.5 -59.5t134.5 -156.5q44 14 90 14q120 0 205 -86.5t85 -207t-85 -207t-205 -86.5h-128v250q0 21 -14.5 35.5t-35.5 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-250h-222q-80 0 -136 57.5t-56 136.5q0 69 43 122.5t108 67.5q-2 19 -2 37q0 100 49 185 t134 134t185 49zM525 500h150q10 0 17.5 -7.5t7.5 -17.5v-275h137q21 0 26 -11.5t-8 -27.5l-223 -244q-13 -16 -32 -16t-32 16l-223 244q-13 16 -8 27.5t26 11.5h137v275q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe198;" d="M502 1089q110 0 201 -59.5t135 -156.5q43 15 89 15q121 0 206 -86.5t86 -206.5q0 -99 -60 -181t-150 -110l-378 360q-13 16 -31.5 16t-31.5 -16l-381 -365h-9q-79 0 -135.5 57.5t-56.5 136.5q0 69 43 122.5t108 67.5q-2 19 -2 38q0 100 49 184.5t133.5 134t184.5 49.5z M632 467l223 -228q13 -16 8 -27.5t-26 -11.5h-137v-275q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v275h-137q-21 0 -26 11.5t8 27.5q199 204 223 228q19 19 31.5 19t32.5 -19z" />
<glyph unicode="&#xe199;" d="M700 100v100h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170l-270 -300h400v-100h-50q-21 0 -35.5 -14.5t-14.5 -35.5v-50h400v50q0 21 -14.5 35.5t-35.5 14.5h-50z" />
<glyph unicode="&#xe200;" d="M600 1179q94 0 167.5 -56.5t99.5 -145.5q89 -6 150.5 -71.5t61.5 -155.5q0 -61 -29.5 -112.5t-79.5 -82.5q9 -29 9 -55q0 -74 -52.5 -126.5t-126.5 -52.5q-55 0 -100 30v-251q21 0 35.5 -14.5t14.5 -35.5v-50h-300v50q0 21 14.5 35.5t35.5 14.5v251q-45 -30 -100 -30 q-74 0 -126.5 52.5t-52.5 126.5q0 18 4 38q-47 21 -75.5 65t-28.5 97q0 74 52.5 126.5t126.5 52.5q5 0 23 -2q0 2 -1 10t-1 13q0 116 81.5 197.5t197.5 81.5z" />
<glyph unicode="&#xe201;" d="M1010 1010q111 -111 150.5 -260.5t0 -299t-150.5 -260.5q-83 -83 -191.5 -126.5t-218.5 -43.5t-218.5 43.5t-191.5 126.5q-111 111 -150.5 260.5t0 299t150.5 260.5q83 83 191.5 126.5t218.5 43.5t218.5 -43.5t191.5 -126.5zM476 1065q-4 0 -8 -1q-121 -34 -209.5 -122.5 t-122.5 -209.5q-4 -12 2.5 -23t18.5 -14l36 -9q3 -1 7 -1q23 0 29 22q27 96 98 166q70 71 166 98q11 3 17.5 13.5t3.5 22.5l-9 35q-3 13 -14 19q-7 4 -15 4zM512 920q-4 0 -9 -2q-80 -24 -138.5 -82.5t-82.5 -138.5q-4 -13 2 -24t19 -14l34 -9q4 -1 8 -1q22 0 28 21 q18 58 58.5 98.5t97.5 58.5q12 3 18 13.5t3 21.5l-9 35q-3 12 -14 19q-7 4 -15 4zM719.5 719.5q-49.5 49.5 -119.5 49.5t-119.5 -49.5t-49.5 -119.5t49.5 -119.5t119.5 -49.5t119.5 49.5t49.5 119.5t-49.5 119.5zM855 551q-22 0 -28 -21q-18 -58 -58.5 -98.5t-98.5 -57.5 q-11 -4 -17 -14.5t-3 -21.5l9 -35q3 -12 14 -19q7 -4 15 -4q4 0 9 2q80 24 138.5 82.5t82.5 138.5q4 13 -2.5 24t-18.5 14l-34 9q-4 1 -8 1zM1000 515q-23 0 -29 -22q-27 -96 -98 -166q-70 -71 -166 -98q-11 -3 -17.5 -13.5t-3.5 -22.5l9 -35q3 -13 14 -19q7 -4 15 -4 q4 0 8 1q121 34 209.5 122.5t122.5 209.5q4 12 -2.5 23t-18.5 14l-36 9q-3 1 -7 1z" />
<glyph unicode="&#xe202;" d="M700 800h300v-380h-180v200h-340v-200h-380v755q0 10 7.5 17.5t17.5 7.5h575v-400zM1000 900h-200v200zM700 300h162l-212 -212l-212 212h162v200h100v-200zM520 0h-395q-10 0 -17.5 7.5t-7.5 17.5v395zM1000 220v-195q0 -10 -7.5 -17.5t-17.5 -7.5h-195z" />
<glyph unicode="&#xe203;" d="M700 800h300v-520l-350 350l-550 -550v1095q0 10 7.5 17.5t17.5 7.5h575v-400zM1000 900h-200v200zM862 200h-162v-200h-100v200h-162l212 212zM480 0h-355q-10 0 -17.5 7.5t-7.5 17.5v55h380v-80zM1000 80v-55q0 -10 -7.5 -17.5t-17.5 -7.5h-155v80h180z" />
<glyph unicode="&#xe204;" d="M1162 800h-162v-200h100l100 -100h-300v300h-162l212 212zM200 800h200q27 0 40 -2t29.5 -10.5t23.5 -30t7 -57.5h300v-100h-600l-200 -350v450h100q0 36 7 57.5t23.5 30t29.5 10.5t40 2zM800 400h240l-240 -400h-800l300 500h500v-100z" />
<glyph unicode="&#xe205;" d="M650 1100h100q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h50v50q0 21 14.5 35.5t35.5 14.5zM1000 850v150q41 0 70.5 -29.5t29.5 -70.5v-800 q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-1 0 -20 4l246 246l-326 326v324q0 41 29.5 70.5t70.5 29.5v-150q0 -62 44 -106t106 -44h300q62 0 106 44t44 106zM412 250l-212 -212v162h-200v100h200v162z" />
<glyph unicode="&#xe206;" d="M450 1100h100q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-300q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h50v50q0 21 14.5 35.5t35.5 14.5zM800 850v150q41 0 70.5 -29.5t29.5 -70.5v-500 h-200v-300h200q0 -36 -7 -57.5t-23.5 -30t-29.5 -10.5t-40 -2h-600q-41 0 -70.5 29.5t-29.5 70.5v800q0 41 29.5 70.5t70.5 29.5v-150q0 -62 44 -106t106 -44h300q62 0 106 44t44 106zM1212 250l-212 -212v162h-200v100h200v162z" />
<glyph unicode="&#xe209;" d="M658 1197l637 -1104q23 -38 7 -65.5t-60 -27.5h-1276q-44 0 -60 27.5t7 65.5l637 1104q22 39 54 39t54 -39zM704 800h-208q-20 0 -32 -14.5t-8 -34.5l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5zM500 300v-100h200 v100h-200z" />
<glyph unicode="&#xe210;" d="M425 1100h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM425 800h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5 t17.5 7.5zM825 800h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM25 500h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150 q0 10 7.5 17.5t17.5 7.5zM425 500h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM825 500h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5 v150q0 10 7.5 17.5t17.5 7.5zM25 200h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM425 200h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5 t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM825 200h250q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-250q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe211;" d="M700 1200h100v-200h-100v-100h350q62 0 86.5 -39.5t-3.5 -94.5l-66 -132q-41 -83 -81 -134h-772q-40 51 -81 134l-66 132q-28 55 -3.5 94.5t86.5 39.5h350v100h-100v200h100v100h200v-100zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-12l137 -100 h-950l138 100h-13q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe212;" d="M600 1300q40 0 68.5 -29.5t28.5 -70.5h-194q0 41 28.5 70.5t68.5 29.5zM443 1100h314q18 -37 18 -75q0 -8 -3 -25h328q41 0 44.5 -16.5t-30.5 -38.5l-175 -145h-678l-178 145q-34 22 -29 38.5t46 16.5h328q-3 17 -3 25q0 38 18 75zM250 700h700q21 0 35.5 -14.5 t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-150v-200l275 -200h-950l275 200v200h-150q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe213;" d="M600 1181q75 0 128 -53t53 -128t-53 -128t-128 -53t-128 53t-53 128t53 128t128 53zM602 798h46q34 0 55.5 -28.5t21.5 -86.5q0 -76 39 -183h-324q39 107 39 183q0 58 21.5 86.5t56.5 28.5h45zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-13 l138 -100h-950l137 100h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe214;" d="M600 1300q47 0 92.5 -53.5t71 -123t25.5 -123.5q0 -78 -55.5 -133.5t-133.5 -55.5t-133.5 55.5t-55.5 133.5q0 62 34 143l144 -143l111 111l-163 163q34 26 63 26zM602 798h46q34 0 55.5 -28.5t21.5 -86.5q0 -76 39 -183h-324q39 107 39 183q0 58 21.5 86.5t56.5 28.5h45 zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-13l138 -100h-950l137 100h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe215;" d="M600 1200l300 -161v-139h-300q0 -57 18.5 -108t50 -91.5t63 -72t70 -67.5t57.5 -61h-530q-60 83 -90.5 177.5t-30.5 178.5t33 164.5t87.5 139.5t126 96.5t145.5 41.5v-98zM250 400h700q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-13l138 -100h-950l137 100 h-12q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5zM50 100h1100q21 0 35.5 -14.5t14.5 -35.5v-50h-1200v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe216;" d="M600 1300q41 0 70.5 -29.5t29.5 -70.5v-78q46 -26 73 -72t27 -100v-50h-400v50q0 54 27 100t73 72v78q0 41 29.5 70.5t70.5 29.5zM400 800h400q54 0 100 -27t72 -73h-172v-100h200v-100h-200v-100h200v-100h-200v-100h200q0 -83 -58.5 -141.5t-141.5 -58.5h-400 q-83 0 -141.5 58.5t-58.5 141.5v400q0 83 58.5 141.5t141.5 58.5z" />
<glyph unicode="&#xe218;" d="M150 1100h900q21 0 35.5 -14.5t14.5 -35.5v-500q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v500q0 21 14.5 35.5t35.5 14.5zM125 400h950q10 0 17.5 -7.5t7.5 -17.5v-50q0 -10 -7.5 -17.5t-17.5 -7.5h-283l224 -224q13 -13 13 -31.5t-13 -32 t-31.5 -13.5t-31.5 13l-88 88h-524l-87 -88q-13 -13 -32 -13t-32 13.5t-13 32t13 31.5l224 224h-289q-10 0 -17.5 7.5t-7.5 17.5v50q0 10 7.5 17.5t17.5 7.5zM541 300l-100 -100h324l-100 100h-124z" />
<glyph unicode="&#xe219;" d="M200 1100h800q83 0 141.5 -58.5t58.5 -141.5v-200h-100q0 41 -29.5 70.5t-70.5 29.5h-250q-41 0 -70.5 -29.5t-29.5 -70.5h-100q0 41 -29.5 70.5t-70.5 29.5h-250q-41 0 -70.5 -29.5t-29.5 -70.5h-100v200q0 83 58.5 141.5t141.5 58.5zM100 600h1000q41 0 70.5 -29.5 t29.5 -70.5v-300h-1200v300q0 41 29.5 70.5t70.5 29.5zM300 100v-50q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v50h200zM1100 100v-50q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v50h200z" />
<glyph unicode="&#xe221;" d="M480 1165l682 -683q31 -31 31 -75.5t-31 -75.5l-131 -131h-481l-517 518q-32 31 -32 75.5t32 75.5l295 296q31 31 75.5 31t76.5 -31zM108 794l342 -342l303 304l-341 341zM250 100h800q21 0 35.5 -14.5t14.5 -35.5v-50h-900v50q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe223;" d="M1057 647l-189 506q-8 19 -27.5 33t-40.5 14h-400q-21 0 -40.5 -14t-27.5 -33l-189 -506q-8 -19 1.5 -33t30.5 -14h625v-150q0 -21 14.5 -35.5t35.5 -14.5t35.5 14.5t14.5 35.5v150h125q21 0 30.5 14t1.5 33zM897 0h-595v50q0 21 14.5 35.5t35.5 14.5h50v50 q0 21 14.5 35.5t35.5 14.5h48v300h200v-300h47q21 0 35.5 -14.5t14.5 -35.5v-50h50q21 0 35.5 -14.5t14.5 -35.5v-50z" />
<glyph unicode="&#xe224;" d="M900 800h300v-575q0 -10 -7.5 -17.5t-17.5 -7.5h-375v591l-300 300v84q0 10 7.5 17.5t17.5 7.5h375v-400zM1200 900h-200v200zM400 600h300v-575q0 -10 -7.5 -17.5t-17.5 -7.5h-650q-10 0 -17.5 7.5t-7.5 17.5v950q0 10 7.5 17.5t17.5 7.5h375v-400zM700 700h-200v200z " />
<glyph unicode="&#xe225;" d="M484 1095h195q75 0 146 -32.5t124 -86t89.5 -122.5t48.5 -142q18 -14 35 -20q31 -10 64.5 6.5t43.5 48.5q10 34 -15 71q-19 27 -9 43q5 8 12.5 11t19 -1t23.5 -16q41 -44 39 -105q-3 -63 -46 -106.5t-104 -43.5h-62q-7 -55 -35 -117t-56 -100l-39 -234q-3 -20 -20 -34.5 t-38 -14.5h-100q-21 0 -33 14.5t-9 34.5l12 70q-49 -14 -91 -14h-195q-24 0 -65 8l-11 -64q-3 -20 -20 -34.5t-38 -14.5h-100q-21 0 -33 14.5t-9 34.5l26 157q-84 74 -128 175l-159 53q-19 7 -33 26t-14 40v50q0 21 14.5 35.5t35.5 14.5h124q11 87 56 166l-111 95 q-16 14 -12.5 23.5t24.5 9.5h203q116 101 250 101zM675 1000h-250q-10 0 -17.5 -7.5t-7.5 -17.5v-50q0 -10 7.5 -17.5t17.5 -7.5h250q10 0 17.5 7.5t7.5 17.5v50q0 10 -7.5 17.5t-17.5 7.5z" />
<glyph unicode="&#xe226;" d="M641 900l423 247q19 8 42 2.5t37 -21.5l32 -38q14 -15 12.5 -36t-17.5 -34l-139 -120h-390zM50 1100h106q67 0 103 -17t66 -71l102 -212h823q21 0 35.5 -14.5t14.5 -35.5v-50q0 -21 -14 -40t-33 -26l-737 -132q-23 -4 -40 6t-26 25q-42 67 -100 67h-300q-62 0 -106 44 t-44 106v200q0 62 44 106t106 44zM173 928h-80q-19 0 -28 -14t-9 -35v-56q0 -51 42 -51h134q16 0 21.5 8t5.5 24q0 11 -16 45t-27 51q-18 28 -43 28zM550 727q-32 0 -54.5 -22.5t-22.5 -54.5t22.5 -54.5t54.5 -22.5t54.5 22.5t22.5 54.5t-22.5 54.5t-54.5 22.5zM130 389 l152 130q18 19 34 24t31 -3.5t24.5 -17.5t25.5 -28q28 -35 50.5 -51t48.5 -13l63 5l48 -179q13 -61 -3.5 -97.5t-67.5 -79.5l-80 -69q-47 -40 -109 -35.5t-103 51.5l-130 151q-40 47 -35.5 109.5t51.5 102.5zM380 377l-102 -88q-31 -27 2 -65l37 -43q13 -15 27.5 -19.5 t31.5 6.5l61 53q19 16 14 49q-2 20 -12 56t-17 45q-11 12 -19 14t-23 -8z" />
<glyph unicode="&#xe227;" d="M625 1200h150q10 0 17.5 -7.5t7.5 -17.5v-109q79 -33 131 -87.5t53 -128.5q1 -46 -15 -84.5t-39 -61t-46 -38t-39 -21.5l-17 -6q6 0 15 -1.5t35 -9t50 -17.5t53 -30t50 -45t35.5 -64t14.5 -84q0 -59 -11.5 -105.5t-28.5 -76.5t-44 -51t-49.5 -31.5t-54.5 -16t-49.5 -6.5 t-43.5 -1v-75q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v75h-100v-75q0 -10 -7.5 -17.5t-17.5 -7.5h-150q-10 0 -17.5 7.5t-7.5 17.5v75h-175q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5h75v600h-75q-10 0 -17.5 7.5t-7.5 17.5v150 q0 10 7.5 17.5t17.5 7.5h175v75q0 10 7.5 17.5t17.5 7.5h150q10 0 17.5 -7.5t7.5 -17.5v-75h100v75q0 10 7.5 17.5t17.5 7.5zM400 900v-200h263q28 0 48.5 10.5t30 25t15 29t5.5 25.5l1 10q0 4 -0.5 11t-6 24t-15 30t-30 24t-48.5 11h-263zM400 500v-200h363q28 0 48.5 10.5 t30 25t15 29t5.5 25.5l1 10q0 4 -0.5 11t-6 24t-15 30t-30 24t-48.5 11h-363z" />
<glyph unicode="&#xe230;" d="M212 1198h780q86 0 147 -61t61 -147v-416q0 -51 -18 -142.5t-36 -157.5l-18 -66q-29 -87 -93.5 -146.5t-146.5 -59.5h-572q-82 0 -147 59t-93 147q-8 28 -20 73t-32 143.5t-20 149.5v416q0 86 61 147t147 61zM600 1045q-70 0 -132.5 -11.5t-105.5 -30.5t-78.5 -41.5 t-57 -45t-36 -41t-20.5 -30.5l-6 -12l156 -243h560l156 243q-2 5 -6 12.5t-20 29.5t-36.5 42t-57 44.5t-79 42t-105 29.5t-132.5 12zM762 703h-157l195 261z" />
<glyph unicode="&#xe231;" d="M475 1300h150q103 0 189 -86t86 -189v-500q0 -41 -42 -83t-83 -42h-450q-41 0 -83 42t-42 83v500q0 103 86 189t189 86zM700 300v-225q0 -21 -27 -48t-48 -27h-150q-21 0 -48 27t-27 48v225h300z" />
<glyph unicode="&#xe232;" d="M475 1300h96q0 -150 89.5 -239.5t239.5 -89.5v-446q0 -41 -42 -83t-83 -42h-450q-41 0 -83 42t-42 83v500q0 103 86 189t189 86zM700 300v-225q0 -21 -27 -48t-48 -27h-150q-21 0 -48 27t-27 48v225h300z" />
<glyph unicode="&#xe233;" d="M1294 767l-638 -283l-378 170l-78 -60v-224l100 -150v-199l-150 148l-150 -149v200l100 150v250q0 4 -0.5 10.5t0 9.5t1 8t3 8t6.5 6l47 40l-147 65l642 283zM1000 380l-350 -166l-350 166v147l350 -165l350 165v-147z" />
<glyph unicode="&#xe234;" d="M250 800q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM650 800q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM1050 800q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44z" />
<glyph unicode="&#xe235;" d="M550 1100q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM550 700q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44zM550 300q62 0 106 -44t44 -106t-44 -106t-106 -44t-106 44t-44 106t44 106t106 44z" />
<glyph unicode="&#xe236;" d="M125 1100h950q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-950q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5zM125 700h950q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-950q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5 t17.5 7.5zM125 300h950q10 0 17.5 -7.5t7.5 -17.5v-150q0 -10 -7.5 -17.5t-17.5 -7.5h-950q-10 0 -17.5 7.5t-7.5 17.5v150q0 10 7.5 17.5t17.5 7.5z" />
<glyph unicode="&#xe237;" d="M350 1200h500q162 0 256 -93.5t94 -256.5v-500q0 -165 -93.5 -257.5t-256.5 -92.5h-500q-165 0 -257.5 92.5t-92.5 257.5v500q0 165 92.5 257.5t257.5 92.5zM900 1000h-600q-41 0 -70.5 -29.5t-29.5 -70.5v-600q0 -41 29.5 -70.5t70.5 -29.5h600q41 0 70.5 29.5 t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5zM350 900h500q21 0 35.5 -14.5t14.5 -35.5v-300q0 -21 -14.5 -35.5t-35.5 -14.5h-500q-21 0 -35.5 14.5t-14.5 35.5v300q0 21 14.5 35.5t35.5 14.5zM400 800v-200h400v200h-400z" />
<glyph unicode="&#xe238;" d="M150 1100h1000q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-200h50q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-200h50q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5t-35.5 -14.5h-50v-200h50q21 0 35.5 -14.5t14.5 -35.5t-14.5 -35.5 t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h50v200h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h50v200h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5h50v200h-50q-21 0 -35.5 14.5t-14.5 35.5t14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe239;" d="M650 1187q87 -67 118.5 -156t0 -178t-118.5 -155q-87 66 -118.5 155t0 178t118.5 156zM300 800q124 0 212 -88t88 -212q-124 0 -212 88t-88 212zM1000 800q0 -124 -88 -212t-212 -88q0 124 88 212t212 88zM300 500q124 0 212 -88t88 -212q-124 0 -212 88t-88 212z M1000 500q0 -124 -88 -212t-212 -88q0 124 88 212t212 88zM700 199v-144q0 -21 -14.5 -35.5t-35.5 -14.5t-35.5 14.5t-14.5 35.5v142q40 -4 43 -4q17 0 57 6z" />
<glyph unicode="&#xe240;" d="M745 878l69 19q25 6 45 -12l298 -295q11 -11 15 -26.5t-2 -30.5q-5 -14 -18 -23.5t-28 -9.5h-8q1 0 1 -13q0 -29 -2 -56t-8.5 -62t-20 -63t-33 -53t-51 -39t-72.5 -14h-146q-184 0 -184 288q0 24 10 47q-20 4 -62 4t-63 -4q11 -24 11 -47q0 -288 -184 -288h-142 q-48 0 -84.5 21t-56 51t-32 71.5t-16 75t-3.5 68.5q0 13 2 13h-7q-15 0 -27.5 9.5t-18.5 23.5q-6 15 -2 30.5t15 25.5l298 296q20 18 46 11l76 -19q20 -5 30.5 -22.5t5.5 -37.5t-22.5 -31t-37.5 -5l-51 12l-182 -193h891l-182 193l-44 -12q-20 -5 -37.5 6t-22.5 31t6 37.5 t31 22.5z" />
<glyph unicode="&#xe241;" d="M1200 900h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-200v-850q0 -22 25 -34.5t50 -13.5l25 -2v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v850h-200q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h1000v-300zM500 450h-25q0 15 -4 24.5t-9 14.5t-17 7.5t-20 3t-25 0.5h-100v-425q0 -11 12.5 -17.5t25.5 -7.5h12v-50h-200v50q50 0 50 25v425h-100q-17 0 -25 -0.5t-20 -3t-17 -7.5t-9 -14.5t-4 -24.5h-25v150h500v-150z" />
<glyph unicode="&#xe242;" d="M1000 300v50q-25 0 -55 32q-14 14 -25 31t-16 27l-4 11l-289 747h-69l-300 -754q-18 -35 -39 -56q-9 -9 -24.5 -18.5t-26.5 -14.5l-11 -5v-50h273v50q-49 0 -78.5 21.5t-11.5 67.5l69 176h293l61 -166q13 -34 -3.5 -66.5t-55.5 -32.5v-50h312zM412 691l134 342l121 -342 h-255zM1100 150v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5z" />
<glyph unicode="&#xe243;" d="M50 1200h1100q21 0 35.5 -14.5t14.5 -35.5v-1100q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-21 0 -35.5 14.5t-14.5 35.5v1100q0 21 14.5 35.5t35.5 14.5zM611 1118h-70q-13 0 -18 -12l-299 -753q-17 -32 -35 -51q-18 -18 -56 -34q-12 -5 -12 -18v-50q0 -8 5.5 -14t14.5 -6 h273q8 0 14 6t6 14v50q0 8 -6 14t-14 6q-55 0 -71 23q-10 14 0 39l63 163h266l57 -153q11 -31 -6 -55q-12 -17 -36 -17q-8 0 -14 -6t-6 -14v-50q0 -8 6 -14t14 -6h313q8 0 14 6t6 14v50q0 7 -5.5 13t-13.5 7q-17 0 -42 25q-25 27 -40 63h-1l-288 748q-5 12 -19 12zM639 611 h-197l103 264z" />
<glyph unicode="&#xe244;" d="M1200 1100h-1200v100h1200v-100zM50 1000h400q21 0 35.5 -14.5t14.5 -35.5v-900q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v900q0 21 14.5 35.5t35.5 14.5zM650 1000h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM700 900v-300h300v300h-300z" />
<glyph unicode="&#xe245;" d="M50 1200h400q21 0 35.5 -14.5t14.5 -35.5v-900q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v900q0 21 14.5 35.5t35.5 14.5zM650 700h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400 q0 21 14.5 35.5t35.5 14.5zM700 600v-300h300v300h-300zM1200 0h-1200v100h1200v-100z" />
<glyph unicode="&#xe246;" d="M50 1000h400q21 0 35.5 -14.5t14.5 -35.5v-350h100v150q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-150h100v-100h-100v-150q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v150h-100v-350q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5v800q0 21 14.5 35.5t35.5 14.5zM700 700v-300h300v300h-300z" />
<glyph unicode="&#xe247;" d="M100 0h-100v1200h100v-1200zM250 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM300 1000v-300h300v300h-300zM250 500h900q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe248;" d="M600 1100h150q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-150v-100h450q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h350v100h-150q-21 0 -35.5 14.5 t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5h150v100h100v-100zM400 1000v-300h300v300h-300z" />
<glyph unicode="&#xe249;" d="M1200 0h-100v1200h100v-1200zM550 1100h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM600 1000v-300h300v300h-300zM50 500h900q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-900q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5z" />
<glyph unicode="&#xe250;" d="M865 565l-494 -494q-23 -23 -41 -23q-14 0 -22 13.5t-8 38.5v1000q0 25 8 38.5t22 13.5q18 0 41 -23l494 -494q14 -14 14 -35t-14 -35z" />
<glyph unicode="&#xe251;" d="M335 635l494 494q29 29 50 20.5t21 -49.5v-1000q0 -41 -21 -49.5t-50 20.5l-494 494q-14 14 -14 35t14 35z" />
<glyph unicode="&#xe252;" d="M100 900h1000q41 0 49.5 -21t-20.5 -50l-494 -494q-14 -14 -35 -14t-35 14l-494 494q-29 29 -20.5 50t49.5 21z" />
<glyph unicode="&#xe253;" d="M635 865l494 -494q29 -29 20.5 -50t-49.5 -21h-1000q-41 0 -49.5 21t20.5 50l494 494q14 14 35 14t35 -14z" />
<glyph unicode="&#xe254;" d="M700 741v-182l-692 -323v221l413 193l-413 193v221zM1200 0h-800v200h800v-200z" />
<glyph unicode="&#xe255;" d="M1200 900h-200v-100h200v-100h-300v300h200v100h-200v100h300v-300zM0 700h50q0 21 4 37t9.5 26.5t18 17.5t22 11t28.5 5.5t31 2t37 0.5h100v-550q0 -22 -25 -34.5t-50 -13.5l-25 -2v-100h400v100q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v550h100q25 0 37 -0.5t31 -2 t28.5 -5.5t22 -11t18 -17.5t9.5 -26.5t4 -37h50v300h-800v-300z" />
<glyph unicode="&#xe256;" d="M800 700h-50q0 21 -4 37t-9.5 26.5t-18 17.5t-22 11t-28.5 5.5t-31 2t-37 0.5h-100v-550q0 -22 25 -34.5t50 -14.5l25 -1v-100h-400v100q4 0 11 0.5t24 3t30 7t24 15t11 24.5v550h-100q-25 0 -37 -0.5t-31 -2t-28.5 -5.5t-22 -11t-18 -17.5t-9.5 -26.5t-4 -37h-50v300 h800v-300zM1100 200h-200v-100h200v-100h-300v300h200v100h-200v100h300v-300z" />
<glyph unicode="&#xe257;" d="M701 1098h160q16 0 21 -11t-7 -23l-464 -464l464 -464q12 -12 7 -23t-21 -11h-160q-13 0 -23 9l-471 471q-7 8 -7 18t7 18l471 471q10 9 23 9z" />
<glyph unicode="&#xe258;" d="M339 1098h160q13 0 23 -9l471 -471q7 -8 7 -18t-7 -18l-471 -471q-10 -9 -23 -9h-160q-16 0 -21 11t7 23l464 464l-464 464q-12 12 -7 23t21 11z" />
<glyph unicode="&#xe259;" d="M1087 882q11 -5 11 -21v-160q0 -13 -9 -23l-471 -471q-8 -7 -18 -7t-18 7l-471 471q-9 10 -9 23v160q0 16 11 21t23 -7l464 -464l464 464q12 12 23 7z" />
<glyph unicode="&#xe260;" d="M618 993l471 -471q9 -10 9 -23v-160q0 -16 -11 -21t-23 7l-464 464l-464 -464q-12 -12 -23 -7t-11 21v160q0 13 9 23l471 471q8 7 18 7t18 -7z" />
<glyph unicode="&#xf8ff;" d="M1000 1200q0 -124 -88 -212t-212 -88q0 124 88 212t212 88zM450 1000h100q21 0 40 -14t26 -33l79 -194q5 1 16 3q34 6 54 9.5t60 7t65.5 1t61 -10t56.5 -23t42.5 -42t29 -64t5 -92t-19.5 -121.5q-1 -7 -3 -19.5t-11 -50t-20.5 -73t-32.5 -81.5t-46.5 -83t-64 -70 t-82.5 -50q-13 -5 -42 -5t-65.5 2.5t-47.5 2.5q-14 0 -49.5 -3.5t-63 -3.5t-43.5 7q-57 25 -104.5 78.5t-75 111.5t-46.5 112t-26 90l-7 35q-15 63 -18 115t4.5 88.5t26 64t39.5 43.5t52 25.5t58.5 13t62.5 2t59.5 -4.5t55.5 -8l-147 192q-12 18 -5.5 30t27.5 12z" />
<glyph unicode="&#x1f511;" d="M250 1200h600q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-150v-500l-255 -178q-19 -9 -32 -1t-13 29v650h-150q-21 0 -35.5 14.5t-14.5 35.5v400q0 21 14.5 35.5t35.5 14.5zM400 1100v-100h300v100h-300z" />
<glyph unicode="&#x1f6aa;" d="M250 1200h750q39 0 69.5 -40.5t30.5 -84.5v-933l-700 -117v950l600 125h-700v-1000h-100v1025q0 23 15.5 49t34.5 26zM500 525v-100l100 20v100z" />
</font>
</defs></svg>

After

Width:  |  Height:  |  Size: 106 KiB

View File

File diff suppressed because one or more lines are too long

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 B

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