1#!/usr/bin/python3 -OO
2# Copyright 2007-2021 The SABnzbd-Team <team@sabnzbd.org>
3#
4# This program is free software; you can redistribute it and/or
5# modify it under the terms of the GNU General Public License
6# as published by the Free Software Foundation; either version 2
7# of the License, or (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program; if not, write to the Free Software
16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17
18"""
19sabnzbd.interface - webinterface
20"""
21
22import os
23import time
24from datetime import datetime
25import cherrypy
26import logging
27import urllib.request, urllib.parse, urllib.error
28import re
29import hashlib
30import socket
31import ssl
32import functools
33import ipaddress
34from threading import Thread
35from random import randint
36from xml.sax.saxutils import escape
37from Cheetah.Template import Template
38from typing import Optional, Callable, Union
39
40import sabnzbd
41import sabnzbd.rss
42from sabnzbd.misc import (
43    to_units,
44    from_units,
45    time_format,
46    calc_age,
47    int_conv,
48    get_base_url,
49    is_ipv4_addr,
50    is_ipv6_addr,
51    opts_to_pp,
52    get_server_addrinfo,
53    is_lan_addr,
54    is_loopback_addr,
55    ip_in_subnet,
56    strip_ipv4_mapped_notation,
57)
58from sabnzbd.filesystem import real_path, long_path, globber, globber_full, remove_all, clip_path, same_file
59from sabnzbd.encoding import xml_name, utob
60import sabnzbd.config as config
61import sabnzbd.cfg as cfg
62import sabnzbd.notifier as notifier
63import sabnzbd.newsunpack
64from sabnzbd.utils.servertests import test_nntp_server_dict
65from sabnzbd.utils.diskspeed import diskspeedmeasure
66from sabnzbd.utils.getperformance import getpystone
67from sabnzbd.utils.internetspeed import internetspeed
68import sabnzbd.utils.ssdp
69from sabnzbd.constants import MEBI, DEF_SKIN_COLORS, DEF_STDCONFIG, DEF_MAIN_TMPL, DEFAULT_PRIORITY, CHEETAH_DIRECTIVES
70from sabnzbd.lang import list_languages
71from sabnzbd.api import (
72    list_scripts,
73    list_cats,
74    del_from_section,
75    api_handler,
76    build_queue,
77    build_status,
78    retry_job,
79    build_header,
80    build_history,
81    del_hist_job,
82    Ttemplate,
83    build_queue_header,
84)
85
86##############################################################################
87# Security functions
88##############################################################################
89_MSG_ACCESS_DENIED = "Access denied"
90_MSG_ACCESS_DENIED_CONFIG_LOCK = "Access denied - Configuration locked"
91_MSG_ACCESS_DENIED_HOSTNAME = "Access denied - Hostname verification failed: https://sabnzbd.org/hostname-check"
92_MSG_MISSING_AUTH = "Missing authentication"
93_MSG_APIKEY_REQUIRED = "API Key Required"
94_MSG_APIKEY_INCORRECT = "API Key Incorrect"
95
96
97def secured_expose(
98    wrap_func: Optional[Callable] = None,
99    check_configlock: bool = False,
100    check_for_login: bool = True,
101    check_api_key: bool = False,
102    access_type: int = 4,
103) -> Union[Callable, str]:
104    """Wrapper for both cherrypy.expose and login/access check"""
105    if not wrap_func:
106        return functools.partial(
107            secured_expose,
108            check_configlock=check_configlock,
109            check_for_login=check_for_login,
110            check_api_key=check_api_key,
111            access_type=access_type,
112        )
113
114    # Expose to cherrypy
115    wrap_func.exposed = True
116
117    @functools.wraps(wrap_func)
118    def internal_wrap(*args, **kwargs):
119        # Label for logging in this and other functions, handling X-Forwarded-For
120        # The cherrypy.request object allows adding custom attributes
121        if cherrypy.request.headers.get("X-Forwarded-For"):
122            cherrypy.request.remote_label = "%s (X-Forwarded-For: %s) [%s]" % (
123                cherrypy.request.remote.ip,
124                cherrypy.request.headers.get("X-Forwarded-For"),
125                cherrypy.request.headers.get("User-Agent"),
126            )
127        else:
128            cherrypy.request.remote_label = "%s [%s]" % (
129                cherrypy.request.remote.ip,
130                cherrypy.request.headers.get("User-Agent"),
131            )
132
133        # Log all requests
134        if cfg.api_logging():
135            logging.debug(
136                "Request %s %s from %s %s",
137                cherrypy.request.method,
138                cherrypy.request.path_info,
139                cherrypy.request.remote_label,
140                kwargs,
141            )
142
143        # Add X-Frame-Headers headers to page-requests
144        if cfg.x_frame_options():
145            cherrypy.response.headers["X-Frame-Options"] = "SameOrigin"
146
147        # Check if config is locked
148        if check_configlock and cfg.configlock():
149            cherrypy.response.status = 403
150            return _MSG_ACCESS_DENIED_CONFIG_LOCK
151
152        # Check if external access and if it's allowed
153        if not check_access(access_type=access_type, warn_user=True):
154            cherrypy.response.status = 403
155            return _MSG_ACCESS_DENIED
156
157        # Verify login status, only for non-key pages
158        if check_for_login and not check_api_key and not check_login():
159            raise Raiser("/login/")
160
161        # Verify host used for the visit
162        if not check_hostname():
163            cherrypy.response.status = 403
164            return _MSG_ACCESS_DENIED_HOSTNAME
165
166        # Some pages need correct API key
167        if check_api_key:
168            msg = check_apikey(kwargs)
169            if msg:
170                cherrypy.response.status = 403
171                return msg
172
173        # All good, cool!
174        return wrap_func(*args, **kwargs)
175
176    return internal_wrap
177
178
179def check_access(access_type: int = 4, warn_user: bool = False) -> bool:
180    """Check if external address is allowed given access_type:
181    1=nzb
182    2=api
183    3=full_api
184    4=webui
185    5=webui with login for external
186    """
187    # Easy, it's allowed
188    if access_type <= cfg.inet_exposure():
189        return True
190
191    remote_ip = cherrypy.request.remote.ip
192
193    # Check for localhost
194    if is_loopback_addr(remote_ip):
195        return True
196
197    is_allowed = False
198    if not cfg.local_ranges():
199        # No local ranges defined, allow all private addresses by default
200        is_allowed = is_lan_addr(remote_ip)
201    else:
202        is_allowed = any(ip_in_subnet(remote_ip, r) for r in cfg.local_ranges())
203
204    if not is_allowed and warn_user:
205        log_warning_and_ip(T("Refused connection from:"))
206    return is_allowed
207
208
209def check_hostname():
210    """Check if hostname is allowed, to mitigate DNS-rebinding attack.
211    Similar to CVE-2019-5702, we need to add protection even
212    if only allowed to be accessed via localhost.
213    """
214    # If login is enabled, no API-key can be deducted
215    if cfg.username() and cfg.password():
216        return True
217
218    # Don't allow requests without Host
219    host = cherrypy.request.headers.get("Host")
220    if not host:
221        return False
222
223    # Remove the port-part (like ':8080'), if it is there, always on the right hand side.
224    # Not to be confused with IPv6 colons (within square brackets)
225    host = re.sub(":[0123456789]+$", "", host).lower()
226
227    # Fine if localhost or IP
228    if host == "localhost" or is_ipv4_addr(host) or is_ipv6_addr(host):
229        return True
230
231    # Check on the whitelist
232    if host in cfg.host_whitelist():
233        return True
234
235    # Fine if ends with ".local" or ".local.", aka mDNS name
236    # See rfc6762 Multicast DNS
237    if host.endswith((".local", ".local.")):
238        return True
239
240    # Ohoh, bad
241    log_warning_and_ip(T('Refused connection with hostname "%s" from:') % host)
242    return False
243
244
245# Create a more unique ID for each instance
246COOKIE_SECRET = str(randint(1000, 100000) * os.getpid())
247
248
249def set_login_cookie(remove=False, remember_me=False):
250    """We try to set a cookie as unique as possible
251    to the current user. Based on it's IP and the
252    current process ID of the SAB instance and a random
253    number, so cookies cannot be re-used
254    """
255    salt = randint(1, 1000)
256    cookie_str = utob(str(salt) + cherrypy.request.remote.ip + COOKIE_SECRET)
257    cherrypy.response.cookie["login_cookie"] = hashlib.sha1(cookie_str).hexdigest()
258    cherrypy.response.cookie["login_cookie"]["path"] = "/"
259    cherrypy.response.cookie["login_cookie"]["httponly"] = 1
260    cherrypy.response.cookie["login_salt"] = salt
261    cherrypy.response.cookie["login_salt"]["path"] = "/"
262    cherrypy.response.cookie["login_salt"]["httponly"] = 1
263
264    # If we want to be remembered
265    if remember_me:
266        cherrypy.response.cookie["login_cookie"]["max-age"] = 3600 * 24 * 14
267        cherrypy.response.cookie["login_salt"]["max-age"] = 3600 * 24 * 14
268
269    # To remove
270    if remove:
271        cherrypy.response.cookie["login_cookie"]["expires"] = 0
272        cherrypy.response.cookie["login_salt"]["expires"] = 0
273    else:
274        # Notify about new login
275        notifier.send_notification(T("User logged in"), T("User logged in to the web interface"), "new_login")
276
277
278def check_login_cookie():
279    # Do we have everything?
280    if "login_cookie" not in cherrypy.request.cookie or "login_salt" not in cherrypy.request.cookie:
281        return False
282
283    cookie_str = utob(str(cherrypy.request.cookie["login_salt"].value) + cherrypy.request.remote.ip + COOKIE_SECRET)
284    return cherrypy.request.cookie["login_cookie"].value == hashlib.sha1(cookie_str).hexdigest()
285
286
287def check_login():
288    # Not when no authentication required or basic-auth is on
289    if not cfg.html_login() or not cfg.username() or not cfg.password():
290        return True
291
292    # If we show login for external IP, by using access_type=6 we can check if IP match
293    if cfg.inet_exposure() == 5 and check_access(access_type=6):
294        return True
295
296    # Check the cookie
297    return check_login_cookie()
298
299
300def check_basic_auth(_, username, password):
301    """CherryPy basic authentication validation"""
302    return username == cfg.username() and password == cfg.password()
303
304
305def set_auth(conf):
306    """Set the authentication for CherryPy"""
307    if cfg.username() and cfg.password() and not cfg.html_login():
308        conf.update(
309            {
310                "tools.auth_basic.on": True,
311                "tools.auth_basic.realm": "SABnzbd",
312                "tools.auth_basic.checkpassword": check_basic_auth,
313            }
314        )
315        conf.update(
316            {
317                "/api": {"tools.auth_basic.on": False},
318                "%s/api" % cfg.url_base(): {"tools.auth_basic.on": False},
319            }
320        )
321    else:
322        conf.update({"tools.auth_basic.on": False})
323
324
325def check_apikey(kwargs):
326    """Check API-key or NZB-key
327    Return None when OK, otherwise an error message
328    """
329    mode = kwargs.get("mode", "")
330    name = kwargs.get("name", "")
331
332    # Lookup required access level for the specific api-call
333    req_access = sabnzbd.api.api_level(mode, name)
334    if not check_access(req_access, warn_user=True):
335        return _MSG_ACCESS_DENIED
336
337    # Skip for auth and version calls
338    if mode in ("version", "auth"):
339        return None
340
341    # First check API-key, if OK that's sufficient
342    if not cfg.disable_key():
343        key = kwargs.get("apikey")
344        if not key:
345            log_warning_and_ip(
346                T("API Key missing, please enter the api key from Config->General into your 3rd party program:")
347            )
348            return _MSG_APIKEY_REQUIRED
349        elif req_access == 1 and key == cfg.nzb_key():
350            return None
351        elif key == cfg.api_key():
352            return None
353        else:
354            log_warning_and_ip(T("API Key incorrect, Use the api key from Config->General in your 3rd party program:"))
355            return _MSG_APIKEY_INCORRECT
356
357    # No active API-key, check web credentials instead
358    if cfg.username() and cfg.password():
359        if check_login() or (
360            kwargs.get("ma_username") == cfg.username() and kwargs.get("ma_password") == cfg.password()
361        ):
362            pass
363        else:
364            log_warning_and_ip(
365                T(
366                    "Authentication missing, please enter username/password from Config->General into your 3rd party program:"
367                )
368            )
369            return _MSG_MISSING_AUTH
370    return None
371
372
373def log_warning_and_ip(txt):
374    """Include the IP and the Proxy-IP for warnings"""
375    if cfg.api_warnings():
376        logging.warning("%s %s", txt, cherrypy.request.remote_label)
377
378
379##############################################################################
380# Helper raiser functions
381##############################################################################
382def Raiser(root: str = "", **kwargs):
383    # Add extras
384    if kwargs:
385        root = "%s?%s" % (root, urllib.parse.urlencode(kwargs))
386
387    # Optionally add the leading /sabnzbd/ (or what the user set)
388    if not root.startswith(cfg.url_base()):
389        root = cherrypy.request.script_name + root
390
391    # Log the redirect
392    if cfg.api_logging():
393        logging.debug("Request %s %s redirected to %s", cherrypy.request.method, cherrypy.request.path_info, root)
394
395    # Send the redirect
396    return cherrypy.HTTPRedirect(root)
397
398
399def queueRaiser(root, kwargs):
400    return Raiser(root, start=kwargs.get("start"), limit=kwargs.get("limit"), search=kwargs.get("search"))
401
402
403def rssRaiser(root, kwargs):
404    return Raiser(root, feed=kwargs.get("feed"))
405
406
407##############################################################################
408# Page definitions
409##############################################################################
410class MainPage:
411    def __init__(self):
412        self.__root = "/"
413
414        # Add all sub-pages
415        self.login = LoginPage()
416        self.queue = QueuePage("/queue/")
417        self.history = HistoryPage("/history/")
418        self.status = Status("/status/")
419        self.config = ConfigPage("/config/")
420        self.nzb = NzoPage("/nzb/")
421        self.wizard = Wizard("/wizard/")
422
423    @secured_expose
424    def index(self, **kwargs):
425        # Redirect to wizard if no servers are set
426        if kwargs.get("skip_wizard") or config.get_servers():
427            info = build_header()
428
429            info["scripts"] = list_scripts(default=True)
430            info["script"] = "Default"
431
432            info["cat"] = "Default"
433            info["categories"] = list_cats(True)
434            info["have_rss_defined"] = bool(config.get_rss())
435            info["have_watched_dir"] = bool(cfg.dirscan_dir())
436
437            # Have logout only with HTML and if inet=5, only when we are external
438            info["have_logout"] = (
439                cfg.username()
440                and cfg.password()
441                and (
442                    cfg.html_login()
443                    and (cfg.inet_exposure() < 5 or (cfg.inet_exposure() == 5 and not check_access(access_type=6)))
444                )
445            )
446
447            bytespersec_list = sabnzbd.BPSMeter.get_bps_list()
448            info["bytespersec_list"] = ",".join([str(bps) for bps in bytespersec_list])
449
450            template = Template(
451                file=os.path.join(sabnzbd.WEB_DIR, "main.tmpl"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
452            )
453            return template.respond()
454        else:
455            # Redirect to the setup wizard
456            raise cherrypy.HTTPRedirect("%s/wizard/" % cfg.url_base())
457
458    @secured_expose(check_api_key=True)
459    def shutdown(self, **kwargs):
460        # Check for PID
461        pid_in = kwargs.get("pid")
462        if pid_in and int(pid_in) != os.getpid():
463            return "Incorrect PID for this instance, remove PID from URL to initiate shutdown."
464
465        sabnzbd.shutdown_program()
466        return T("SABnzbd shutdown finished")
467
468    @secured_expose(check_api_key=True)
469    def pause(self, **kwargs):
470        sabnzbd.Scheduler.plan_resume(0)
471        sabnzbd.Downloader.pause()
472        raise Raiser(self.__root)
473
474    @secured_expose(check_api_key=True)
475    def resume(self, **kwargs):
476        sabnzbd.Scheduler.plan_resume(0)
477        sabnzbd.unpause_all()
478        raise Raiser(self.__root)
479
480    @secured_expose(check_api_key=True, access_type=1)
481    def api(self, **kwargs):
482        """Redirect to API-handler, we check the access_type in the API-handler"""
483        return api_handler(kwargs)
484
485    @secured_expose
486    def scriptlog(self, **kwargs):
487        """Needed for all skins, URL is fixed due to postproc"""
488        # No session key check, due to fixed URLs
489        name = kwargs.get("name")
490        if name:
491            history_db = sabnzbd.get_db_connection()
492            return ShowString(history_db.get_name(name), history_db.get_script_log(name))
493        else:
494            raise Raiser(self.__root)
495
496    @secured_expose(check_api_key=True)
497    def retry(self, **kwargs):
498        """Duplicate of retry of History, needed for some skins"""
499        job = kwargs.get("job", "")
500        url = kwargs.get("url", "").strip()
501        pp = kwargs.get("pp")
502        cat = kwargs.get("cat")
503        script = kwargs.get("script")
504        if url:
505            sabnzbd.add_url(url, pp, script, cat, nzbname=kwargs.get("nzbname"))
506        del_hist_job(job, del_files=True)
507        raise Raiser(self.__root)
508
509    @secured_expose(check_api_key=True)
510    def retry_pp(self, **kwargs):
511        # Duplicate of History/retry_pp to please the SMPL skin :(
512        retry_job(kwargs.get("job"), kwargs.get("nzbfile"), kwargs.get("password"))
513        raise Raiser(self.__root)
514
515    @secured_expose
516    def robots_txt(self, **kwargs):
517        """Keep web crawlers out"""
518        cherrypy.response.headers["Content-Type"] = "text/plain"
519        return "User-agent: *\nDisallow: /\n"
520
521    @secured_expose
522    def description_xml(self, **kwargs):
523        """Provide the description.xml which was broadcast via SSDP"""
524        if is_lan_addr(cherrypy.request.remote.ip):
525            cherrypy.response.headers["Content-Type"] = "application/xml"
526            return utob(sabnzbd.utils.ssdp.server_ssdp_xml())
527        else:
528            return None
529
530
531##############################################################################
532class Wizard:
533    def __init__(self, root):
534        self.__root = root
535
536    @secured_expose(check_configlock=True)
537    def index(self, **kwargs):
538        """Show the language selection page"""
539        if sabnzbd.WIN32:
540            from sabnzbd.utils.apireg import get_install_lng
541
542            cfg.language.set(get_install_lng())
543            logging.debug('Installer language code "%s"', cfg.language())
544
545        info = build_header(sabnzbd.WIZARD_DIR)
546        info["languages"] = list_languages()
547        template = Template(
548            file=os.path.join(sabnzbd.WIZARD_DIR, "index.html"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
549        )
550        return template.respond()
551
552    @secured_expose(check_configlock=True)
553    def one(self, **kwargs):
554        """Accept language and show server page"""
555        if kwargs.get("lang"):
556            cfg.language.set(kwargs.get("lang"))
557
558        # Always setup Glitter
559        change_web_dir("Glitter - Auto")
560
561        info = build_header(sabnzbd.WIZARD_DIR)
562        info["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION
563
564        # Just in case, add server
565        servers = config.get_servers()
566        if not servers:
567            info["host"] = ""
568            info["port"] = ""
569            info["username"] = ""
570            info["password"] = ""
571            info["connections"] = ""
572            info["ssl"] = 0
573            info["ssl_verify"] = 2
574        else:
575            # Sort servers to get the first enabled one
576            server_names = sorted(
577                servers,
578                key=lambda svr: "%d%02d%s"
579                % (int(not servers[svr].enable()), servers[svr].priority(), servers[svr].displayname().lower()),
580            )
581            for server in server_names:
582                # If there are multiple servers, just use the first enabled one
583                s = servers[server]
584                info["host"] = s.host()
585                info["port"] = s.port()
586                info["username"] = s.username()
587                info["password"] = s.password.get_stars()
588                info["connections"] = s.connections()
589                info["ssl"] = s.ssl()
590                info["ssl_verify"] = s.ssl_verify()
591                if s.enable():
592                    break
593        template = Template(
594            file=os.path.join(sabnzbd.WIZARD_DIR, "one.html"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
595        )
596        return template.respond()
597
598    @secured_expose(check_configlock=True)
599    def two(self, **kwargs):
600        """Accept server and show the final page for restart"""
601        # Save server details
602        if kwargs:
603            kwargs["enable"] = 1
604            handle_server(kwargs)
605
606        config.save_config()
607
608        # Show Restart screen
609        info = build_header(sabnzbd.WIZARD_DIR)
610
611        info["access_url"], info["urls"] = get_access_info()
612        info["download_dir"] = cfg.download_dir.get_clipped_path()
613        info["complete_dir"] = cfg.complete_dir.get_clipped_path()
614
615        template = Template(
616            file=os.path.join(sabnzbd.WIZARD_DIR, "two.html"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
617        )
618        return template.respond()
619
620    @secured_expose
621    def exit(self, **kwargs):
622        """Stop SABnzbd"""
623        sabnzbd.shutdown_program()
624        return T("SABnzbd shutdown finished")
625
626
627def get_access_info():
628    """Build up a list of url's that sabnzbd can be accessed from"""
629    # Access_url is used to provide the user a link to SABnzbd depending on the host
630    cherryhost = cfg.cherryhost()
631    host = socket.gethostname().lower()
632    socks = [host]
633
634    if cherryhost == "0.0.0.0":
635        # Grab a list of all ips for the hostname
636        try:
637            addresses = socket.getaddrinfo(host, None)
638        except:
639            addresses = []
640        for addr in addresses:
641            address = addr[4][0]
642            # Filter out ipv6 addresses (should not be allowed)
643            if ":" not in address and address not in socks:
644                socks.append(address)
645        socks.insert(0, "localhost")
646    elif cherryhost == "::":
647        # Grab a list of all ips for the hostname
648        addresses = socket.getaddrinfo(host, None)
649        for addr in addresses:
650            address = addr[4][0]
651            # Only ipv6 addresses will work
652            if ":" in address:
653                address = "[%s]" % address
654                if address not in socks:
655                    socks.append(address)
656        socks.insert(0, "localhost")
657    elif cherryhost:
658        socks = [cherryhost]
659
660    # Add the current requested URL as the base
661    access_url = urllib.parse.urljoin(cherrypy.request.base, cfg.url_base())
662
663    urls = [access_url]
664    for sock in socks:
665        if sock:
666            if cfg.enable_https() and cfg.https_port():
667                url = "https://%s:%s%s" % (sock, cfg.https_port(), cfg.url_base())
668            elif cfg.enable_https():
669                url = "https://%s:%s%s" % (sock, cfg.cherryport(), cfg.url_base())
670            else:
671                url = "http://%s:%s%s" % (sock, cfg.cherryport(), cfg.url_base())
672            urls.append(url)
673
674    # Return a unique list
675    return access_url, set(urls)
676
677
678##############################################################################
679class LoginPage:
680    @secured_expose(check_for_login=False)
681    def index(self, **kwargs):
682        # Base output var
683        info = build_header(sabnzbd.WEB_DIR_CONFIG)
684        info["error"] = ""
685
686        # Logout?
687        if kwargs.get("logout"):
688            set_login_cookie(remove=True)
689            raise Raiser()
690
691        # Check if there's even a username/password set
692        if check_login():
693            raise Raiser(cherrypy.request.script_name + "/")
694
695        # Check login info
696        if kwargs.get("username") == cfg.username() and kwargs.get("password") == cfg.password():
697            # Save login cookie
698            set_login_cookie(remember_me=kwargs.get("remember_me", False))
699            # Log the success
700            logging.info("Successful login from %s", cherrypy.request.remote_label)
701            # Redirect
702            raise Raiser(cherrypy.request.script_name + "/")
703        elif kwargs.get("username") or kwargs.get("password"):
704            info["error"] = T("Authentication failed, check username/password.")
705            # Warn about the potential security problem
706            logging.warning(T("Unsuccessful login attempt from %s"), cherrypy.request.remote_label)
707
708        # Show login
709        template = Template(
710            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "login", "main.tmpl"),
711            searchList=[info],
712            compilerSettings=CHEETAH_DIRECTIVES,
713        )
714        return template.respond()
715
716
717##############################################################################
718class NzoPage:
719    def __init__(self, root):
720        self.__root = root
721        self.__cached_selection = {}  # None
722
723    @secured_expose
724    def default(self, *args, **kwargs):
725        # Allowed URL's
726        # /nzb/SABnzbd_nzo_xxxxx/
727        # /nzb/SABnzbd_nzo_xxxxx/details
728        # /nzb/SABnzbd_nzo_xxxxx/files
729        # /nzb/SABnzbd_nzo_xxxxx/bulk_operation
730        # /nzb/SABnzbd_nzo_xxxxx/save
731        nzo_id = None
732        for a in args:
733            if a.startswith("SABnzbd_nzo"):
734                nzo_id = a
735                break
736
737        nzo = sabnzbd.NzbQueue.get_nzo(nzo_id)
738        if nzo_id and nzo:
739            info, pnfo_list, bytespersec, q_size, bytes_left_previous_page = build_queue_header()
740
741            # /SABnzbd_nzo_xxxxx/bulk_operation
742            if "bulk_operation" in args:
743                return self.bulk_operation(nzo_id, kwargs)
744
745            # /SABnzbd_nzo_xxxxx/details
746            elif "details" in args:
747                info = self.nzo_details(info, pnfo_list, nzo_id)
748
749            # /SABnzbd_nzo_xxxxx/files
750            elif "files" in args:
751                info = self.nzo_files(info, nzo_id)
752
753            # /SABnzbd_nzo_xxxxx/save
754            elif "save" in args:
755                self.save_details(nzo_id, args, kwargs)
756                return  # never reached
757
758            # /SABnzbd_nzo_xxxxx/
759            else:
760                info = self.nzo_details(info, pnfo_list, nzo_id)
761                info = self.nzo_files(info, nzo_id)
762
763            template = Template(
764                file=os.path.join(sabnzbd.WEB_DIR, "nzo.tmpl"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
765            )
766            return template.respond()
767        else:
768            # Job no longer exists, go to main page
769            raise Raiser(urllib.parse.urljoin(self.__root, "../queue/"))
770
771    def nzo_details(self, info, pnfo_list, nzo_id):
772        slot = {}
773        n = 0
774        for pnfo in pnfo_list:
775            if pnfo.nzo_id == nzo_id:
776                nzo = sabnzbd.NzbQueue.get_nzo(nzo_id)
777                repair = pnfo.repair
778                unpack = pnfo.unpack
779                delete = pnfo.delete
780                unpackopts = opts_to_pp(repair, unpack, delete)
781                script = pnfo.script
782                if script is None:
783                    script = "None"
784                cat = pnfo.category
785                if not cat:
786                    cat = "None"
787
788                slot["nzo_id"] = str(nzo_id)
789                slot["cat"] = cat
790                slot["filename"] = nzo.final_name
791                slot["filename_clean"] = nzo.final_name
792                slot["password"] = nzo.password or ""
793                slot["script"] = script
794                slot["priority"] = str(pnfo.priority)
795                slot["unpackopts"] = str(unpackopts)
796                info["index"] = n
797                break
798            n += 1
799
800        info["slot"] = slot
801        info["scripts"] = list_scripts()
802        info["categories"] = list_cats()
803        info["noofslots"] = len(pnfo_list)
804
805        return info
806
807    def nzo_files(self, info, nzo_id):
808        active = []
809        nzo = sabnzbd.NzbQueue.get_nzo(nzo_id)
810        if nzo:
811            pnfo = nzo.gather_info(full=True)
812            info["nzo_id"] = pnfo.nzo_id
813            info["filename"] = pnfo.filename
814
815            for nzf in pnfo.active_files:
816                checked = False
817                if nzf.nzf_id in self.__cached_selection and self.__cached_selection[nzf.nzf_id] == "on":
818                    checked = True
819                active.append(
820                    {
821                        "filename": nzf.filename,
822                        "mbleft": "%.2f" % (nzf.bytes_left / MEBI),
823                        "mb": "%.2f" % (nzf.bytes / MEBI),
824                        "size": to_units(nzf.bytes, "B"),
825                        "sizeleft": to_units(nzf.bytes_left, "B"),
826                        "nzf_id": nzf.nzf_id,
827                        "age": calc_age(nzf.date),
828                        "checked": checked,
829                    }
830                )
831
832        info["active_files"] = active
833        return info
834
835    def save_details(self, nzo_id, args, kwargs):
836        index = kwargs.get("index", None)
837        name = kwargs.get("name", None)
838        password = kwargs.get("password", None)
839        if password == "":
840            password = None
841        pp = kwargs.get("pp", None)
842        script = kwargs.get("script", None)
843        cat = kwargs.get("cat", None)
844        priority = kwargs.get("priority", None)
845        nzo = sabnzbd.NzbQueue.get_nzo(nzo_id)
846
847        if index is not None:
848            sabnzbd.NzbQueue.switch(nzo_id, index)
849        if name is not None:
850            sabnzbd.NzbQueue.change_name(nzo_id, name, password)
851
852        if cat is not None and nzo.cat is not cat and not (nzo.cat == "*" and cat == "Default"):
853            sabnzbd.NzbQueue.change_cat(nzo_id, cat, priority)
854            # Category changed, so make sure "Default" attributes aren't set again
855            if script == "Default":
856                script = None
857            if priority == "Default":
858                priority = None
859            if pp == "Default":
860                pp = None
861
862        if script is not None and nzo.script != script:
863            sabnzbd.NzbQueue.change_script(nzo_id, script)
864        if pp is not None and nzo.pp != pp:
865            sabnzbd.NzbQueue.change_opts(nzo_id, pp)
866        if priority is not None and nzo.priority != int(priority):
867            sabnzbd.NzbQueue.set_priority(nzo_id, priority)
868
869        raise Raiser(urllib.parse.urljoin(self.__root, "../queue/"))
870
871    def bulk_operation(self, nzo_id, kwargs):
872        self.__cached_selection = kwargs
873        if kwargs["action_key"] == "Delete":
874            for key in kwargs:
875                if kwargs[key] == "on":
876                    sabnzbd.NzbQueue.remove_nzf(nzo_id, key, force_delete=True)
877
878        elif kwargs["action_key"] in ("Top", "Up", "Down", "Bottom"):
879            nzf_ids = []
880            for key in kwargs:
881                if kwargs[key] == "on":
882                    nzf_ids.append(key)
883            size = int_conv(kwargs.get("action_size", 1))
884            if kwargs["action_key"] == "Top":
885                sabnzbd.NzbQueue.move_top_bulk(nzo_id, nzf_ids)
886            elif kwargs["action_key"] == "Up":
887                sabnzbd.NzbQueue.move_up_bulk(nzo_id, nzf_ids, size)
888            elif kwargs["action_key"] == "Down":
889                sabnzbd.NzbQueue.move_down_bulk(nzo_id, nzf_ids, size)
890            elif kwargs["action_key"] == "Bottom":
891                sabnzbd.NzbQueue.move_bottom_bulk(nzo_id, nzf_ids)
892
893        if sabnzbd.NzbQueue.get_nzo(nzo_id):
894            url = urllib.parse.urljoin(self.__root, nzo_id)
895        else:
896            url = urllib.parse.urljoin(self.__root, "../queue")
897        if url and not url.endswith("/"):
898            url += "/"
899        raise Raiser(url)
900
901
902##############################################################################
903class QueuePage:
904    def __init__(self, root):
905        self.__root = root
906
907    @secured_expose
908    def index(self, **kwargs):
909        start = int_conv(kwargs.get("start"))
910        limit = int_conv(kwargs.get("limit"))
911        search = kwargs.get("search")
912        info, _pnfo_list, _bytespersec = build_queue(start=start, limit=limit, trans=True, search=search)
913
914        template = Template(
915            file=os.path.join(sabnzbd.WEB_DIR, "queue.tmpl"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
916        )
917        return template.respond()
918
919    @secured_expose(check_api_key=True)
920    def delete(self, **kwargs):
921        uid = kwargs.get("uid")
922        del_files = int_conv(kwargs.get("del_files"))
923        if uid:
924            sabnzbd.NzbQueue.remove(uid, delete_all_data=del_files)
925        raise queueRaiser(self.__root, kwargs)
926
927    @secured_expose(check_api_key=True)
928    def purge(self, **kwargs):
929        sabnzbd.NzbQueue.remove_all(kwargs.get("search"))
930        raise queueRaiser(self.__root, kwargs)
931
932    @secured_expose(check_api_key=True)
933    def change_queue_complete_action(self, **kwargs):
934        """Action or script to be performed once the queue has been completed
935        Scripts are prefixed with 'script_'
936        """
937        action = kwargs.get("action")
938        sabnzbd.change_queue_complete_action(action)
939        raise queueRaiser(self.__root, kwargs)
940
941    @secured_expose(check_api_key=True)
942    def switch(self, **kwargs):
943        uid1 = kwargs.get("uid1")
944        uid2 = kwargs.get("uid2")
945        if uid1 and uid2:
946            sabnzbd.NzbQueue.switch(uid1, uid2)
947        raise queueRaiser(self.__root, kwargs)
948
949    @secured_expose(check_api_key=True)
950    def change_opts(self, **kwargs):
951        nzo_id = kwargs.get("nzo_id")
952        pp = kwargs.get("pp", "")
953        if nzo_id and pp and pp.isdigit():
954            sabnzbd.NzbQueue.change_opts(nzo_id, int(pp))
955        raise queueRaiser(self.__root, kwargs)
956
957    @secured_expose(check_api_key=True)
958    def change_script(self, **kwargs):
959        nzo_id = kwargs.get("nzo_id")
960        script = kwargs.get("script", "")
961        if nzo_id and script:
962            if script == "None":
963                script = None
964            sabnzbd.NzbQueue.change_script(nzo_id, script)
965        raise queueRaiser(self.__root, kwargs)
966
967    @secured_expose(check_api_key=True)
968    def change_cat(self, **kwargs):
969        nzo_id = kwargs.get("nzo_id")
970        cat = kwargs.get("cat", "")
971        if nzo_id and cat:
972            if cat == "None":
973                cat = None
974            sabnzbd.NzbQueue.change_cat(nzo_id, cat)
975
976        raise queueRaiser(self.__root, kwargs)
977
978    @secured_expose(check_api_key=True)
979    def shutdown(self, **kwargs):
980        sabnzbd.shutdown_program()
981        return T("SABnzbd shutdown finished")
982
983    @secured_expose(check_api_key=True)
984    def pause(self, **kwargs):
985        sabnzbd.Scheduler.plan_resume(0)
986        sabnzbd.Downloader.pause()
987        raise queueRaiser(self.__root, kwargs)
988
989    @secured_expose(check_api_key=True)
990    def resume(self, **kwargs):
991        sabnzbd.Scheduler.plan_resume(0)
992        sabnzbd.unpause_all()
993        raise queueRaiser(self.__root, kwargs)
994
995    @secured_expose(check_api_key=True)
996    def pause_nzo(self, **kwargs):
997        uid = kwargs.get("uid", "")
998        sabnzbd.NzbQueue.pause_multiple_nzo(uid.split(","))
999        raise queueRaiser(self.__root, kwargs)
1000
1001    @secured_expose(check_api_key=True)
1002    def resume_nzo(self, **kwargs):
1003        uid = kwargs.get("uid", "")
1004        sabnzbd.NzbQueue.resume_multiple_nzo(uid.split(","))
1005        raise queueRaiser(self.__root, kwargs)
1006
1007    @secured_expose(check_api_key=True)
1008    def set_priority(self, **kwargs):
1009        sabnzbd.NzbQueue.set_priority(kwargs.get("nzo_id"), kwargs.get("priority"))
1010        raise queueRaiser(self.__root, kwargs)
1011
1012    @secured_expose(check_api_key=True)
1013    def sort_by_avg_age(self, **kwargs):
1014        sabnzbd.NzbQueue.sort_queue("avg_age", kwargs.get("dir"))
1015        raise queueRaiser(self.__root, kwargs)
1016
1017    @secured_expose(check_api_key=True)
1018    def sort_by_name(self, **kwargs):
1019        sabnzbd.NzbQueue.sort_queue("name", kwargs.get("dir"))
1020        raise queueRaiser(self.__root, kwargs)
1021
1022    @secured_expose(check_api_key=True)
1023    def sort_by_size(self, **kwargs):
1024        sabnzbd.NzbQueue.sort_queue("size", kwargs.get("dir"))
1025        raise queueRaiser(self.__root, kwargs)
1026
1027
1028##############################################################################
1029class HistoryPage:
1030    def __init__(self, root):
1031        self.__root = root
1032
1033    @secured_expose
1034    def index(self, **kwargs):
1035        start = int_conv(kwargs.get("start"))
1036        limit = int_conv(kwargs.get("limit"))
1037        search = kwargs.get("search")
1038        failed_only = int_conv(kwargs.get("failed_only"))
1039
1040        history = build_header()
1041        history["failed_only"] = failed_only
1042        history["rating_enable"] = bool(cfg.rating_enable())
1043
1044        postfix = T("B")  # : Abbreviation for bytes, as in GB
1045        grand, month, week, day = sabnzbd.BPSMeter.get_sums()
1046        history["total_size"], history["month_size"], history["week_size"], history["day_size"] = (
1047            to_units(grand, postfix=postfix),
1048            to_units(month, postfix=postfix),
1049            to_units(week, postfix=postfix),
1050            to_units(day, postfix=postfix),
1051        )
1052
1053        history["lines"], history["fetched"], history["noofslots"] = build_history(
1054            start=start, limit=limit, search=search, failed_only=failed_only
1055        )
1056
1057        if search:
1058            history["search"] = escape(search)
1059        else:
1060            history["search"] = ""
1061
1062        history["start"] = int_conv(start)
1063        history["limit"] = int_conv(limit)
1064        history["finish"] = history["start"] + history["limit"]
1065        if history["finish"] > history["noofslots"]:
1066            history["finish"] = history["noofslots"]
1067        if not history["finish"]:
1068            history["finish"] = history["fetched"]
1069        history["time_format"] = time_format
1070
1071        template = Template(
1072            file=os.path.join(sabnzbd.WEB_DIR, "history.tmpl"),
1073            searchList=[history],
1074            compilerSettings=CHEETAH_DIRECTIVES,
1075        )
1076        return template.respond()
1077
1078    @secured_expose(check_api_key=True)
1079    def purge(self, **kwargs):
1080        history_db = sabnzbd.get_db_connection()
1081        history_db.remove_history()
1082        raise queueRaiser(self.__root, kwargs)
1083
1084    @secured_expose(check_api_key=True)
1085    def delete(self, **kwargs):
1086        job = kwargs.get("job")
1087        del_files = int_conv(kwargs.get("del_files"))
1088        if job:
1089            jobs = job.split(",")
1090            for job in jobs:
1091                del_hist_job(job, del_files=del_files)
1092        raise queueRaiser(self.__root, kwargs)
1093
1094    @secured_expose(check_api_key=True)
1095    def retry_pp(self, **kwargs):
1096        retry_job(kwargs.get("job"), kwargs.get("nzbfile"), kwargs.get("password"))
1097        raise queueRaiser(self.__root, kwargs)
1098
1099
1100##############################################################################
1101class ConfigPage:
1102    def __init__(self, root):
1103        self.__root = root
1104        self.folders = ConfigFolders("/config/folders/")
1105        self.notify = ConfigNotify("/config/notify/")
1106        self.general = ConfigGeneral("/config/general/")
1107        self.rss = ConfigRss("/config/rss/")
1108        self.scheduling = ConfigScheduling("/config/scheduling/")
1109        self.server = ConfigServer("/config/server/")
1110        self.switches = ConfigSwitches("/config/switches/")
1111        self.categories = ConfigCats("/config/categories/")
1112        self.sorting = ConfigSorting("/config/sorting/")
1113        self.special = ConfigSpecial("/config/special/")
1114
1115    @secured_expose(check_configlock=True)
1116    def index(self, **kwargs):
1117        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
1118        conf["configfn"] = clip_path(config.get_filename())
1119        conf["cmdline"] = sabnzbd.CMDLINE
1120        conf["build"] = sabnzbd.__baseline__[:7]
1121
1122        conf["have_unzip"] = bool(sabnzbd.newsunpack.ZIP_COMMAND)
1123        conf["have_7zip"] = bool(sabnzbd.newsunpack.SEVEN_COMMAND)
1124        conf["have_sabyenc"] = sabnzbd.decoder.SABYENC_ENABLED
1125        conf["have_mt_par2"] = sabnzbd.newsunpack.PAR2_MT
1126
1127        conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION
1128        conf["ssl_version"] = ssl.OPENSSL_VERSION
1129
1130        new = {}
1131        for svr in config.get_servers():
1132            new[svr] = {}
1133        conf["servers"] = new
1134
1135        conf["folders"] = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
1136
1137        template = Template(
1138            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config.tmpl"),
1139            searchList=[conf],
1140            compilerSettings=CHEETAH_DIRECTIVES,
1141        )
1142        return template.respond()
1143
1144    @secured_expose(check_api_key=True)
1145    def restart(self, **kwargs):
1146        logging.info("Restart requested by interface")
1147        # Do the shutdown async to still send goodbye to browser
1148        Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start()
1149        return T(
1150            '&nbsp<br />SABnzbd shutdown finished.<br />Wait for about 5 second and then click the button below.<br /><br /><strong><a href="..">Refresh</a></strong><br />'
1151        )
1152
1153    @secured_expose(check_api_key=True)
1154    def repair(self, **kwargs):
1155        logging.info("Queue repair requested by interface")
1156        sabnzbd.request_repair()
1157        # Do the shutdown async to still send goodbye to browser
1158        Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start()
1159        return T(
1160            '&nbsp<br />SABnzbd shutdown finished.<br />Wait for about 5 second and then click the button below.<br /><br /><strong><a href="..">Refresh</a></strong><br />'
1161        )
1162
1163
1164##############################################################################
1165LIST_DIRPAGE = (
1166    "download_dir",
1167    "download_free",
1168    "complete_dir",
1169    "complete_free",
1170    "admin_dir",
1171    "nzb_backup_dir",
1172    "dirscan_dir",
1173    "dirscan_speed",
1174    "script_dir",
1175    "email_dir",
1176    "permissions",
1177    "log_dir",
1178    "password_file",
1179)
1180
1181LIST_BOOL_DIRPAGE = ("fulldisk_autoresume",)
1182
1183
1184class ConfigFolders:
1185    def __init__(self, root):
1186        self.__root = root
1187
1188    @secured_expose(check_configlock=True)
1189    def index(self, **kwargs):
1190        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
1191
1192        for kw in LIST_DIRPAGE + LIST_BOOL_DIRPAGE:
1193            conf[kw] = config.get_config("misc", kw)()
1194
1195        template = Template(
1196            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_folders.tmpl"),
1197            searchList=[conf],
1198            compilerSettings=CHEETAH_DIRECTIVES,
1199        )
1200        return template.respond()
1201
1202    @secured_expose(check_api_key=True, check_configlock=True)
1203    def saveDirectories(self, **kwargs):
1204        for kw in LIST_DIRPAGE + LIST_BOOL_DIRPAGE:
1205            value = kwargs.get(kw)
1206            if value is not None or kw in LIST_BOOL_DIRPAGE:
1207                if kw in ("complete_dir", "dirscan_dir"):
1208                    msg = config.get_config("misc", kw).set(value, create=True)
1209                else:
1210                    msg = config.get_config("misc", kw).set(value)
1211                if msg:
1212                    # return sabnzbd.api.report('json', error=msg)
1213                    return badParameterResponse(msg, kwargs.get("ajax"))
1214
1215        if not sabnzbd.check_incomplete_vs_complete():
1216            return badParameterResponse(
1217                T("The Completed Download Folder cannot be the same or a subfolder of the Temporary Download Folder"),
1218                kwargs.get("ajax"),
1219            )
1220        config.save_config()
1221        if kwargs.get("ajax"):
1222            return sabnzbd.api.report("json")
1223        else:
1224            raise Raiser(self.__root)
1225
1226
1227##############################################################################
1228SWITCH_LIST = (
1229    "par_option",
1230    "top_only",
1231    "direct_unpack",
1232    "enable_meta",
1233    "win_process_prio",
1234    "auto_sort",
1235    "propagation_delay",
1236    "auto_disconnect",
1237    "flat_unpack",
1238    "safe_postproc",
1239    "no_dupes",
1240    "replace_spaces",
1241    "replace_dots",
1242    "ignore_samples",
1243    "pause_on_post_processing",
1244    "nice",
1245    "ionice",
1246    "pre_script",
1247    "pause_on_pwrar",
1248    "sfv_check",
1249    "deobfuscate_final_filenames",
1250    "folder_rename",
1251    "load_balancing",
1252    "quota_size",
1253    "quota_day",
1254    "quota_resume",
1255    "quota_period",
1256    "history_retention",
1257    "pre_check",
1258    "max_art_tries",
1259    "fail_hopeless_jobs",
1260    "enable_all_par",
1261    "enable_recursive",
1262    "no_series_dupes",
1263    "series_propercheck",
1264    "script_can_fail",
1265    "new_nzb_on_failure",
1266    "unwanted_extensions",
1267    "action_on_unwanted_extensions",
1268    "unwanted_extensions_mode",
1269    "sanitize_safe",
1270    "rating_enable",
1271    "rating_api_key",
1272    "rating_filter_enable",
1273    "rating_filter_abort_audio",
1274    "rating_filter_abort_video",
1275    "rating_filter_abort_encrypted",
1276    "rating_filter_abort_encrypted_confirm",
1277    "rating_filter_abort_spam",
1278    "rating_filter_abort_spam_confirm",
1279    "rating_filter_abort_downvoted",
1280    "rating_filter_abort_keywords",
1281    "rating_filter_pause_audio",
1282    "rating_filter_pause_video",
1283    "rating_filter_pause_encrypted",
1284    "rating_filter_pause_encrypted_confirm",
1285    "rating_filter_pause_spam",
1286    "rating_filter_pause_spam_confirm",
1287    "rating_filter_pause_downvoted",
1288    "rating_filter_pause_keywords",
1289)
1290
1291
1292class ConfigSwitches:
1293    def __init__(self, root):
1294        self.__root = root
1295
1296    @secured_expose(check_configlock=True)
1297    def index(self, **kwargs):
1298        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
1299
1300        conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION
1301        conf["have_nice"] = bool(sabnzbd.newsunpack.NICE_COMMAND)
1302        conf["have_ionice"] = bool(sabnzbd.newsunpack.IONICE_COMMAND)
1303        conf["cleanup_list"] = cfg.cleanup_list.get_string()
1304
1305        for kw in SWITCH_LIST:
1306            conf[kw] = config.get_config("misc", kw)()
1307        conf["unwanted_extensions"] = cfg.unwanted_extensions.get_string()
1308
1309        conf["scripts"] = list_scripts() or ["None"]
1310
1311        template = Template(
1312            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_switches.tmpl"),
1313            searchList=[conf],
1314            compilerSettings=CHEETAH_DIRECTIVES,
1315        )
1316        return template.respond()
1317
1318    @secured_expose(check_api_key=True, check_configlock=True)
1319    def saveSwitches(self, **kwargs):
1320        for kw in SWITCH_LIST:
1321            item = config.get_config("misc", kw)
1322            value = kwargs.get(kw)
1323            if kw == "unwanted_extensions" and value:
1324                value = value.lower().replace(".", "")
1325            msg = item.set(value)
1326            if msg:
1327                return badParameterResponse(msg, kwargs.get("ajax"))
1328
1329        cleanup_list = kwargs.get("cleanup_list")
1330        if cleanup_list and sabnzbd.WIN32:
1331            cleanup_list = cleanup_list.lower()
1332        cfg.cleanup_list.set(cleanup_list)
1333
1334        config.save_config()
1335        if kwargs.get("ajax"):
1336            return sabnzbd.api.report("json")
1337        else:
1338            raise Raiser(self.__root)
1339
1340
1341##############################################################################
1342SPECIAL_BOOL_LIST = (
1343    "start_paused",
1344    "no_penalties",
1345    "fast_fail",
1346    "overwrite_files",
1347    "enable_par_cleanup",
1348    "queue_complete_pers",
1349    "api_warnings",
1350    "helpfull_warnings",
1351    "ampm",
1352    "enable_unrar",
1353    "enable_unzip",
1354    "enable_7zip",
1355    "enable_filejoin",
1356    "enable_tsjoin",
1357    "ignore_unrar_dates",
1358    "osx_menu",
1359    "osx_speed",
1360    "win_menu",
1361    "allow_incomplete_nzb",
1362    "rss_filenames",
1363    "ipv6_hosting",
1364    "keep_awake",
1365    "empty_postproc",
1366    "html_login",
1367    "wait_for_dfolder",
1368    "enable_broadcast",
1369    "warn_dupl_jobs",
1370    "replace_illegal",
1371    "backup_for_duplicates",
1372    "disable_api_key",
1373    "api_logging",
1374    "x_frame_options",
1375    "require_modern_tls",
1376)
1377SPECIAL_VALUE_LIST = (
1378    "downloader_sleep_time",
1379    "size_limit",
1380    "movie_rename_limit",
1381    "nomedia_marker",
1382    "max_url_retries",
1383    "req_completion_rate",
1384    "wait_ext_drive",
1385    "max_foldername_length",
1386    "show_sysload",
1387    "url_base",
1388    "num_decoders",
1389    "direct_unpack_threads",
1390    "ipv6_servers",
1391    "selftest_host",
1392    "rating_host",
1393    "ssdp_broadcast_interval",
1394)
1395SPECIAL_LIST_LIST = (
1396    "rss_odd_titles",
1397    "quick_check_ext_ignore",
1398    "host_whitelist",
1399    "local_ranges",
1400)
1401
1402
1403class ConfigSpecial:
1404    def __init__(self, root):
1405        self.__root = root
1406
1407    @secured_expose(check_configlock=True)
1408    def index(self, **kwargs):
1409        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
1410        conf["switches"] = [
1411            (kw, config.get_config("misc", kw)(), config.get_config("misc", kw).default()) for kw in SPECIAL_BOOL_LIST
1412        ]
1413        conf["entries"] = [
1414            (kw, config.get_config("misc", kw)(), config.get_config("misc", kw).default()) for kw in SPECIAL_VALUE_LIST
1415        ]
1416        conf["entries"].extend(
1417            [
1418                (kw, config.get_config("misc", kw).get_string(), config.get_config("misc", kw).default_string())
1419                for kw in SPECIAL_LIST_LIST
1420            ]
1421        )
1422
1423        template = Template(
1424            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_special.tmpl"),
1425            searchList=[conf],
1426            compilerSettings=CHEETAH_DIRECTIVES,
1427        )
1428        return template.respond()
1429
1430    @secured_expose(check_api_key=True, check_configlock=True)
1431    def saveSpecial(self, **kwargs):
1432        for kw in SPECIAL_BOOL_LIST + SPECIAL_VALUE_LIST + SPECIAL_LIST_LIST:
1433            item = config.get_config("misc", kw)
1434            value = kwargs.get(kw)
1435            msg = item.set(value)
1436            if msg:
1437                return badParameterResponse(msg)
1438
1439        config.save_config()
1440        raise Raiser(self.__root)
1441
1442
1443##############################################################################
1444GENERAL_LIST = (
1445    "host",
1446    "port",
1447    "username",
1448    "refresh_rate",
1449    "language",
1450    "cache_limit",
1451    "inet_exposure",
1452    "enable_https",
1453    "https_port",
1454    "https_cert",
1455    "https_key",
1456    "https_chain",
1457    "enable_https_verification",
1458    "auto_browser",
1459    "check_new_rel",
1460)
1461
1462
1463class ConfigGeneral:
1464    def __init__(self, root):
1465        self.__root = root
1466
1467    @secured_expose(check_configlock=True)
1468    def index(self, **kwargs):
1469        def ListColors(web_dir):
1470            lst = []
1471            web_dir = os.path.join(sabnzbd.DIR_INTERFACES, web_dir)
1472            dd = os.path.abspath(web_dir + "/templates/static/stylesheets/colorschemes")
1473            if (not dd) or (not os.access(dd, os.R_OK)):
1474                return lst
1475            for color in globber(dd):
1476                col = color.replace(".css", "")
1477                lst.append(col)
1478            return lst
1479
1480        def add_color(skin_dir, color):
1481            if skin_dir:
1482                if not color:
1483                    try:
1484                        color = DEF_SKIN_COLORS[skin_dir.lower()]
1485                    except KeyError:
1486                        return skin_dir
1487                return "%s - %s" % (skin_dir, color)
1488            else:
1489                return ""
1490
1491        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
1492
1493        conf["configfn"] = config.get_filename()
1494        conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION
1495
1496        wlist = []
1497        interfaces = globber_full(sabnzbd.DIR_INTERFACES)
1498        for k in interfaces:
1499            if k.endswith(DEF_STDCONFIG):
1500                interfaces.remove(k)
1501                continue
1502
1503        for web in interfaces:
1504            rweb = os.path.basename(web)
1505            if os.access(os.path.join(web, DEF_MAIN_TMPL), os.R_OK):
1506                cols = ListColors(rweb)
1507                if cols:
1508                    for col in cols:
1509                        wlist.append(add_color(rweb, col))
1510                else:
1511                    wlist.append(rweb)
1512        conf["web_list"] = wlist
1513        conf["web_dir"] = add_color(cfg.web_dir(), cfg.web_color())
1514        conf["password"] = cfg.password.get_stars()
1515
1516        conf["language"] = cfg.language()
1517        lang_list = list_languages()
1518        if len(lang_list) < 2:
1519            lang_list = []
1520        conf["lang_list"] = lang_list
1521
1522        for kw in GENERAL_LIST:
1523            conf[kw] = config.get_config("misc", kw)()
1524
1525        conf["bandwidth_max"] = cfg.bandwidth_max()
1526        conf["bandwidth_perc"] = cfg.bandwidth_perc()
1527        conf["nzb_key"] = cfg.nzb_key()
1528        conf["my_lcldata"] = cfg.admin_dir.get_clipped_path()
1529        conf["caller_url"] = cherrypy.request.base + cfg.url_base()
1530
1531        template = Template(
1532            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_general.tmpl"),
1533            searchList=[conf],
1534            compilerSettings=CHEETAH_DIRECTIVES,
1535        )
1536        return template.respond()
1537
1538    @secured_expose(check_api_key=True, check_configlock=True)
1539    def saveGeneral(self, **kwargs):
1540        # Handle general options
1541        for kw in GENERAL_LIST:
1542            item = config.get_config("misc", kw)
1543            value = kwargs.get(kw)
1544            msg = item.set(value)
1545            if msg:
1546                return badParameterResponse(msg)
1547
1548        # Handle special options
1549        cfg.password.set(kwargs.get("password"))
1550
1551        web_dir = kwargs.get("web_dir")
1552        change_web_dir(web_dir)
1553
1554        bandwidth_max = kwargs.get("bandwidth_max")
1555        if bandwidth_max is not None:
1556            cfg.bandwidth_max.set(bandwidth_max)
1557        bandwidth_perc = kwargs.get("bandwidth_perc")
1558        if bandwidth_perc is not None:
1559            cfg.bandwidth_perc.set(bandwidth_perc)
1560        bandwidth_perc = cfg.bandwidth_perc()
1561        if bandwidth_perc and not bandwidth_max:
1562            logging.warning_helpful(T("You must set a maximum bandwidth before you can set a bandwidth limit"))
1563
1564        config.save_config()
1565
1566        # Update CherryPy authentication
1567        set_auth(cherrypy.config)
1568        if kwargs.get("ajax"):
1569            return sabnzbd.api.report("json", data={"success": True, "restart_req": sabnzbd.RESTART_REQ})
1570        else:
1571            raise Raiser(self.__root)
1572
1573
1574def change_web_dir(web_dir):
1575    try:
1576        web_dir, web_color = web_dir.split(" - ")
1577    except:
1578        try:
1579            web_color = DEF_SKIN_COLORS[web_dir.lower()]
1580        except:
1581            web_color = ""
1582
1583    web_dir_path = real_path(sabnzbd.DIR_INTERFACES, web_dir)
1584
1585    if not os.path.exists(web_dir_path):
1586        return badParameterResponse("Cannot find web template: %s" % web_dir_path)
1587    else:
1588        cfg.web_dir.set(web_dir)
1589        cfg.web_color.set(web_color)
1590
1591
1592##############################################################################
1593class ConfigServer:
1594    def __init__(self, root):
1595        self.__root = root
1596
1597    @secured_expose(check_configlock=True)
1598    def index(self, **kwargs):
1599        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
1600        new = []
1601        servers = config.get_servers()
1602        server_names = sorted(
1603            servers,
1604            key=lambda svr: "%d%02d%s"
1605            % (int(not servers[svr].enable()), servers[svr].priority(), servers[svr].displayname().lower()),
1606        )
1607        for svr in server_names:
1608            new.append(servers[svr].get_dict(safe=True))
1609            t, m, w, d, daily, articles_tried, articles_success = sabnzbd.BPSMeter.amounts(svr)
1610            if t:
1611                new[-1]["amounts"] = (
1612                    to_units(t),
1613                    to_units(m),
1614                    to_units(w),
1615                    to_units(d),
1616                    daily,
1617                    articles_tried,
1618                    articles_success,
1619                )
1620            new[-1]["quota_left"] = to_units(
1621                servers[svr].quota.get_int() - sabnzbd.BPSMeter.grand_total.get(svr, 0) + servers[svr].usage_at_start()
1622            )
1623
1624        conf["servers"] = new
1625        conf["cats"] = list_cats(default=True)
1626        conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION
1627
1628        template = Template(
1629            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_server.tmpl"),
1630            searchList=[conf],
1631            compilerSettings=CHEETAH_DIRECTIVES,
1632        )
1633        return template.respond()
1634
1635    @secured_expose(check_api_key=True, check_configlock=True)
1636    def addServer(self, **kwargs):
1637        return handle_server(kwargs, self.__root, True)
1638
1639    @secured_expose(check_api_key=True, check_configlock=True)
1640    def saveServer(self, **kwargs):
1641        return handle_server(kwargs, self.__root)
1642
1643    @secured_expose(check_api_key=True, check_configlock=True)
1644    def testServer(self, **kwargs):
1645        return handle_server_test(kwargs, self.__root)
1646
1647    @secured_expose(check_api_key=True, check_configlock=True)
1648    def delServer(self, **kwargs):
1649        kwargs["section"] = "servers"
1650        kwargs["keyword"] = kwargs.get("server")
1651        del_from_section(kwargs)
1652        raise Raiser(self.__root)
1653
1654    @secured_expose(check_api_key=True, check_configlock=True)
1655    def clrServer(self, **kwargs):
1656        server = kwargs.get("server")
1657        if server:
1658            sabnzbd.BPSMeter.clear_server(server)
1659        raise Raiser(self.__root)
1660
1661    @secured_expose(check_api_key=True, check_configlock=True)
1662    def toggleServer(self, **kwargs):
1663        server = kwargs.get("server")
1664        if server:
1665            svr = config.get_config("servers", server)
1666            if svr:
1667                svr.enable.set(not svr.enable())
1668                config.save_config()
1669                sabnzbd.Downloader.update_server(server, server)
1670        raise Raiser(self.__root)
1671
1672
1673def unique_svr_name(server):
1674    """Return a unique variant on given server name"""
1675    num = 0
1676    svr = 1
1677    new_name = server
1678    while svr:
1679        if num:
1680            new_name = "%s@%d" % (server, num)
1681        else:
1682            new_name = "%s" % server
1683        svr = config.get_config("servers", new_name)
1684        num += 1
1685    return new_name
1686
1687
1688def check_server(host, port, ajax):
1689    """Check if server address resolves properly"""
1690    if host.lower() == "localhost" and sabnzbd.AMBI_LOCALHOST:
1691        return badParameterResponse(T("Warning: LOCALHOST is ambiguous, use numerical IP-address."), ajax)
1692
1693    if get_server_addrinfo(host, int_conv(port)):
1694        return ""
1695    else:
1696        return badParameterResponse(T('Server address "%s:%s" is not valid.') % (host, port), ajax)
1697
1698
1699def handle_server(kwargs, root=None, new_svr=False):
1700    """Internal server handler"""
1701    ajax = kwargs.get("ajax")
1702    host = kwargs.get("host", "").strip()
1703    if not host:
1704        return badParameterResponse(T("Server address required"), ajax)
1705
1706    port = kwargs.get("port", "").strip()
1707    if not port:
1708        if not kwargs.get("ssl", "").strip():
1709            port = "119"
1710        else:
1711            port = "563"
1712        kwargs["port"] = port
1713
1714    if kwargs.get("connections", "").strip() == "":
1715        kwargs["connections"] = "1"
1716
1717    if kwargs.get("enable") == "1":
1718        msg = check_server(host, port, ajax)
1719        if msg:
1720            return msg
1721
1722    # Default server name is just the host name
1723    server = host
1724
1725    svr = None
1726    old_server = kwargs.get("server")
1727    if old_server:
1728        svr = config.get_config("servers", old_server)
1729    if svr:
1730        server = old_server
1731    else:
1732        svr = config.get_config("servers", server)
1733
1734    if new_svr:
1735        server = unique_svr_name(server)
1736
1737    for kw in ("ssl", "send_group", "enable", "optional"):
1738        if kw not in kwargs.keys():
1739            kwargs[kw] = None
1740    if svr and not new_svr:
1741        svr.set_dict(kwargs)
1742    else:
1743        old_server = None
1744        config.ConfigServer(server, kwargs)
1745
1746    config.save_config()
1747    sabnzbd.Downloader.update_server(old_server, server)
1748    if root:
1749        if ajax:
1750            return sabnzbd.api.report("json")
1751        else:
1752            raise Raiser(root)
1753
1754
1755def handle_server_test(kwargs, root):
1756    _result, msg = test_nntp_server_dict(kwargs)
1757    return msg
1758
1759
1760##############################################################################
1761class ConfigRss:
1762    def __init__(self, root):
1763        self.__root = root
1764        self.__refresh_readout = None  # Set to URL when new readout is needed
1765        self.__refresh_download = False  # True when feed needs to be read
1766        self.__refresh_force = False  # True if forced download of all matches is required
1767        self.__refresh_ignore = False  # True if first batch of new feed must be ignored
1768        self.__evaluate = False  # True if feed needs to be re-filtered
1769        self.__show_eval_button = False  # True if the "Apply filers" button should be shown
1770        self.__last_msg = ""  # Last error message from RSS reader
1771
1772    @secured_expose(check_configlock=True)
1773    def index(self, **kwargs):
1774        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
1775
1776        conf["scripts"] = list_scripts(default=True)
1777        pick_script = conf["scripts"] != []
1778
1779        conf["categories"] = list_cats(default=True)
1780        pick_cat = conf["categories"] != []
1781
1782        conf["rss_rate"] = cfg.rss_rate()
1783
1784        rss = {}
1785        feeds = config.get_rss()
1786        for feed in feeds:
1787            rss[feed] = feeds[feed].get_dict()
1788            filters = feeds[feed].filters()
1789            rss[feed]["filters"] = filters
1790            rss[feed]["filter_states"] = [bool(sabnzbd.rss.convert_filter(f[4])) for f in filters]
1791            rss[feed]["filtercount"] = len(filters)
1792
1793            rss[feed]["pick_cat"] = pick_cat
1794            rss[feed]["pick_script"] = pick_script
1795            rss[feed]["link"] = urllib.parse.quote_plus(feed)
1796            rss[feed]["baselink"] = [get_base_url(uri) for uri in rss[feed]["uri"]]
1797            rss[feed]["uris"] = feeds[feed].uri.get_string()
1798
1799        active_feed = kwargs.get("feed", "")
1800        conf["active_feed"] = active_feed
1801        conf["rss"] = rss
1802        conf["rss_next"] = time.strftime(time_format("%H:%M"), time.localtime(sabnzbd.RSSReader.next_run))
1803
1804        if active_feed:
1805            readout = bool(self.__refresh_readout)
1806            logging.debug("RSS READOUT = %s", readout)
1807            if not readout:
1808                self.__refresh_download = False
1809                self.__refresh_force = False
1810                self.__refresh_ignore = False
1811            if self.__evaluate:
1812                msg = sabnzbd.RSSReader.run_feed(
1813                    active_feed,
1814                    download=self.__refresh_download,
1815                    force=self.__refresh_force,
1816                    ignoreFirst=self.__refresh_ignore,
1817                    readout=readout,
1818                )
1819            else:
1820                msg = ""
1821            self.__evaluate = False
1822            if readout:
1823                sabnzbd.RSSReader.save()
1824                self.__last_msg = msg
1825            else:
1826                msg = self.__last_msg
1827            self.__refresh_readout = None
1828            conf["evalButton"] = self.__show_eval_button
1829            conf["error"] = msg
1830
1831            conf["downloaded"], conf["matched"], conf["unmatched"] = GetRssLog(active_feed)
1832        else:
1833            self.__last_msg = ""
1834
1835        # Find a unique new Feed name
1836        unum = 1
1837        txt = T("Feed")  # : Used as default Feed name in Config->RSS
1838        while txt + str(unum) in feeds:
1839            unum += 1
1840        conf["feed"] = txt + str(unum)
1841
1842        template = Template(
1843            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_rss.tmpl"),
1844            searchList=[conf],
1845            compilerSettings=CHEETAH_DIRECTIVES,
1846        )
1847        return template.respond()
1848
1849    @secured_expose(check_api_key=True, check_configlock=True)
1850    def save_rss_rate(self, **kwargs):
1851        """Save changed RSS automatic readout rate"""
1852        cfg.rss_rate.set(kwargs.get("rss_rate"))
1853        config.save_config()
1854        sabnzbd.Scheduler.restart()
1855        raise Raiser(self.__root)
1856
1857    @secured_expose(check_api_key=True, check_configlock=True)
1858    def upd_rss_feed(self, **kwargs):
1859        """Update Feed level attributes,
1860        legacy version: ignores 'enable' parameter
1861        """
1862        if kwargs.get("enable") is not None:
1863            del kwargs["enable"]
1864        try:
1865            cf = config.get_rss()[kwargs.get("feed")]
1866        except KeyError:
1867            cf = None
1868        uri = Strip(kwargs.get("uri"))
1869        if cf and uri:
1870            kwargs["uri"] = uri
1871            cf.set_dict(kwargs)
1872            config.save_config()
1873
1874        self.__evaluate = False
1875        self.__show_eval_button = True
1876        raise rssRaiser(self.__root, kwargs)
1877
1878    @secured_expose(check_api_key=True, check_configlock=True)
1879    def save_rss_feed(self, **kwargs):
1880        """Update Feed level attributes"""
1881        feed_name = kwargs.get("feed")
1882        try:
1883            cf = config.get_rss()[feed_name]
1884        except KeyError:
1885            cf = None
1886        if "enable" not in kwargs:
1887            kwargs["enable"] = 0
1888        uri = Strip(kwargs.get("uri"))
1889        if cf and uri:
1890            kwargs["uri"] = uri
1891            cf.set_dict(kwargs)
1892
1893            # Did we get a new name for this feed?
1894            new_name = kwargs.get("feed_new_name")
1895            if new_name and new_name != feed_name:
1896                cf.rename(new_name)
1897                # Update the feed name for the redirect
1898                kwargs["feed"] = new_name
1899
1900            config.save_config()
1901
1902        raise rssRaiser(self.__root, kwargs)
1903
1904    @secured_expose(check_api_key=True, check_configlock=True)
1905    def toggle_rss_feed(self, **kwargs):
1906        """Toggle automatic read-out flag of Feed"""
1907        try:
1908            item = config.get_rss()[kwargs.get("feed")]
1909        except KeyError:
1910            item = None
1911        if cfg:
1912            item.enable.set(not item.enable())
1913            config.save_config()
1914        if kwargs.get("table"):
1915            raise Raiser(self.__root)
1916        else:
1917            raise rssRaiser(self.__root, kwargs)
1918
1919    @secured_expose(check_api_key=True, check_configlock=True)
1920    def add_rss_feed(self, **kwargs):
1921        """Add one new RSS feed definition"""
1922        feed = Strip(kwargs.get("feed")).strip("[]")
1923        uri = Strip(kwargs.get("uri"))
1924        if feed and uri:
1925            try:
1926                cfg = config.get_rss()[feed]
1927            except KeyError:
1928                cfg = None
1929            if (not cfg) and uri:
1930                kwargs["feed"] = feed
1931                kwargs["uri"] = uri
1932                config.ConfigRSS(feed, kwargs)
1933                # Clear out any existing reference to this feed name
1934                # Otherwise first-run detection can fail
1935                sabnzbd.RSSReader.clear_feed(feed)
1936                config.save_config()
1937                self.__refresh_readout = feed
1938                self.__refresh_download = False
1939                self.__refresh_force = False
1940                self.__refresh_ignore = True
1941                self.__evaluate = True
1942                raise rssRaiser(self.__root, kwargs)
1943            else:
1944                raise Raiser(self.__root)
1945        else:
1946            raise Raiser(self.__root)
1947
1948    @secured_expose(check_api_key=True, check_configlock=True)
1949    def upd_rss_filter(self, **kwargs):
1950        """Wrapper, so we can call from api.py"""
1951        self.internal_upd_rss_filter(**kwargs)
1952
1953    def internal_upd_rss_filter(self, **kwargs):
1954        """Save updated filter definition"""
1955        try:
1956            feed_cfg = config.get_rss()[kwargs.get("feed")]
1957        except KeyError:
1958            raise rssRaiser(self.__root, kwargs)
1959
1960        pp = kwargs.get("pp")
1961        if IsNone(pp):
1962            pp = ""
1963        script = ConvertSpecials(kwargs.get("script"))
1964        cat = ConvertSpecials(kwargs.get("cat"))
1965        prio = ConvertSpecials(kwargs.get("priority"))
1966        filt = kwargs.get("filter_text")
1967        enabled = kwargs.get("enabled", "0")
1968
1969        if filt:
1970            feed_cfg.filters.update(
1971                int(kwargs.get("index", 0)), (cat, pp, script, kwargs.get("filter_type"), filt, prio, enabled)
1972            )
1973
1974            # Move filter if requested
1975            index = int_conv(kwargs.get("index", ""))
1976            new_index = kwargs.get("new_index", "")
1977            if new_index and int_conv(new_index) != index:
1978                feed_cfg.filters.move(int(index), int_conv(new_index))
1979
1980            config.save_config()
1981        self.__evaluate = False
1982        self.__show_eval_button = True
1983        raise rssRaiser(self.__root, kwargs)
1984
1985    @secured_expose(check_api_key=True, check_configlock=True)
1986    def del_rss_feed(self, *args, **kwargs):
1987        """Remove complete RSS feed"""
1988        kwargs["section"] = "rss"
1989        kwargs["keyword"] = kwargs.get("feed")
1990        del_from_section(kwargs)
1991        sabnzbd.RSSReader.clear_feed(kwargs.get("feed"))
1992        raise Raiser(self.__root)
1993
1994    @secured_expose(check_api_key=True, check_configlock=True)
1995    def del_rss_filter(self, **kwargs):
1996        """Wrapper, so we can call from api.py"""
1997        self.internal_del_rss_filter(**kwargs)
1998
1999    def internal_del_rss_filter(self, **kwargs):
2000        """Remove one RSS filter"""
2001        try:
2002            feed_cfg = config.get_rss()[kwargs.get("feed")]
2003        except KeyError:
2004            raise rssRaiser(self.__root, kwargs)
2005
2006        feed_cfg.filters.delete(int(kwargs.get("index", 0)))
2007        config.save_config()
2008        self.__evaluate = False
2009        self.__show_eval_button = True
2010        raise rssRaiser(self.__root, kwargs)
2011
2012    @secured_expose(check_api_key=True, check_configlock=True)
2013    def download_rss_feed(self, *args, **kwargs):
2014        """Force download of all matching jobs in a feed"""
2015        if "feed" in kwargs:
2016            feed = kwargs["feed"]
2017            self.__refresh_readout = feed
2018            self.__refresh_download = True
2019            self.__refresh_force = True
2020            self.__refresh_ignore = False
2021            self.__evaluate = True
2022        raise rssRaiser(self.__root, kwargs)
2023
2024    @secured_expose(check_api_key=True, check_configlock=True)
2025    def clean_rss_jobs(self, *args, **kwargs):
2026        """Remove processed RSS jobs from UI"""
2027        sabnzbd.RSSReader.clear_downloaded(kwargs["feed"])
2028        self.__evaluate = True
2029        raise rssRaiser(self.__root, kwargs)
2030
2031    @secured_expose(check_api_key=True, check_configlock=True)
2032    def test_rss_feed(self, *args, **kwargs):
2033        """Read the feed content again and show results"""
2034        if "feed" in kwargs:
2035            feed = kwargs["feed"]
2036            self.__refresh_readout = feed
2037            self.__refresh_download = False
2038            self.__refresh_force = False
2039            self.__refresh_ignore = True
2040            self.__evaluate = True
2041            self.__show_eval_button = False
2042        raise rssRaiser(self.__root, kwargs)
2043
2044    @secured_expose(check_api_key=True, check_configlock=True)
2045    def eval_rss_feed(self, *args, **kwargs):
2046        """Re-apply the filters to the feed"""
2047        if "feed" in kwargs:
2048            self.__refresh_download = False
2049            self.__refresh_force = False
2050            self.__refresh_ignore = False
2051            self.__show_eval_button = False
2052            self.__evaluate = True
2053
2054        raise rssRaiser(self.__root, kwargs)
2055
2056    @secured_expose(check_api_key=True, check_configlock=True)
2057    def download(self, **kwargs):
2058        """Download NZB from provider (Download button)"""
2059        feed = kwargs.get("feed")
2060        url = kwargs.get("url")
2061        nzbname = kwargs.get("nzbname")
2062        att = sabnzbd.RSSReader.lookup_url(feed, url)
2063        if att:
2064            pp = att.get("pp")
2065            cat = att.get("cat")
2066            script = att.get("script")
2067            prio = att.get("prio")
2068
2069            if url:
2070                sabnzbd.add_url(url, pp, script, cat, prio, nzbname)
2071            # Need to pass the title instead
2072            sabnzbd.RSSReader.flag_downloaded(feed, url)
2073        raise rssRaiser(self.__root, kwargs)
2074
2075    @secured_expose(check_api_key=True, check_configlock=True)
2076    def rss_now(self, *args, **kwargs):
2077        """Run an automatic RSS run now"""
2078        sabnzbd.Scheduler.force_rss()
2079        raise Raiser(self.__root)
2080
2081
2082def ConvertSpecials(p):
2083    """Convert None to 'None' and 'Default' to ''"""
2084    if p is None:
2085        p = "None"
2086    elif p.lower() == T("Default").lower():
2087        p = ""
2088    return p
2089
2090
2091def IsNone(value):
2092    """Return True if either None, 'None' or ''"""
2093    return value is None or value == "" or value.lower() == "none"
2094
2095
2096def Strip(txt):
2097    """Return stripped string, can handle None"""
2098    try:
2099        return txt.strip()
2100    except:
2101        return None
2102
2103
2104##############################################################################
2105_SCHED_ACTIONS = (
2106    "resume",
2107    "pause",
2108    "pause_all",
2109    "shutdown",
2110    "restart",
2111    "speedlimit",
2112    "pause_post",
2113    "resume_post",
2114    "scan_folder",
2115    "rss_scan",
2116    "remove_failed",
2117    "remove_completed",
2118    "pause_all_low",
2119    "pause_all_normal",
2120    "pause_all_high",
2121    "resume_all_low",
2122    "resume_all_normal",
2123    "resume_all_high",
2124    "enable_quota",
2125    "disable_quota",
2126)
2127
2128
2129class ConfigScheduling:
2130    def __init__(self, root):
2131        self.__root = root
2132
2133    @secured_expose(check_configlock=True)
2134    def index(self, **kwargs):
2135        def get_days():
2136            days = {
2137                "*": T("Daily"),
2138                "1": T("Monday"),
2139                "2": T("Tuesday"),
2140                "3": T("Wednesday"),
2141                "4": T("Thursday"),
2142                "5": T("Friday"),
2143                "6": T("Saturday"),
2144                "7": T("Sunday"),
2145            }
2146            return days
2147
2148        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
2149
2150        actions = []
2151        actions.extend(_SCHED_ACTIONS)
2152        day_names = get_days()
2153        categories = list_cats(False)
2154        snum = 1
2155        conf["schedlines"] = []
2156        conf["taskinfo"] = []
2157        for ev in sabnzbd.scheduler.sort_schedules(all_events=False):
2158            line = ev[3]
2159            conf["schedlines"].append(line)
2160            try:
2161                enabled, m, h, day_numbers, action = line.split(" ", 4)
2162            except:
2163                continue
2164            action = action.strip()
2165            try:
2166                action, value = action.split(" ", 1)
2167            except:
2168                value = ""
2169            value = value.strip()
2170            if value and not value.lower().strip("0123456789kmgtp%."):
2171                if "%" not in value and from_units(value) < 1.0:
2172                    value = T("off")  # : "Off" value for speedlimit in scheduler
2173                else:
2174                    if "%" not in value and 1 < int_conv(value) < 101:
2175                        value += "%"
2176                    value = value.upper()
2177            if action in actions:
2178                action = Ttemplate("sch-" + action)
2179            else:
2180                if action in ("enable_server", "disable_server"):
2181                    try:
2182                        value = '"%s"' % config.get_servers()[value].displayname()
2183                    except KeyError:
2184                        value = '"%s" <<< %s' % (value, T("Undefined server!"))
2185                    action = Ttemplate("sch-" + action)
2186                if action in ("pause_cat", "resume_cat"):
2187                    action = Ttemplate("sch-" + action)
2188                    if value not in categories:
2189                        # Category name change
2190                        value = '"%s" <<< %s' % (value, T("Incorrect parameter"))
2191                    else:
2192                        value = '"%s"' % value
2193
2194            if day_numbers == "1234567":
2195                days_of_week = "Daily"
2196            elif day_numbers == "12345":
2197                days_of_week = "Weekdays"
2198            elif day_numbers == "67":
2199                days_of_week = "Weekends"
2200            else:
2201                days_of_week = ", ".join([day_names.get(i, "**") for i in day_numbers])
2202
2203            item = (snum, "%02d" % int(h), "%02d" % int(m), days_of_week, "%s %s" % (action, value), enabled)
2204
2205            conf["taskinfo"].append(item)
2206            snum += 1
2207
2208        actions_lng = {}
2209        for action in actions:
2210            actions_lng[action] = Ttemplate("sch-" + action)
2211
2212        actions_servers = {}
2213        servers = config.get_servers()
2214        for srv in servers:
2215            actions_servers[srv] = servers[srv].displayname()
2216
2217        conf["actions_servers"] = actions_servers
2218        conf["actions"] = actions
2219        conf["actions_lng"] = actions_lng
2220        conf["categories"] = categories
2221
2222        template = Template(
2223            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_scheduling.tmpl"),
2224            searchList=[conf],
2225            compilerSettings=CHEETAH_DIRECTIVES,
2226        )
2227        return template.respond()
2228
2229    @secured_expose(check_api_key=True, check_configlock=True)
2230    def addSchedule(self, **kwargs):
2231        servers = config.get_servers()
2232        minute = kwargs.get("minute")
2233        hour = kwargs.get("hour")
2234        days_of_week = "".join([str(x) for x in kwargs.get("daysofweek", "")])
2235        if not days_of_week:
2236            days_of_week = "1234567"
2237        action = kwargs.get("action")
2238        arguments = kwargs.get("arguments")
2239
2240        arguments = arguments.strip().lower()
2241        if arguments in ("on", "enable"):
2242            arguments = "1"
2243        elif arguments in ("off", "disable"):
2244            arguments = "0"
2245
2246        if minute and hour and days_of_week and action:
2247            if action == "speedlimit":
2248                if not arguments or arguments.strip("0123456789kmgtp%."):
2249                    arguments = 0
2250            elif action in _SCHED_ACTIONS:
2251                arguments = ""
2252            elif action in servers:
2253                if arguments == "1":
2254                    arguments = action
2255                    action = "enable_server"
2256                else:
2257                    arguments = action
2258                    action = "disable_server"
2259
2260            elif action in ("pause_cat", "resume_cat"):
2261                # Need original category name, not lowercased
2262                arguments = arguments.strip()
2263            else:
2264                # Something else, leave empty
2265                action = None
2266
2267            if action:
2268                sched = cfg.schedules()
2269                sched.append("%s %s %s %s %s %s" % (1, minute, hour, days_of_week, action, arguments))
2270                cfg.schedules.set(sched)
2271
2272        config.save_config()
2273        sabnzbd.Scheduler.restart()
2274        raise Raiser(self.__root)
2275
2276    @secured_expose(check_api_key=True, check_configlock=True)
2277    def delSchedule(self, **kwargs):
2278        schedules = cfg.schedules()
2279        line = kwargs.get("line")
2280        if line and line in schedules:
2281            schedules.remove(line)
2282            cfg.schedules.set(schedules)
2283            config.save_config()
2284            sabnzbd.Scheduler.restart()
2285        raise Raiser(self.__root)
2286
2287    @secured_expose(check_api_key=True, check_configlock=True)
2288    def toggleSchedule(self, **kwargs):
2289        schedules = cfg.schedules()
2290        line = kwargs.get("line")
2291        if line:
2292            for i, schedule in enumerate(schedules):
2293                if schedule == line:
2294                    # Toggle the schedule
2295                    schedule_split = schedule.split()
2296                    schedule_split[0] = "%d" % (schedule_split[0] == "0")
2297                    schedules[i] = " ".join(schedule_split)
2298                    break
2299            cfg.schedules.set(schedules)
2300            config.save_config()
2301            sabnzbd.Scheduler.restart()
2302        raise Raiser(self.__root)
2303
2304
2305##############################################################################
2306class ConfigCats:
2307    def __init__(self, root):
2308        self.__root = root
2309
2310    @secured_expose(check_configlock=True)
2311    def index(self, **kwargs):
2312        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
2313
2314        conf["scripts"] = list_scripts(default=True)
2315        conf["defdir"] = cfg.complete_dir.get_clipped_path()
2316
2317        categories = config.get_ordered_categories()
2318        conf["have_cats"] = len(categories) > 1
2319
2320        slotinfo = []
2321        for cat in categories:
2322            cat["newzbin"] = cat["newzbin"].replace('"', "&quot;")
2323            slotinfo.append(cat)
2324
2325        # Add empty line
2326        empty = {
2327            "name": "",
2328            "order": "0",
2329            "pp": "-1",
2330            "script": "",
2331            "dir": "",
2332            "newzbin": "",
2333            "priority": DEFAULT_PRIORITY,
2334        }
2335        slotinfo.insert(1, empty)
2336        conf["slotinfo"] = slotinfo
2337
2338        template = Template(
2339            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_cat.tmpl"),
2340            searchList=[conf],
2341            compilerSettings=CHEETAH_DIRECTIVES,
2342        )
2343        return template.respond()
2344
2345    @secured_expose(check_api_key=True, check_configlock=True)
2346    def delete(self, **kwargs):
2347        kwargs["section"] = "categories"
2348        kwargs["keyword"] = kwargs.get("name")
2349        del_from_section(kwargs)
2350        raise Raiser(self.__root)
2351
2352    @secured_expose(check_api_key=True, check_configlock=True)
2353    def save(self, **kwargs):
2354        name = kwargs.get("name", "*")
2355        if name == "*":
2356            newname = name
2357        else:
2358            newname = re.sub('"', "", kwargs.get("newname", ""))
2359        if newname:
2360            # Check if this cat-dir is not sub-folder of incomplete
2361            if same_file(cfg.download_dir.get_path(), real_path(cfg.complete_dir.get_path(), kwargs["dir"])):
2362                return T("Category folder cannot be a subfolder of the Temporary Download Folder.")
2363
2364            # Delete current one and replace with new one
2365            if name:
2366                config.delete("categories", name)
2367            config.ConfigCat(newname.lower(), kwargs)
2368
2369        config.save_config()
2370        raise Raiser(self.__root)
2371
2372
2373##############################################################################
2374SORT_LIST = (
2375    "enable_tv_sorting",
2376    "tv_sort_string",
2377    "tv_categories",
2378    "enable_movie_sorting",
2379    "movie_sort_string",
2380    "movie_sort_extra",
2381    "movie_extra_folder",
2382    "enable_date_sorting",
2383    "date_sort_string",
2384    "movie_categories",
2385    "date_categories",
2386)
2387
2388
2389class ConfigSorting:
2390    def __init__(self, root):
2391        self.__root = root
2392
2393    @secured_expose(check_configlock=True)
2394    def index(self, **kwargs):
2395        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
2396        conf["complete_dir"] = cfg.complete_dir.get_clipped_path()
2397
2398        for kw in SORT_LIST:
2399            conf[kw] = config.get_config("misc", kw)()
2400        conf["categories"] = list_cats(False)
2401
2402        template = Template(
2403            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_sorting.tmpl"),
2404            searchList=[conf],
2405            compilerSettings=CHEETAH_DIRECTIVES,
2406        )
2407        return template.respond()
2408
2409    @secured_expose(check_api_key=True, check_configlock=True)
2410    def saveSorting(self, **kwargs):
2411        try:
2412            kwargs["movie_categories"] = kwargs["movie_cat"]
2413        except:
2414            pass
2415        try:
2416            kwargs["date_categories"] = kwargs["date_cat"]
2417        except:
2418            pass
2419        try:
2420            kwargs["tv_categories"] = kwargs["tv_cat"]
2421        except:
2422            pass
2423
2424        for kw in SORT_LIST:
2425            item = config.get_config("misc", kw)
2426            value = kwargs.get(kw)
2427            msg = item.set(value)
2428            if msg:
2429                return badParameterResponse(msg)
2430
2431        config.save_config()
2432        raise Raiser(self.__root)
2433
2434
2435##############################################################################
2436LOG_API_RE = re.compile(rb"(apikey|api)(=|:)[\w]+", re.I)
2437LOG_API_JSON_RE = re.compile(rb"'(apikey|api)': '[\w]+'", re.I)
2438LOG_USER_RE = re.compile(rb"(user|username)\s?=\s?[\S]+", re.I)
2439LOG_PASS_RE = re.compile(rb"(password)\s?=\s?[\S]+", re.I)
2440LOG_INI_HIDE_RE = re.compile(
2441    rb"(email_pwd|email_account|email_to|rating_api_key|pushover_token|pushover_userkey|pushbullet_apikey|prowl_apikey|growl_password|growl_server|IPv[4|6] address)\s?=\s?[\S]+",
2442    re.I,
2443)
2444LOG_HASH_RE = re.compile(rb"([a-fA-F\d]{25})", re.I)
2445
2446
2447class Status:
2448    def __init__(self, root):
2449        self.__root = root
2450
2451    @secured_expose(check_configlock=True)
2452    def index(self, **kwargs):
2453        header = build_status(skip_dashboard=kwargs.get("skip_dashboard"))
2454        template = Template(
2455            file=os.path.join(sabnzbd.WEB_DIR, "status.tmpl"), searchList=[header], compilerSettings=CHEETAH_DIRECTIVES
2456        )
2457        return template.respond()
2458
2459    @secured_expose(check_api_key=True)
2460    def reset_quota(self, **kwargs):
2461        sabnzbd.BPSMeter.reset_quota(force=True)
2462        raise Raiser(self.__root)
2463
2464    @secured_expose(check_api_key=True)
2465    def disconnect(self, **kwargs):
2466        sabnzbd.Downloader.disconnect()
2467        raise Raiser(self.__root)
2468
2469    @secured_expose(check_api_key=True)
2470    def refresh_conn(self, **kwargs):
2471        # No real action, just reload the page
2472        raise Raiser(self.__root)
2473
2474    @secured_expose(check_api_key=True)
2475    def showlog(self, **kwargs):
2476        try:
2477            sabnzbd.LOGHANDLER.flush()
2478        except:
2479            pass
2480
2481        # Fetch the INI and the log-data and add a message at the top
2482        log_data = b"--------------------------------\n\n"
2483        log_data += b"The log includes a copy of your sabnzbd.ini with\nall usernames, passwords and API-keys removed."
2484        log_data += b"\n\n--------------------------------\n"
2485
2486        with open(sabnzbd.LOGFILE, "rb") as f:
2487            log_data += f.read()
2488
2489        with open(config.get_filename(), "rb") as f:
2490            log_data += f.read()
2491
2492        # We need to remove all passwords/usernames/api-keys
2493        log_data = LOG_API_RE.sub(b"apikey=<APIKEY>", log_data)
2494        log_data = LOG_API_JSON_RE.sub(b"'apikey':<APIKEY>'", log_data)
2495        log_data = LOG_USER_RE.sub(b"\\g<1>=<USER>", log_data)
2496        log_data = LOG_PASS_RE.sub(b"password=<PASSWORD>", log_data)
2497        log_data = LOG_INI_HIDE_RE.sub(b"\\1 = <REMOVED>", log_data)
2498        log_data = LOG_HASH_RE.sub(b"<HASH>", log_data)
2499
2500        # Try to replace the username
2501        try:
2502            import getpass
2503
2504            cur_user = getpass.getuser()
2505            if cur_user:
2506                log_data = log_data.replace(utob(cur_user), b"<USERNAME>")
2507        except:
2508            pass
2509        # Set headers
2510        cherrypy.response.headers["Content-Type"] = "application/x-download;charset=utf-8"
2511        cherrypy.response.headers["Content-Disposition"] = 'attachment;filename="sabnzbd.log"'
2512        return log_data
2513
2514    @secured_expose(check_api_key=True)
2515    def clearwarnings(self, **kwargs):
2516        sabnzbd.GUIHANDLER.clear()
2517        raise Raiser(self.__root)
2518
2519    @secured_expose(check_api_key=True)
2520    def change_loglevel(self, **kwargs):
2521        cfg.log_level.set(kwargs.get("loglevel"))
2522        config.save_config()
2523
2524        raise Raiser(self.__root)
2525
2526    @secured_expose(check_api_key=True)
2527    def unblock_server(self, **kwargs):
2528        sabnzbd.Downloader.unblock(kwargs.get("server"))
2529        # Short sleep so that UI shows new server status
2530        time.sleep(1.0)
2531        raise Raiser(self.__root)
2532
2533    @secured_expose(check_api_key=True)
2534    def delete(self, **kwargs):
2535        orphan_delete(kwargs)
2536        raise Raiser(self.__root)
2537
2538    @secured_expose(check_api_key=True)
2539    def delete_all(self, **kwargs):
2540        orphan_delete_all()
2541        raise Raiser(self.__root)
2542
2543    @secured_expose(check_api_key=True)
2544    def add(self, **kwargs):
2545        orphan_add(kwargs)
2546        raise Raiser(self.__root)
2547
2548    @secured_expose(check_api_key=True)
2549    def add_all(self, **kwargs):
2550        orphan_add_all()
2551        raise Raiser(self.__root)
2552
2553    @secured_expose(check_api_key=True)
2554    def dashrefresh(self, **kwargs):
2555        # This function is run when Refresh button on Dashboard is clicked
2556        # Put the time consuming dashboard functions here; they only get executed when the user clicks the Refresh button
2557
2558        # PyStone
2559        sabnzbd.PYSTONE_SCORE = getpystone()
2560
2561        # Diskspeed of download (aka incomplete) directory:
2562        dir_speed = diskspeedmeasure(sabnzbd.cfg.download_dir.get_path())
2563        if dir_speed:
2564            sabnzbd.DOWNLOAD_DIR_SPEED = round(dir_speed, 1)
2565        else:
2566            sabnzbd.DOWNLOAD_DIR_SPEED = 0
2567
2568        time.sleep(1.0)
2569        # Diskspeed of complete directory:
2570        dir_speed = diskspeedmeasure(sabnzbd.cfg.complete_dir.get_path())
2571        if dir_speed:
2572            sabnzbd.COMPLETE_DIR_SPEED = round(dir_speed, 1)
2573        else:
2574            sabnzbd.COMPLETE_DIR_SPEED = 0
2575
2576        # Internet bandwidth
2577        sabnzbd.INTERNET_BANDWIDTH = round(internetspeed(), 1)
2578
2579        raise Raiser(self.__root)  # Refresh screen
2580
2581
2582def orphan_delete(kwargs):
2583    path = kwargs.get("name")
2584    if path:
2585        path = os.path.join(long_path(cfg.download_dir.get_path()), path)
2586        logging.info("Removing orphaned job %s", path)
2587        remove_all(path, recursive=True)
2588
2589
2590def orphan_delete_all():
2591    paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
2592    for path in paths:
2593        kwargs = {"name": path}
2594        orphan_delete(kwargs)
2595
2596
2597def orphan_add(kwargs):
2598    path = kwargs.get("name")
2599    if path:
2600        path = os.path.join(long_path(cfg.download_dir.get_path()), path)
2601        logging.info("Re-adding orphaned job %s", path)
2602        sabnzbd.NzbQueue.repair_job(path, None, None)
2603
2604
2605def orphan_add_all():
2606    paths = sabnzbd.NzbQueue.scan_jobs(all_jobs=False, action=False)
2607    for path in paths:
2608        kwargs = {"name": path}
2609        orphan_add(kwargs)
2610
2611
2612def badParameterResponse(msg, ajax=None):
2613    """Return a html page with error message and a 'back' button"""
2614    if ajax:
2615        return sabnzbd.api.report("json", error=msg)
2616    else:
2617        return """
2618<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
2619<html>
2620<head>
2621           <title>SABnzbd %s - %s</title>
2622</head>
2623<body>
2624<h3>%s</h3>
2625%s
2626<br><br>
2627<FORM><INPUT TYPE="BUTTON" VALUE="%s" ONCLICK="history.go(-1)"></FORM>
2628</body>
2629</html>
2630""" % (
2631            sabnzbd.__version__,
2632            T("ERROR:"),
2633            T("Incorrect parameter"),
2634            msg,
2635            T("Back"),
2636        )
2637
2638
2639def ShowString(name, msg):
2640    """Return a html page listing a file and a 'back' button"""
2641    return """
2642<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
2643<html>
2644<head>
2645           <title>%s</title>
2646</head>
2647<body>
2648           <FORM><INPUT TYPE="BUTTON" VALUE="%s" ONCLICK="history.go(-1)"></FORM>
2649           <h3>%s</h3>
2650           <code><pre>%s</pre></code>
2651</body>
2652</html>
2653""" % (
2654        xml_name(name),
2655        T("Back"),
2656        xml_name(name),
2657        escape(msg),
2658    )
2659
2660
2661def GetRssLog(feed):
2662    def make_item(job):
2663        # Make a copy
2664        job = job.copy()
2665
2666        # Now we apply some formatting
2667        job["title"] = job["title"]
2668        job["skip"] = "*" * int(job.get("status", "").endswith("*"))
2669        # These fields could be empty
2670        job["cat"] = job.get("cat", "")
2671        job["size"] = job.get("size", "")
2672        job["infourl"] = job.get("infourl", "")
2673
2674        # Auto-fetched jobs didn't have these fields set
2675        if job.get("url"):
2676            job["baselink"] = get_base_url(job.get("url"))
2677            if sabnzbd.rss.special_rss_site(job.get("url")):
2678                job["nzbname"] = ""
2679            else:
2680                job["nzbname"] = job["title"]
2681        else:
2682            job["baselink"] = ""
2683            job["nzbname"] = job["title"]
2684
2685        if job.get("size", 0):
2686            job["size_units"] = to_units(job["size"])
2687        else:
2688            job["size_units"] = "-"
2689
2690        # And we add extra fields for sorting
2691        if job.get("age", 0):
2692            job["age_ms"] = (job["age"] - datetime.utcfromtimestamp(0)).total_seconds()
2693            job["age"] = calc_age(job["age"], True)
2694        else:
2695            job["age_ms"] = ""
2696            job["age"] = ""
2697
2698        if job.get("time_downloaded"):
2699            job["time_downloaded_ms"] = time.mktime(job["time_downloaded"])
2700            job["time_downloaded"] = time.strftime(time_format("%H:%M %a %d %b"), job["time_downloaded"])
2701        else:
2702            job["time_downloaded_ms"] = ""
2703            job["time_downloaded"] = ""
2704
2705        return job
2706
2707    jobs = sabnzbd.RSSReader.show_result(feed).values()
2708    good, bad, done = ([], [], [])
2709    for job in jobs:
2710        if job["status"][0] == "G":
2711            good.append(make_item(job))
2712        elif job["status"][0] == "B":
2713            bad.append(make_item(job))
2714        elif job["status"] == "D":
2715            done.append(make_item(job))
2716
2717    try:
2718        # Sort based on actual age, in try-catch just to be sure
2719        good.sort(key=lambda job: job["age_ms"], reverse=True)
2720        bad.sort(key=lambda job: job["age_ms"], reverse=True)
2721        done.sort(key=lambda job: job["time_downloaded_ms"], reverse=True)
2722    except:
2723        # Let the javascript do it then..
2724        pass
2725
2726    return done, good, bad
2727
2728
2729##############################################################################
2730LIST_EMAIL = (
2731    "email_endjob",
2732    "email_cats",
2733    "email_full",
2734    "email_server",
2735    "email_to",
2736    "email_from",
2737    "email_account",
2738    "email_pwd",
2739    "email_rss",
2740)
2741LIST_NCENTER = (
2742    "ncenter_enable",
2743    "ncenter_cats",
2744    "ncenter_prio_startup",
2745    "ncenter_prio_download",
2746    "ncenter_prio_pause_resume",
2747    "ncenter_prio_pp",
2748    "ncenter_prio_pp",
2749    "ncenter_prio_complete",
2750    "ncenter_prio_failed",
2751    "ncenter_prio_disk_full",
2752    "ncenter_prio_warning",
2753    "ncenter_prio_error",
2754    "ncenter_prio_queue_done",
2755    "ncenter_prio_other",
2756    "ncenter_prio_new_login",
2757)
2758LIST_ACENTER = (
2759    "acenter_enable",
2760    "acenter_cats",
2761    "acenter_prio_startup",
2762    "acenter_prio_download",
2763    "acenter_prio_pause_resume",
2764    "acenter_prio_pp",
2765    "acenter_prio_complete",
2766    "acenter_prio_failed",
2767    "acenter_prio_disk_full",
2768    "acenter_prio_warning",
2769    "acenter_prio_error",
2770    "acenter_prio_queue_done",
2771    "acenter_prio_other",
2772    "acenter_prio_new_login",
2773)
2774LIST_NTFOSD = (
2775    "ntfosd_enable",
2776    "ntfosd_cats",
2777    "ntfosd_prio_startup",
2778    "ntfosd_prio_download",
2779    "ntfosd_prio_pause_resume",
2780    "ntfosd_prio_pp",
2781    "ntfosd_prio_complete",
2782    "ntfosd_prio_failed",
2783    "ntfosd_prio_disk_full",
2784    "ntfosd_prio_warning",
2785    "ntfosd_prio_error",
2786    "ntfosd_prio_queue_done",
2787    "ntfosd_prio_other",
2788    "ntfosd_prio_new_login",
2789)
2790LIST_PROWL = (
2791    "prowl_enable",
2792    "prowl_cats",
2793    "prowl_apikey",
2794    "prowl_prio_startup",
2795    "prowl_prio_download",
2796    "prowl_prio_pause_resume",
2797    "prowl_prio_pp",
2798    "prowl_prio_complete",
2799    "prowl_prio_failed",
2800    "prowl_prio_disk_full",
2801    "prowl_prio_warning",
2802    "prowl_prio_error",
2803    "prowl_prio_queue_done",
2804    "prowl_prio_other",
2805    "prowl_prio_new_login",
2806)
2807LIST_PUSHOVER = (
2808    "pushover_enable",
2809    "pushover_cats",
2810    "pushover_token",
2811    "pushover_userkey",
2812    "pushover_device",
2813    "pushover_prio_startup",
2814    "pushover_prio_download",
2815    "pushover_prio_pause_resume",
2816    "pushover_prio_pp",
2817    "pushover_prio_complete",
2818    "pushover_prio_failed",
2819    "pushover_prio_disk_full",
2820    "pushover_prio_warning",
2821    "pushover_prio_error",
2822    "pushover_prio_queue_done",
2823    "pushover_prio_other",
2824    "pushover_prio_new_login",
2825    "pushover_emergency_retry",
2826    "pushover_emergency_expire",
2827)
2828LIST_PUSHBULLET = (
2829    "pushbullet_enable",
2830    "pushbullet_cats",
2831    "pushbullet_apikey",
2832    "pushbullet_device",
2833    "pushbullet_prio_startup",
2834    "pushbullet_prio_download",
2835    "pushbullet_prio_pause_resume",
2836    "pushbullet_prio_pp",
2837    "pushbullet_prio_complete",
2838    "pushbullet_prio_failed",
2839    "pushbullet_prio_disk_full",
2840    "pushbullet_prio_warning",
2841    "pushbullet_prio_error",
2842    "pushbullet_prio_queue_done",
2843    "pushbullet_prio_other",
2844    "pushbullet_prio_new_login",
2845)
2846LIST_NSCRIPT = (
2847    "nscript_enable",
2848    "nscript_cats",
2849    "nscript_script",
2850    "nscript_parameters",
2851    "nscript_prio_startup",
2852    "nscript_prio_download",
2853    "nscript_prio_pause_resume",
2854    "nscript_prio_pp",
2855    "nscript_prio_complete",
2856    "nscript_prio_failed",
2857    "nscript_prio_disk_full",
2858    "nscript_prio_warning",
2859    "nscript_prio_error",
2860    "nscript_prio_queue_done",
2861    "nscript_prio_other",
2862    "nscript_prio_new_login",
2863)
2864
2865
2866class ConfigNotify:
2867    def __init__(self, root):
2868        self.__root = root
2869        self.__lastmail = None
2870
2871    @secured_expose(check_configlock=True)
2872    def index(self, **kwargs):
2873        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
2874
2875        conf["categories"] = list_cats(False)
2876        conf["lastmail"] = self.__lastmail
2877        conf["have_ntfosd"] = sabnzbd.notifier.have_ntfosd()
2878        conf["have_ncenter"] = sabnzbd.DARWIN and sabnzbd.FOUNDATION
2879        conf["scripts"] = list_scripts(default=False, none=True)
2880
2881        for kw in LIST_EMAIL:
2882            conf[kw] = config.get_config("misc", kw).get_string()
2883        for kw in LIST_PROWL:
2884            conf[kw] = config.get_config("prowl", kw)()
2885        for kw in LIST_PUSHOVER:
2886            conf[kw] = config.get_config("pushover", kw)()
2887        for kw in LIST_PUSHBULLET:
2888            conf[kw] = config.get_config("pushbullet", kw)()
2889        for kw in LIST_NCENTER:
2890            conf[kw] = config.get_config("ncenter", kw)()
2891        for kw in LIST_ACENTER:
2892            conf[kw] = config.get_config("acenter", kw)()
2893        for kw in LIST_NTFOSD:
2894            conf[kw] = config.get_config("ntfosd", kw)()
2895        for kw in LIST_NSCRIPT:
2896            conf[kw] = config.get_config("nscript", kw)()
2897        conf["notify_types"] = sabnzbd.notifier.NOTIFICATION
2898
2899        template = Template(
2900            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_notify.tmpl"),
2901            searchList=[conf],
2902            compilerSettings=CHEETAH_DIRECTIVES,
2903        )
2904        return template.respond()
2905
2906    @secured_expose(check_api_key=True, check_configlock=True)
2907    def saveEmail(self, **kwargs):
2908        ajax = kwargs.get("ajax")
2909
2910        for kw in LIST_EMAIL:
2911            msg = config.get_config("misc", kw).set(kwargs.get(kw))
2912            if msg:
2913                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
2914        for kw in LIST_NCENTER:
2915            msg = config.get_config("ncenter", kw).set(kwargs.get(kw))
2916            if msg:
2917                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
2918        for kw in LIST_ACENTER:
2919            msg = config.get_config("acenter", kw).set(kwargs.get(kw))
2920            if msg:
2921                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
2922        for kw in LIST_NTFOSD:
2923            msg = config.get_config("ntfosd", kw).set(kwargs.get(kw))
2924            if msg:
2925                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
2926        for kw in LIST_PROWL:
2927            msg = config.get_config("prowl", kw).set(kwargs.get(kw))
2928            if msg:
2929                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
2930        for kw in LIST_PUSHOVER:
2931            msg = config.get_config("pushover", kw).set(kwargs.get(kw))
2932            if msg:
2933                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
2934        for kw in LIST_PUSHBULLET:
2935            msg = config.get_config("pushbullet", kw).set(kwargs.get(kw, 0))
2936            if msg:
2937                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
2938        for kw in LIST_NSCRIPT:
2939            msg = config.get_config("nscript", kw).set(kwargs.get(kw, 0))
2940            if msg:
2941                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
2942
2943        config.save_config()
2944        self.__lastmail = None
2945        if ajax:
2946            return sabnzbd.api.report("json")
2947        else:
2948            raise Raiser(self.__root)
2949