#!/usr/bin/python3.7 -OO
# Copyright 2007-2020 The SABnzbd-Team <team@sabnzbd.org>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

"""
sabnzbd.interface - webinterface
"""

import os
import time
from datetime import datetime
import cherrypy
import logging
import urllib.request, urllib.parse, urllib.error
import re
import hashlib
import socket
import ssl
import functools
from threading import Thread
from random import randint
from xml.sax.saxutils import escape

import sabnzbd
import sabnzbd.rss
import sabnzbd.scheduler as scheduler

from Cheetah.Template import Template
from sabnzbd.misc import (
    to_units,
    from_units,
    time_format,
    calc_age,
    int_conv,
    get_base_url,
    probablyipv4,
    probablyipv6,
    opts_to_pp,
)
from sabnzbd.filesystem import real_path, long_path, globber, globber_full, remove_all, clip_path, same_file
from sabnzbd.newswrapper import GetServerParms
from sabnzbd.bpsmeter import BPSMeter
from sabnzbd.encoding import xml_name, utob
import sabnzbd.config as config
import sabnzbd.cfg as cfg
import sabnzbd.notifier as notifier
import sabnzbd.newsunpack
from sabnzbd.downloader import Downloader
from sabnzbd.nzbqueue import NzbQueue
from sabnzbd.utils.servertests import test_nntp_server_dict
from sabnzbd.decoder import SABYENC_ENABLED
from sabnzbd.utils.diskspeed import diskspeedmeasure
from sabnzbd.utils.getperformance import getpystone
from sabnzbd.utils.internetspeed import internetspeed

from sabnzbd.constants import MEBI, DEF_SKIN_COLORS, DEF_STDCONFIG, DEF_MAIN_TMPL, DEFAULT_PRIORITY, CHEETAH_DIRECTIVES

from sabnzbd.lang import list_languages

from sabnzbd.api import (
    list_scripts,
    list_cats,
    del_from_section,
    api_handler,
    build_queue,
    build_status,
    retry_job,
    build_header,
    build_history,
    format_bytes,
    report,
    del_hist_job,
    Ttemplate,
    build_queue_header,
)

##############################################################################
# Global constants
##############################################################################


##############################################################################
# Security functions
##############################################################################
def secured_expose(wrap_func=None, check_configlock=False, check_api_key=False):
    """ Wrapper for both cherrypy.expose and login/access check """
    if not wrap_func:
        return functools.partial(secured_expose, check_configlock=check_configlock, check_api_key=check_api_key)

    # Expose to cherrypy
    wrap_func.exposed = True

    @functools.wraps(wrap_func)
    def internal_wrap(*args, **kwargs):
        # Add X-Frame-Headers headers to page-requests
        if cfg.x_frame_options():
            cherrypy.response.headers["X-Frame-Options"] = "SameOrigin"

        # Check if config is locked
        if check_configlock and cfg.configlock():
            cherrypy.response.status = 403
            return "Access denied - Configuration locked"

        # Check if external access
        if not check_access():
            cherrypy.response.status = 403
            return "Access denied"

        # Verify login status, only for non-key pages
        if not check_login() and not check_api_key:
            raise Raiser("/login/")

        # Verify host used for the visit
        if not check_hostname():
            cherrypy.response.status = 403
            return "Access denied - Hostname verification failed: https://sabnzbd.org/hostname-check"

        # Some pages need correct API key
        if check_api_key:
            msg = check_apikey(kwargs)
            if msg:
                return msg

        # All good, cool!
        return wrap_func(*args, **kwargs)

    return internal_wrap


def check_access(access_type=4):
    """ Check if external address is allowed given access_type:
        1=nzb
        2=api
        3=full_api
        4=webui
        5=webui with login for external
    """
    referrer = cherrypy.request.remote.ip

    # CherryPy will report ::ffff:192.168.0.10 on dual-stack situation
    # It will always contain that ::ffff: prefix
    range_ok = not cfg.local_ranges() or bool(
        [1 for r in cfg.local_ranges() if (referrer.startswith(r) or referrer.replace("::ffff:", "").startswith(r))]
    )
    allowed = referrer in ("127.0.0.1", "::ffff:127.0.0.1", "::1") or range_ok or access_type <= cfg.inet_exposure()
    if not allowed:
        logging.debug("Refused connection from %s", referrer)
    return allowed


def check_hostname():
    """ Check if hostname is allowed, to mitigate DNS-rebinding attack.
        Similar to CVE-2019-5702, we need to add protection even
        if only allowed to be accessed via localhost.
    """
    # If login is enabled, no API-key can be deducted
    if cfg.username() and cfg.password():
        return True

    # Don't allow requests without Host
    host = cherrypy.request.headers.get("Host")
    if not host:
        return False

    # Remove the port-part (like ':8080'), if it is there, always on the right hand side.
    # Not to be confused with IPv6 colons (within square brackets)
    host = re.sub(":[0123456789]+$", "", host).lower()

    # Fine if localhost or IP
    if host == "localhost" or probablyipv4(host) or probablyipv6(host):
        return True

    # Check on the whitelist
    if host in cfg.host_whitelist():
        return True

    # Fine if ends with ".local" or ".local.", aka mDNS name
    # See rfc6762 Multicast DNS
    if host.endswith((".local", ".local.")):
        return True

    # Ohoh, bad
    log_warning_and_ip(T('Refused connection with hostname "%s" from:') % host)
    return False


# Create a more unique ID for each instance
COOKIE_SECRET = str(randint(1000, 100000) * os.getpid())


def set_login_cookie(remove=False, remember_me=False):
    """ We try to set a cookie as unique as possible
        to the current user. Based on it's IP and the
        current process ID of the SAB instance and a random
        number, so cookies cannot be re-used
    """
    salt = randint(1, 1000)
    cookie_str = utob(str(salt) + cherrypy.request.remote.ip + COOKIE_SECRET)
    cherrypy.response.cookie["login_cookie"] = hashlib.sha1(cookie_str).hexdigest()
    cherrypy.response.cookie["login_cookie"]["path"] = "/"
    cherrypy.response.cookie["login_cookie"]["httponly"] = 1
    cherrypy.response.cookie["login_salt"] = salt
    cherrypy.response.cookie["login_salt"]["path"] = "/"
    cherrypy.response.cookie["login_salt"]["httponly"] = 1

    # If we want to be remembered
    if remember_me:
        cherrypy.response.cookie["login_cookie"]["max-age"] = 3600 * 24 * 14
        cherrypy.response.cookie["login_salt"]["max-age"] = 3600 * 24 * 14

    # To remove
    if remove:
        cherrypy.response.cookie["login_cookie"]["expires"] = 0
        cherrypy.response.cookie["login_salt"]["expires"] = 0
    else:
        # Notify about new login
        notifier.send_notification(T("User logged in"), T("User logged in to the web interface"), "new_login")


def check_login_cookie():
    # Do we have everything?
    if "login_cookie" not in cherrypy.request.cookie or "login_salt" not in cherrypy.request.cookie:
        return False

    cookie_str = utob(str(cherrypy.request.cookie["login_salt"].value) + cherrypy.request.remote.ip + COOKIE_SECRET)
    return cherrypy.request.cookie["login_cookie"].value == hashlib.sha1(cookie_str).hexdigest()


def check_login():
    # Not when no authentication required or basic-auth is on
    if not cfg.html_login() or not cfg.username() or not cfg.password():
        return True

    # If we show login for external IP, by using access_type=6 we can check if IP match
    if cfg.inet_exposure() == 5 and check_access(access_type=6):
        return True

    # Check the cookie
    return check_login_cookie()


def check_basic_auth(_, username, password):
    """ CherryPy basic authentication validation """
    return username == cfg.username() and password == cfg.password()


def set_auth(conf):
    """ Set the authentication for CherryPy """
    if cfg.username() and cfg.password() and not cfg.html_login():
        conf.update(
            {
                "tools.auth_basic.on": True,
                "tools.auth_basic.realm": "SABnzbd",
                "tools.auth_basic.checkpassword": check_basic_auth,
            }
        )
        conf.update(
            {"/api": {"tools.auth_basic.on": False}, "%s/api" % cfg.url_base(): {"tools.auth_basic.on": False},}
        )
    else:
        conf.update({"tools.auth_basic.on": False})


def check_apikey(kwargs):
    """ Check API-key or NZB-key
        Return None when OK, otherwise an error message
    """
    mode = kwargs.get("mode", "")
    name = kwargs.get("name", "")

    # Lookup required access level, returns 4 for config-things
    req_access = sabnzbd.api.api_level(mode, name)

    if req_access == 1 and check_access(1):
        # NZB-only actions
        pass
    elif not check_access(req_access):
        return "Access denied"

    # First check API-key, if OK that's sufficient
    if not cfg.disable_key():
        key = kwargs.get("apikey")
        if not key:
            if cfg.api_warnings():
                log_warning_and_ip(
                    T("API Key missing, please enter the api key from Config->General into your 3rd party program:")
                )
            return "API Key Required"
        elif req_access == 1 and key == cfg.nzb_key():
            return None
        elif key == cfg.api_key():
            return None
        else:
            log_warning_and_ip(T("API Key incorrect, Use the api key from Config->General in your 3rd party program:"))
            return "API Key Incorrect"

    # No active API-key, check web credentials instead
    if cfg.username() and cfg.password():
        if check_login() or (
            kwargs.get("ma_username") == cfg.username() and kwargs.get("ma_password") == cfg.password()
        ):
            pass
        else:
            if cfg.api_warnings():
                log_warning_and_ip(
                    T(
                        "Authentication missing, please enter username/password from Config->General into your 3rd party program:"
                    )
                )
            return "Missing authentication"
    return None


def log_warning_and_ip(txt):
    """ Include the IP and the Proxy-IP for warnings """
    # Was it proxy forwarded?
    xff = cherrypy.request.headers.get("X-Forwarded-For")
    if xff:
        txt = "%s %s (X-Forwarded-For: %s)>%s" % (
            txt,
            cherrypy.request.remote.ip,
            xff,
            cherrypy.request.headers.get("User-Agent", "??"),
        )
    else:
        txt = "%s %s>%s" % (txt, cherrypy.request.remote.ip, cherrypy.request.headers.get("User-Agent", "??"))
    logging.warning(txt)


##############################################################################
# Helper raiser functions
##############################################################################
def Raiser(root="", **kwargs):
    args = {}
    for key in kwargs:
        val = kwargs.get(key)
        if val:
            args[key] = val
    # Add extras
    if args:
        root = "%s?%s" % (root, urllib.parse.urlencode(args))
    # Optionally add the leading /sabnzbd/ (or what the user set)
    if not root.startswith(cfg.url_base()):
        root = cherrypy.request.script_name + root
    # Send the redirect
    return cherrypy.HTTPRedirect(root)


def queueRaiser(root, kwargs):
    return Raiser(root, start=kwargs.get("start"), limit=kwargs.get("limit"), search=kwargs.get("search"))


def rssRaiser(root, kwargs):
    return Raiser(root, feed=kwargs.get("feed"))


##############################################################################
# Page definitions
##############################################################################
class MainPage:
    def __init__(self):
        self.__root = "/"

        # Add all sub-pages
        self.login = LoginPage()
        self.queue = QueuePage("/queue/")
        self.history = HistoryPage("/history/")
        self.status = Status("/status/")
        self.config = ConfigPage("/config/")
        self.nzb = NzoPage("/nzb/")
        self.wizard = Wizard("/wizard/")

    @secured_expose
    def index(self, **kwargs):
        # Redirect to wizard if no servers are set
        if kwargs.get("skip_wizard") or config.get_servers():
            info = build_header()

            info["scripts"] = list_scripts(default=True)
            info["script"] = "Default"

            info["cat"] = "Default"
            info["categories"] = list_cats(True)
            info["have_rss_defined"] = bool(config.get_rss())
            info["have_watched_dir"] = bool(cfg.dirscan_dir())

            # Have logout only with HTML and if inet=5, only when we are external
            info["have_logout"] = (
                cfg.username()
                and cfg.password()
                and (
                    cfg.html_login()
                    and (cfg.inet_exposure() < 5 or (cfg.inet_exposure() == 5 and not check_access(access_type=6)))
                )
            )

            bytespersec_list = BPSMeter.do.get_bps_list()
            info["bytespersec_list"] = ",".join([str(bps) for bps in bytespersec_list])

            template = Template(
                file=os.path.join(sabnzbd.WEB_DIR, "main.tmpl"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
            )
            return template.respond()
        else:
            # Redirect to the setup wizard
            raise cherrypy.HTTPRedirect("%s/wizard/" % cfg.url_base())

    @secured_expose(check_api_key=True)
    def shutdown(self, **kwargs):
        # Check for PID
        pid_in = kwargs.get("pid")
        if pid_in and int(pid_in) != os.getpid():
            return "Incorrect PID for this instance, remove PID from URL to initiate shutdown."

        sabnzbd.shutdown_program()
        return T("SABnzbd shutdown finished")

    @secured_expose(check_api_key=True)
    def pause(self, **kwargs):
        scheduler.plan_resume(0)
        Downloader.do.pause()
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def resume(self, **kwargs):
        scheduler.plan_resume(0)
        sabnzbd.unpause_all()
        raise Raiser(self.__root)

    @cherrypy.expose
    def api(self, **kwargs):
        """ Redirect to API-handler """
        return api_handler(kwargs)

    @secured_expose
    def scriptlog(self, **kwargs):
        """ Needed for all skins, URL is fixed due to postproc """
        # No session key check, due to fixed URLs
        name = kwargs.get("name")
        if name:
            history_db = sabnzbd.get_db_connection()
            return ShowString(history_db.get_name(name), history_db.get_script_log(name))
        else:
            raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def retry(self, **kwargs):
        """ Duplicate of retry of History, needed for some skins """
        job = kwargs.get("job", "")
        url = kwargs.get("url", "").strip()
        pp = kwargs.get("pp")
        cat = kwargs.get("cat")
        script = kwargs.get("script")
        if url:
            sabnzbd.add_url(url, pp, script, cat, nzbname=kwargs.get("nzbname"))
        del_hist_job(job, del_files=True)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def retry_pp(self, **kwargs):
        # Duplicate of History/retry_pp to please the SMPL skin :(
        retry_job(kwargs.get("job"), kwargs.get("nzbfile"), kwargs.get("password"))
        raise Raiser(self.__root)

    @secured_expose
    def robots_txt(self, **kwargs):
        """ Keep web crawlers out """
        cherrypy.response.headers["Content-Type"] = "text/plain"
        return "User-agent: *\nDisallow: /\n"


##############################################################################
class Wizard:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        """ Show the language selection page """
        if sabnzbd.WIN32:
            from sabnzbd.utils.apireg import get_install_lng

            cfg.language.set(get_install_lng())
            logging.debug('Installer language code "%s"', cfg.language())

        info = build_header(sabnzbd.WIZARD_DIR)
        info["languages"] = list_languages()
        template = Template(
            file=os.path.join(sabnzbd.WIZARD_DIR, "index.html"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
        )
        return template.respond()

    @secured_expose(check_configlock=True)
    def one(self, **kwargs):
        """ Accept language and show server page """
        if kwargs.get("lang"):
            cfg.language.set(kwargs.get("lang"))

        # Always setup Glitter
        change_web_dir("Glitter - Default")

        info = build_header(sabnzbd.WIZARD_DIR)
        info["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION

        # Just in case, add server
        servers = config.get_servers()
        if not servers:
            info["host"] = ""
            info["port"] = ""
            info["username"] = ""
            info["password"] = ""
            info["connections"] = ""
            info["ssl"] = 0
            info["ssl_verify"] = 2
        else:
            # Sort servers to get the first enabled one
            server_names = sorted(
                servers.keys(),
                key=lambda svr: "%d%02d%s"
                % (int(not servers[svr].enable()), servers[svr].priority(), servers[svr].displayname().lower()),
            )
            for server in server_names:
                # If there are multiple servers, just use the first enabled one
                s = servers[server]
                info["host"] = s.host()
                info["port"] = s.port()
                info["username"] = s.username()
                info["password"] = s.password.get_stars()
                info["connections"] = s.connections()
                info["ssl"] = s.ssl()
                info["ssl_verify"] = s.ssl_verify()
                if s.enable():
                    break
        template = Template(
            file=os.path.join(sabnzbd.WIZARD_DIR, "one.html"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
        )
        return template.respond()

    @secured_expose(check_configlock=True)
    def two(self, **kwargs):
        """ Accept server and show the final page for restart """
        # Save server details
        if kwargs:
            kwargs["enable"] = 1
            handle_server(kwargs)

        config.save_config()

        # Show Restart screen
        info = build_header(sabnzbd.WIZARD_DIR)

        info["access_url"], info["urls"] = get_access_info()
        info["download_dir"] = cfg.download_dir.get_clipped_path()
        info["complete_dir"] = cfg.complete_dir.get_clipped_path()

        template = Template(
            file=os.path.join(sabnzbd.WIZARD_DIR, "two.html"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
        )
        return template.respond()

    @secured_expose
    def exit(self, **kwargs):
        """ Stop SABnzbd """
        sabnzbd.shutdown_program()
        return T("SABnzbd shutdown finished")


def get_access_info():
    """ Build up a list of url's that sabnzbd can be accessed from """
    # Access_url is used to provide the user a link to sabnzbd depending on the host
    access_uri = "localhost"
    cherryhost = cfg.cherryhost()

    if cherryhost == "0.0.0.0":
        host = socket.gethostname()
        socks = [host]
        # Grab a list of all ips for the hostname
        try:
            addresses = socket.getaddrinfo(host, None)
        except:
            addresses = []
        for addr in addresses:
            address = addr[4][0]
            # Filter out ipv6 addresses (should not be allowed)
            if ":" not in address and address not in socks:
                socks.append(address)
        if "host" in cherrypy.request.headers:
            host = cherrypy.request.headers["host"]
            host = host.rsplit(":")[0]
            access_uri = host
            socks.insert(0, host)
        else:
            socks.insert(0, "localhost")

    elif cherryhost == "::":
        host = socket.gethostname()
        socks = [host]
        # Grab a list of all ips for the hostname
        addresses = socket.getaddrinfo(host, None)
        for addr in addresses:
            address = addr[4][0]
            # Only ipv6 addresses will work
            if ":" in address:
                address = "[%s]" % address
                if address not in socks:
                    socks.append(address)
        if "host" in cherrypy.request.headers:
            host = cherrypy.request.headers["host"]
            host = host.rsplit(":")[0]
            access_uri = host
            socks.insert(0, host)
        else:
            socks.insert(0, "localhost")

    elif not cherryhost:
        socks = [socket.gethostname()]
        access_uri = socket.gethostname()
    else:
        socks = [cherryhost]
        access_uri = cherryhost

    urls = []
    for sock in socks:
        if sock:
            if cfg.enable_https() and cfg.https_port():
                url = "https://%s:%s%s" % (sock, cfg.https_port(), cfg.url_base())
            elif cfg.enable_https():
                url = "https://%s:%s%s" % (sock, cfg.cherryport(), cfg.url_base())
            else:
                url = "http://%s:%s%s" % (sock, cfg.cherryport(), cfg.url_base())

            urls.append(url)

    if cfg.enable_https() and cfg.https_port():
        access_url = "https://%s:%s%s" % (sock, cfg.https_port(), cfg.url_base())
    elif cfg.enable_https():
        access_url = "https://%s:%s%s" % (access_uri, cfg.cherryport(), cfg.url_base())
    else:
        access_url = "http://%s:%s%s" % (access_uri, cfg.cherryport(), cfg.url_base())

    return access_url, urls


##############################################################################
class LoginPage:
    @cherrypy.expose
    def index(self, **kwargs):
        # Base output var
        info = build_header(sabnzbd.WEB_DIR_CONFIG)
        info["error"] = ""

        # Logout?
        if kwargs.get("logout"):
            set_login_cookie(remove=True)
            raise Raiser()

        # Check if there's even a username/password set
        if check_login():
            raise Raiser(cherrypy.request.script_name + "/")

        # Was it proxy forwarded?
        xff = cherrypy.request.headers.get("X-Forwarded-For")

        # Check login info
        if kwargs.get("username") == cfg.username() and kwargs.get("password") == cfg.password():
            # Save login cookie
            set_login_cookie(remember_me=kwargs.get("remember_me", False))
            # Log the succes
            if xff:
                logging.info("Successful login from %s (X-Forwarded-For: %s)", cherrypy.request.remote.ip, xff)
            else:
                logging.info("Successful login from %s", cherrypy.request.remote.ip)
            # Redirect
            raise Raiser(cherrypy.request.script_name + "/")
        elif kwargs.get("username") or kwargs.get("password"):
            info["error"] = T("Authentication failed, check username/password.")
            # Warn about the potential security problem
            fail_msg = T("Unsuccessful login attempt from %s") % cherrypy.request.remote.ip
            if xff:
                fail_msg = "%s (X-Forwarded-For: %s)" % (fail_msg, xff)
            logging.warning(fail_msg)

        # Show login
        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "login", "main.tmpl"),
            searchList=[info],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()


##############################################################################
class NzoPage:
    def __init__(self, root):
        self.__root = root
        self.__cached_selection = {}  # None

    @secured_expose
    def default(self, *args, **kwargs):
        # Allowed URL's
        # /nzb/SABnzbd_nzo_xxxxx/
        # /nzb/SABnzbd_nzo_xxxxx/details
        # /nzb/SABnzbd_nzo_xxxxx/files
        # /nzb/SABnzbd_nzo_xxxxx/bulk_operation
        # /nzb/SABnzbd_nzo_xxxxx/save
        nzo_id = None
        for a in args:
            if a.startswith("SABnzbd_nzo"):
                nzo_id = a
                break

        nzo = NzbQueue.do.get_nzo(nzo_id)
        if nzo_id and nzo:
            info, pnfo_list, bytespersec, q_size, bytes_left_previous_page = build_queue_header()

            # /SABnzbd_nzo_xxxxx/bulk_operation
            if "bulk_operation" in args:
                return self.bulk_operation(nzo_id, kwargs)

            # /SABnzbd_nzo_xxxxx/details
            elif "details" in args:
                info = self.nzo_details(info, pnfo_list, nzo_id)

            # /SABnzbd_nzo_xxxxx/files
            elif "files" in args:
                info = self.nzo_files(info, nzo_id)

            # /SABnzbd_nzo_xxxxx/save
            elif "save" in args:
                self.save_details(nzo_id, args, kwargs)
                return  # never reached

            # /SABnzbd_nzo_xxxxx/
            else:
                info = self.nzo_details(info, pnfo_list, nzo_id)
                info = self.nzo_files(info, nzo_id)

            template = Template(
                file=os.path.join(sabnzbd.WEB_DIR, "nzo.tmpl"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
            )
            return template.respond()
        else:
            # Job no longer exists, go to main page
            raise Raiser(urllib.parse.urljoin(self.__root, "../queue/"))

    def nzo_details(self, info, pnfo_list, nzo_id):
        slot = {}
        n = 0
        for pnfo in pnfo_list:
            if pnfo.nzo_id == nzo_id:
                nzo = NzbQueue.do.get_nzo(nzo_id)
                repair = pnfo.repair
                unpack = pnfo.unpack
                delete = pnfo.delete
                unpackopts = opts_to_pp(repair, unpack, delete)
                script = pnfo.script
                if script is None:
                    script = "None"
                cat = pnfo.category
                if not cat:
                    cat = "None"

                slot["nzo_id"] = str(nzo_id)
                slot["cat"] = cat
                slot["filename"] = nzo.final_name
                slot["filename_clean"] = nzo.final_name
                slot["password"] = nzo.password or ""
                slot["script"] = script
                slot["priority"] = str(pnfo.priority)
                slot["unpackopts"] = str(unpackopts)
                info["index"] = n
                break
            n += 1

        info["slot"] = slot
        info["scripts"] = list_scripts()
        info["categories"] = list_cats()
        info["noofslots"] = len(pnfo_list)

        return info

    def nzo_files(self, info, nzo_id):
        active = []
        nzo = NzbQueue.do.get_nzo(nzo_id)
        if nzo:
            pnfo = nzo.gather_info(full=True)
            info["nzo_id"] = pnfo.nzo_id
            info["filename"] = pnfo.filename

            for nzf in pnfo.active_files:
                checked = False
                if nzf.nzf_id in self.__cached_selection and self.__cached_selection[nzf.nzf_id] == "on":
                    checked = True
                active.append(
                    {
                        "filename": nzf.filename if nzf.filename else nzf.subject,
                        "mbleft": "%.2f" % (nzf.bytes_left / MEBI),
                        "mb": "%.2f" % (nzf.bytes / MEBI),
                        "size": format_bytes(nzf.bytes),
                        "sizeleft": format_bytes(nzf.bytes_left),
                        "nzf_id": nzf.nzf_id,
                        "age": calc_age(nzf.date),
                        "checked": checked,
                    }
                )

        info["active_files"] = active
        return info

    def save_details(self, nzo_id, args, kwargs):
        index = kwargs.get("index", None)
        name = kwargs.get("name", None)
        password = kwargs.get("password", None)
        if password == "":
            password = None
        pp = kwargs.get("pp", None)
        script = kwargs.get("script", None)
        cat = kwargs.get("cat", None)
        priority = kwargs.get("priority", None)
        nzo = NzbQueue.do.get_nzo(nzo_id)

        if index is not None:
            NzbQueue.do.switch(nzo_id, index)
        if name is not None:
            NzbQueue.do.change_name(nzo_id, name, password)

        if cat is not None and nzo.cat is not cat and not (nzo.cat == "*" and cat == "Default"):
            NzbQueue.do.change_cat(nzo_id, cat, priority)
            # Category changed, so make sure "Default" attributes aren't set again
            if script == "Default":
                script = None
            if priority == "Default":
                priority = None
            if pp == "Default":
                pp = None

        if script is not None and nzo.script != script:
            NzbQueue.do.change_script(nzo_id, script)
        if pp is not None and nzo.pp != pp:
            NzbQueue.do.change_opts(nzo_id, pp)
        if priority is not None and nzo.priority != int(priority):
            NzbQueue.do.set_priority(nzo_id, priority)

        raise Raiser(urllib.parse.urljoin(self.__root, "../queue/"))

    def bulk_operation(self, nzo_id, kwargs):
        self.__cached_selection = kwargs
        if kwargs["action_key"] == "Delete":
            for key in kwargs:
                if kwargs[key] == "on":
                    NzbQueue.do.remove_nzf(nzo_id, key, force_delete=True)

        elif kwargs["action_key"] in ("Top", "Up", "Down", "Bottom"):
            nzf_ids = []
            for key in kwargs:
                if kwargs[key] == "on":
                    nzf_ids.append(key)
            size = int_conv(kwargs.get("action_size", 1))
            if kwargs["action_key"] == "Top":
                NzbQueue.do.move_top_bulk(nzo_id, nzf_ids)
            elif kwargs["action_key"] == "Up":
                NzbQueue.do.move_up_bulk(nzo_id, nzf_ids, size)
            elif kwargs["action_key"] == "Down":
                NzbQueue.do.move_down_bulk(nzo_id, nzf_ids, size)
            elif kwargs["action_key"] == "Bottom":
                NzbQueue.do.move_bottom_bulk(nzo_id, nzf_ids)

        if NzbQueue.do.get_nzo(nzo_id):
            url = urllib.parse.urljoin(self.__root, nzo_id)
        else:
            url = urllib.parse.urljoin(self.__root, "../queue")
        if url and not url.endswith("/"):
            url += "/"
        raise Raiser(url)


##############################################################################
class QueuePage:
    def __init__(self, root):
        self.__root = root

    @secured_expose
    def index(self, **kwargs):
        start = int_conv(kwargs.get("start"))
        limit = int_conv(kwargs.get("limit"))
        search = kwargs.get("search")
        info, _pnfo_list, _bytespersec = build_queue(start=start, limit=limit, trans=True, search=search)

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR, "queue.tmpl"), searchList=[info], compilerSettings=CHEETAH_DIRECTIVES
        )
        return template.respond()

    @secured_expose(check_api_key=True)
    def delete(self, **kwargs):
        uid = kwargs.get("uid")
        del_files = int_conv(kwargs.get("del_files"))
        if uid:
            NzbQueue.do.remove(uid, add_to_history=False, delete_all_data=del_files)
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def purge(self, **kwargs):
        NzbQueue.do.remove_all(kwargs.get("search"))
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def change_queue_complete_action(self, **kwargs):
        """ Action or script to be performed once the queue has been completed
            Scripts are prefixed with 'script_'
        """
        action = kwargs.get("action")
        sabnzbd.change_queue_complete_action(action)
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def switch(self, **kwargs):
        uid1 = kwargs.get("uid1")
        uid2 = kwargs.get("uid2")
        if uid1 and uid2:
            NzbQueue.do.switch(uid1, uid2)
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def change_opts(self, **kwargs):
        nzo_id = kwargs.get("nzo_id")
        pp = kwargs.get("pp", "")
        if nzo_id and pp and pp.isdigit():
            NzbQueue.do.change_opts(nzo_id, int(pp))
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def change_script(self, **kwargs):
        nzo_id = kwargs.get("nzo_id")
        script = kwargs.get("script", "")
        if nzo_id and script:
            if script == "None":
                script = None
            NzbQueue.do.change_script(nzo_id, script)
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def change_cat(self, **kwargs):
        nzo_id = kwargs.get("nzo_id")
        cat = kwargs.get("cat", "")
        if nzo_id and cat:
            if cat == "None":
                cat = None
            NzbQueue.do.change_cat(nzo_id, cat)

        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def shutdown(self, **kwargs):
        sabnzbd.shutdown_program()
        return T("SABnzbd shutdown finished")

    @secured_expose(check_api_key=True)
    def pause(self, **kwargs):
        scheduler.plan_resume(0)
        Downloader.do.pause()
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def resume(self, **kwargs):
        scheduler.plan_resume(0)
        sabnzbd.unpause_all()
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def pause_nzo(self, **kwargs):
        uid = kwargs.get("uid", "")
        NzbQueue.do.pause_multiple_nzo(uid.split(","))
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def resume_nzo(self, **kwargs):
        uid = kwargs.get("uid", "")
        NzbQueue.do.resume_multiple_nzo(uid.split(","))
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def set_priority(self, **kwargs):
        NzbQueue.do.set_priority(kwargs.get("nzo_id"), kwargs.get("priority"))
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def sort_by_avg_age(self, **kwargs):
        NzbQueue.do.sort_queue("avg_age", kwargs.get("dir"))
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def sort_by_name(self, **kwargs):
        NzbQueue.do.sort_queue("name", kwargs.get("dir"))
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def sort_by_size(self, **kwargs):
        NzbQueue.do.sort_queue("size", kwargs.get("dir"))
        raise queueRaiser(self.__root, kwargs)


##############################################################################
class HistoryPage:
    def __init__(self, root):
        self.__root = root
        self.__failed_only = False

    @secured_expose
    def index(self, **kwargs):
        start = int_conv(kwargs.get("start"))
        limit = int_conv(kwargs.get("limit"))
        search = kwargs.get("search")
        failed_only = kwargs.get("failed_only")
        if failed_only is None:
            failed_only = self.__failed_only

        history = build_header()
        history["failed_only"] = failed_only
        history["rating_enable"] = bool(cfg.rating_enable())

        postfix = T("B")  # : Abbreviation for bytes, as in GB
        grand, month, week, day = BPSMeter.do.get_sums()
        history["total_size"], history["month_size"], history["week_size"], history["day_size"] = (
            to_units(grand, postfix=postfix),
            to_units(month, postfix=postfix),
            to_units(week, postfix=postfix),
            to_units(day, postfix=postfix),
        )

        history["lines"], history["fetched"], history["noofslots"] = build_history(
            limit=limit, start=start, search=search, failed_only=failed_only
        )

        if search:
            history["search"] = escape(search)
        else:
            history["search"] = ""

        history["start"] = int_conv(start)
        history["limit"] = int_conv(limit)
        history["finish"] = history["start"] + history["limit"]
        if history["finish"] > history["noofslots"]:
            history["finish"] = history["noofslots"]
        if not history["finish"]:
            history["finish"] = history["fetched"]
        history["time_format"] = time_format

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR, "history.tmpl"),
            searchList=[history],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True)
    def purge(self, **kwargs):
        history_db = sabnzbd.get_db_connection()
        history_db.remove_history()
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def delete(self, **kwargs):
        job = kwargs.get("job")
        del_files = int_conv(kwargs.get("del_files"))
        if job:
            jobs = job.split(",")
            for job in jobs:
                del_hist_job(job, del_files=del_files)
        raise queueRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True)
    def retry_pp(self, **kwargs):
        retry_job(kwargs.get("job"), kwargs.get("nzbfile"), kwargs.get("password"))
        raise queueRaiser(self.__root, kwargs)


##############################################################################
class ConfigPage:
    def __init__(self, root):
        self.__root = root
        self.folders = ConfigFolders("/config/folders/")
        self.notify = ConfigNotify("/config/notify/")
        self.general = ConfigGeneral("/config/general/")
        self.rss = ConfigRss("/config/rss/")
        self.scheduling = ConfigScheduling("/config/scheduling/")
        self.server = ConfigServer("/config/server/")
        self.switches = ConfigSwitches("/config/switches/")
        self.categories = ConfigCats("/config/categories/")
        self.sorting = ConfigSorting("/config/sorting/")
        self.special = ConfigSpecial("/config/special/")

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
        conf["configfn"] = clip_path(config.get_filename())
        conf["cmdline"] = sabnzbd.CMDLINE
        conf["build"] = sabnzbd.__baseline__[:7]

        conf["have_unzip"] = bool(sabnzbd.newsunpack.ZIP_COMMAND)
        conf["have_7zip"] = bool(sabnzbd.newsunpack.SEVEN_COMMAND)
        conf["have_sabyenc"] = SABYENC_ENABLED
        conf["have_mt_par2"] = sabnzbd.newsunpack.PAR2_MT

        conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION
        conf["ssl_version"] = ssl.OPENSSL_VERSION

        new = {}
        for svr in config.get_servers():
            new[svr] = {}
        conf["servers"] = new

        conf["folders"] = NzbQueue.do.scan_jobs(all_jobs=False, action=False)

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True)
    def restart(self, **kwargs):
        logging.info("Restart requested by interface")
        # Do the shutdown async to still send goodbye to browser
        Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start()
        return T(
            '&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 />'
        )

    @secured_expose(check_api_key=True)
    def repair(self, **kwargs):
        logging.info("Queue repair requested by interface")
        sabnzbd.request_repair()
        # Do the shutdown async to still send goodbye to browser
        Thread(target=sabnzbd.trigger_restart, kwargs={"timeout": 1}).start()
        return T(
            '&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 />'
        )


##############################################################################
LIST_DIRPAGE = (
    "download_dir",
    "download_free",
    "complete_dir",
    "admin_dir",
    "nzb_backup_dir",
    "dirscan_dir",
    "dirscan_speed",
    "script_dir",
    "email_dir",
    "permissions",
    "log_dir",
    "password_file",
)


class ConfigFolders:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        conf = build_header(sabnzbd.WEB_DIR_CONFIG)

        for kw in LIST_DIRPAGE:
            conf[kw] = config.get_config("misc", kw)()

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_folders.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def saveDirectories(self, **kwargs):
        for kw in LIST_DIRPAGE:
            value = kwargs.get(kw)
            if value is not None:
                if kw in ("complete_dir", "dirscan_dir"):
                    msg = config.get_config("misc", kw).set(value, create=True)
                else:
                    msg = config.get_config("misc", kw).set(value)
                if msg:
                    # return sabnzbd.api.report('json', error=msg)
                    return badParameterResponse(msg, kwargs.get("ajax"))

        sabnzbd.check_incomplete_vs_complete()
        config.save_config()
        if kwargs.get("ajax"):
            return sabnzbd.api.report("json")
        else:
            raise Raiser(self.__root)


##############################################################################
SWITCH_LIST = (
    "par_option",
    "top_only",
    "direct_unpack",
    "enable_meta",
    "win_process_prio",
    "auto_sort",
    "propagation_delay",
    "auto_disconnect",
    "flat_unpack",
    "safe_postproc",
    "no_dupes",
    "replace_spaces",
    "replace_dots",
    "ignore_samples",
    "pause_on_post_processing",
    "nice",
    "ionice",
    "pre_script",
    "pause_on_pwrar",
    "sfv_check",
    "folder_rename",
    "load_balancing",
    "quota_size",
    "quota_day",
    "quota_resume",
    "quota_period",
    "history_retention",
    "pre_check",
    "max_art_tries",
    "fail_hopeless_jobs",
    "enable_all_par",
    "enable_recursive",
    "no_series_dupes",
    "series_propercheck",
    "script_can_fail",
    "new_nzb_on_failure",
    "unwanted_extensions",
    "action_on_unwanted_extensions",
    "sanitize_safe",
    "rating_enable",
    "rating_api_key",
    "rating_filter_enable",
    "rating_filter_abort_audio",
    "rating_filter_abort_video",
    "rating_filter_abort_encrypted",
    "rating_filter_abort_encrypted_confirm",
    "rating_filter_abort_spam",
    "rating_filter_abort_spam_confirm",
    "rating_filter_abort_downvoted",
    "rating_filter_abort_keywords",
    "rating_filter_pause_audio",
    "rating_filter_pause_video",
    "rating_filter_pause_encrypted",
    "rating_filter_pause_encrypted_confirm",
    "rating_filter_pause_spam",
    "rating_filter_pause_spam_confirm",
    "rating_filter_pause_downvoted",
    "rating_filter_pause_keywords",
)


class ConfigSwitches:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        conf = build_header(sabnzbd.WEB_DIR_CONFIG)

        conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION
        conf["have_nice"] = bool(sabnzbd.newsunpack.NICE_COMMAND)
        conf["have_ionice"] = bool(sabnzbd.newsunpack.IONICE_COMMAND)
        conf["cleanup_list"] = cfg.cleanup_list.get_string()

        for kw in SWITCH_LIST:
            conf[kw] = config.get_config("misc", kw)()
        conf["unwanted_extensions"] = cfg.unwanted_extensions.get_string()

        conf["scripts"] = list_scripts() or ["None"]

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_switches.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def saveSwitches(self, **kwargs):
        for kw in SWITCH_LIST:
            item = config.get_config("misc", kw)
            value = kwargs.get(kw)
            if kw == "unwanted_extensions" and value:
                value = value.lower().replace(".", "")
            msg = item.set(value)
            if msg:
                return badParameterResponse(msg, kwargs.get("ajax"))

        cleanup_list = kwargs.get("cleanup_list")
        if cleanup_list and sabnzbd.WIN32:
            cleanup_list = cleanup_list.lower()
        cfg.cleanup_list.set(cleanup_list)

        config.save_config()
        if kwargs.get("ajax"):
            return sabnzbd.api.report("json")
        else:
            raise Raiser(self.__root)


##############################################################################
SPECIAL_BOOL_LIST = (
    "start_paused",
    "no_penalties",
    "fast_fail",
    "ignore_wrong_unrar",
    "overwrite_files",
    "enable_par_cleanup",
    "queue_complete_pers",
    "api_warnings",
    "ampm",
    "enable_unrar",
    "enable_unzip",
    "enable_7zip",
    "enable_filejoin",
    "enable_tsjoin",
    "ignore_unrar_dates",
    "osx_menu",
    "osx_speed",
    "win_menu",
    "allow_incomplete_nzb",
    "rss_filenames",
    "ipv6_hosting",
    "keep_awake",
    "empty_postproc",
    "html_login",
    "wait_for_dfolder",
    "max_art_opt",
    "warn_empty_nzb",
    "enable_bonjour",
    "warn_dupl_jobs",
    "replace_illegal",
    "backup_for_duplicates",
    "disable_api_key",
    "api_logging",
    "x_frame_options",
    "require_modern_tls",
)
SPECIAL_VALUE_LIST = (
    "size_limit",
    "movie_rename_limit",
    "nomedia_marker",
    "max_url_retries",
    "req_completion_rate",
    "wait_ext_drive",
    "show_sysload",
    "url_base",
    "direct_unpack_threads",
    "ipv6_servers",
    "selftest_host",
    "rating_host",
)
SPECIAL_LIST_LIST = ("rss_odd_titles", "quick_check_ext_ignore", "host_whitelist")


class ConfigSpecial:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
        conf["switches"] = [
            (kw, config.get_config("misc", kw)(), config.get_config("misc", kw).default()) for kw in SPECIAL_BOOL_LIST
        ]
        conf["entries"] = [
            (kw, config.get_config("misc", kw)(), config.get_config("misc", kw).default()) for kw in SPECIAL_VALUE_LIST
        ]
        conf["entries"].extend(
            [
                (kw, config.get_config("misc", kw).get_string(), config.get_config("misc", kw).default_string())
                for kw in SPECIAL_LIST_LIST
            ]
        )

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_special.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def saveSpecial(self, **kwargs):
        for kw in SPECIAL_BOOL_LIST + SPECIAL_VALUE_LIST + SPECIAL_LIST_LIST:
            item = config.get_config("misc", kw)
            value = kwargs.get(kw)
            msg = item.set(value)
            if msg:
                return badParameterResponse(msg)

        config.save_config()
        raise Raiser(self.__root)


##############################################################################
GENERAL_LIST = (
    "host",
    "port",
    "username",
    "refresh_rate",
    "language",
    "cache_limit",
    "local_ranges",
    "inet_exposure",
    "enable_https",
    "https_port",
    "https_cert",
    "https_key",
    "https_chain",
    "enable_https_verification",
    "auto_browser",
    "check_new_rel",
)


class ConfigGeneral:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        def ListColors(web_dir):
            lst = []
            web_dir = os.path.join(sabnzbd.DIR_INTERFACES, web_dir)
            dd = os.path.abspath(web_dir + "/templates/static/stylesheets/colorschemes")
            if (not dd) or (not os.access(dd, os.R_OK)):
                return lst
            for color in globber(dd):
                col = color.replace(".css", "")
                lst.append(col)
            return lst

        def add_color(skin_dir, color):
            if skin_dir:
                if not color:
                    try:
                        color = DEF_SKIN_COLORS[skin_dir.lower()]
                    except KeyError:
                        return skin_dir
                return "%s - %s" % (skin_dir, color)
            else:
                return ""

        conf = build_header(sabnzbd.WEB_DIR_CONFIG)

        conf["configfn"] = config.get_filename()
        conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION

        wlist = []
        interfaces = globber_full(sabnzbd.DIR_INTERFACES)
        for k in interfaces:
            if k.endswith(DEF_STDCONFIG):
                interfaces.remove(k)
                continue

        for web in interfaces:
            rweb = os.path.basename(web)
            if os.access(os.path.join(web, DEF_MAIN_TMPL), os.R_OK):
                cols = ListColors(rweb)
                if cols:
                    for col in cols:
                        wlist.append(add_color(rweb, col))
                else:
                    wlist.append(rweb)
        conf["web_list"] = wlist
        conf["web_dir"] = add_color(cfg.web_dir(), cfg.web_color())
        conf["password"] = cfg.password.get_stars()

        conf["language"] = cfg.language()
        lang_list = list_languages()
        if len(lang_list) < 2:
            lang_list = []
        conf["lang_list"] = lang_list

        for kw in GENERAL_LIST:
            conf[kw] = config.get_config("misc", kw)()

        conf["bandwidth_max"] = cfg.bandwidth_max()
        conf["bandwidth_perc"] = cfg.bandwidth_perc()
        conf["nzb_key"] = cfg.nzb_key()
        conf["local_ranges"] = cfg.local_ranges.get_string()
        conf["my_lcldata"] = cfg.admin_dir.get_clipped_path()
        conf["caller_url"] = cherrypy.request.base + cfg.url_base()

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_general.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def saveGeneral(self, **kwargs):
        # Handle general options
        for kw in GENERAL_LIST:
            item = config.get_config("misc", kw)
            value = kwargs.get(kw)
            msg = item.set(value)
            if msg:
                return badParameterResponse(msg)

        # Handle special options
        cfg.password.set(kwargs.get("password"))

        web_dir = kwargs.get("web_dir")
        change_web_dir(web_dir)

        bandwidth_max = kwargs.get("bandwidth_max")
        if bandwidth_max is not None:
            cfg.bandwidth_max.set(bandwidth_max)
        bandwidth_perc = kwargs.get("bandwidth_perc")
        if bandwidth_perc is not None:
            cfg.bandwidth_perc.set(bandwidth_perc)
        bandwidth_perc = cfg.bandwidth_perc()
        if bandwidth_perc and not bandwidth_max:
            logging.warning(T("You must set a maximum bandwidth before you can set a bandwidth limit"))

        config.save_config()

        # Update CherryPy authentication
        set_auth(cherrypy.config)
        if kwargs.get("ajax"):
            return sabnzbd.api.report("json", data={"success": True, "restart_req": sabnzbd.RESTART_REQ})
        else:
            raise Raiser(self.__root)


def change_web_dir(web_dir):
    try:
        web_dir, web_color = web_dir.split(" - ")
    except:
        try:
            web_color = DEF_SKIN_COLORS[web_dir.lower()]
        except:
            web_color = ""

    web_dir_path = real_path(sabnzbd.DIR_INTERFACES, web_dir)

    if not os.path.exists(web_dir_path):
        return badParameterResponse("Cannot find web template: %s" % web_dir_path)
    else:
        cfg.web_dir.set(web_dir)
        cfg.web_color.set(web_color)


##############################################################################
class ConfigServer:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
        new = []
        servers = config.get_servers()
        server_names = sorted(
            list(servers.keys()),
            key=lambda svr: "%d%02d%s"
            % (int(not servers[svr].enable()), servers[svr].priority(), servers[svr].displayname().lower()),
        )
        for svr in server_names:
            new.append(servers[svr].get_dict(safe=True))
            t, m, w, d, timeline = BPSMeter.do.amounts(svr)
            if t:
                new[-1]["amounts"] = to_units(t), to_units(m), to_units(w), to_units(d), timeline
        conf["servers"] = new
        conf["cats"] = list_cats(default=True)
        conf["certificate_validation"] = sabnzbd.CERTIFICATE_VALIDATION

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_server.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def addServer(self, **kwargs):
        return handle_server(kwargs, self.__root, True)

    @secured_expose(check_api_key=True, check_configlock=True)
    def saveServer(self, **kwargs):
        return handle_server(kwargs, self.__root)

    @secured_expose(check_api_key=True, check_configlock=True)
    def testServer(self, **kwargs):
        return handle_server_test(kwargs, self.__root)

    @secured_expose(check_api_key=True, check_configlock=True)
    def delServer(self, **kwargs):
        kwargs["section"] = "servers"
        kwargs["keyword"] = kwargs.get("server")
        del_from_section(kwargs)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True, check_configlock=True)
    def clrServer(self, **kwargs):
        server = kwargs.get("server")
        if server:
            BPSMeter.do.clear_server(server)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True, check_configlock=True)
    def toggleServer(self, **kwargs):
        server = kwargs.get("server")
        if server:
            svr = config.get_config("servers", server)
            if svr:
                svr.enable.set(not svr.enable())
                config.save_config()
                Downloader.do.update_server(server, server)
        raise Raiser(self.__root)


def unique_svr_name(server):
    """ Return a unique variant on given server name """
    num = 0
    svr = 1
    new_name = server
    while svr:
        if num:
            new_name = "%s@%d" % (server, num)
        else:
            new_name = "%s" % server
        svr = config.get_config("servers", new_name)
        num += 1
    return new_name


def check_server(host, port, ajax):
    """ Check if server address resolves properly """
    if host.lower() == "localhost" and sabnzbd.AMBI_LOCALHOST:
        return badParameterResponse(T("Warning: LOCALHOST is ambiguous, use numerical IP-address."), ajax)

    if GetServerParms(host, int_conv(port)):
        return ""
    else:
        return badParameterResponse(T('Server address "%s:%s" is not valid.') % (host, port), ajax)


def handle_server(kwargs, root=None, new_svr=False):
    """ Internal server handler """
    ajax = kwargs.get("ajax")
    host = kwargs.get("host", "").strip()
    if not host:
        return badParameterResponse(T("Server address required"), ajax)

    port = kwargs.get("port", "").strip()
    if not port:
        if not kwargs.get("ssl", "").strip():
            port = "119"
        else:
            port = "563"
        kwargs["port"] = port

    if kwargs.get("connections", "").strip() == "":
        kwargs["connections"] = "1"

    if kwargs.get("enable") == "1":
        msg = check_server(host, port, ajax)
        if msg:
            return msg

    # Default server name is just the host name
    server = host

    svr = None
    old_server = kwargs.get("server")
    if old_server:
        svr = config.get_config("servers", old_server)
    if svr:
        server = old_server
    else:
        svr = config.get_config("servers", server)

    if new_svr:
        server = unique_svr_name(server)

    for kw in ("ssl", "send_group", "enable", "optional"):
        if kw not in kwargs.keys():
            kwargs[kw] = None
    if svr and not new_svr:
        svr.set_dict(kwargs)
    else:
        old_server = None
        config.ConfigServer(server, kwargs)

    config.save_config()
    Downloader.do.update_server(old_server, server)
    if root:
        if ajax:
            return sabnzbd.api.report("json")
        else:
            raise Raiser(root)


def handle_server_test(kwargs, root):
    _result, msg = test_nntp_server_dict(kwargs)
    return msg


##############################################################################
class ConfigRss:
    def __init__(self, root):
        self.__root = root
        self.__refresh_readout = None  # Set to URL when new readout is needed
        self.__refresh_download = False  # True when feed needs to be read
        self.__refresh_force = False  # True if forced download of all matches is required
        self.__refresh_ignore = False  # True if first batch of new feed must be ignored
        self.__evaluate = False  # True if feed needs to be re-filtered
        self.__show_eval_button = False  # True if the "Apply filers" button should be shown
        self.__last_msg = ""  # Last error message from RSS reader

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        conf = build_header(sabnzbd.WEB_DIR_CONFIG)

        conf["scripts"] = list_scripts(default=True)
        pick_script = conf["scripts"] != []

        conf["categories"] = list_cats(default=True)
        pick_cat = conf["categories"] != []

        conf["rss_rate"] = cfg.rss_rate()

        rss = {}
        feeds = config.get_rss()
        for feed in feeds:
            rss[feed] = feeds[feed].get_dict()
            filters = feeds[feed].filters()
            rss[feed]["filters"] = filters
            rss[feed]["filter_states"] = [bool(sabnzbd.rss.convert_filter(f[4])) for f in filters]
            rss[feed]["filtercount"] = len(filters)

            rss[feed]["pick_cat"] = pick_cat
            rss[feed]["pick_script"] = pick_script
            rss[feed]["link"] = urllib.parse.quote_plus(feed.encode("utf-8"))
            rss[feed]["baselink"] = [get_base_url(uri) for uri in rss[feed]["uri"]]
            rss[feed]["uris"] = feeds[feed].uri.get_string()

        active_feed = kwargs.get("feed", "")
        conf["active_feed"] = active_feed
        conf["rss"] = rss
        conf["rss_next"] = time.strftime(time_format("%H:%M"), time.localtime(sabnzbd.rss.next_run()))

        if active_feed:
            readout = bool(self.__refresh_readout)
            logging.debug("RSS READOUT = %s", readout)
            if not readout:
                self.__refresh_download = False
                self.__refresh_force = False
                self.__refresh_ignore = False
            if self.__evaluate:
                msg = sabnzbd.rss.run_feed(
                    active_feed,
                    download=self.__refresh_download,
                    force=self.__refresh_force,
                    ignoreFirst=self.__refresh_ignore,
                    readout=readout,
                )
            else:
                msg = ""
            self.__evaluate = False
            if readout:
                sabnzbd.rss.save()
                self.__last_msg = msg
            else:
                msg = self.__last_msg
            self.__refresh_readout = None
            conf["evalButton"] = self.__show_eval_button
            conf["error"] = msg

            conf["downloaded"], conf["matched"], conf["unmatched"] = GetRssLog(active_feed)
        else:
            self.__last_msg = ""

        # Find a unique new Feed name
        unum = 1
        txt = T("Feed")  # : Used as default Feed name in Config->RSS
        while txt + str(unum) in feeds:
            unum += 1
        conf["feed"] = txt + str(unum)

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_rss.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def save_rss_rate(self, **kwargs):
        """ Save changed RSS automatic readout rate """
        cfg.rss_rate.set(kwargs.get("rss_rate"))
        config.save_config()
        scheduler.restart()
        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def upd_rss_feed(self, **kwargs):
        """ Update Feed level attributes,
            legacy version: ignores 'enable' parameter
        """
        if kwargs.get("enable") is not None:
            del kwargs["enable"]
        try:
            cf = config.get_rss()[kwargs.get("feed")]
        except KeyError:
            cf = None
        uri = Strip(kwargs.get("uri"))
        if cf and uri:
            kwargs["uri"] = uri
            cf.set_dict(kwargs)
            config.save_config()

        self.__evaluate = False
        self.__show_eval_button = True
        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def save_rss_feed(self, **kwargs):
        """ Update Feed level attributes """
        try:
            cf = config.get_rss()[kwargs.get("feed")]
        except KeyError:
            cf = None
        if "enable" not in kwargs:
            kwargs["enable"] = 0
        uri = Strip(kwargs.get("uri"))
        if cf and uri:
            kwargs["uri"] = uri
            cf.set_dict(kwargs)
            config.save_config()

        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def toggle_rss_feed(self, **kwargs):
        """ Toggle automatic read-out flag of Feed """
        try:
            item = config.get_rss()[kwargs.get("feed")]
        except KeyError:
            item = None
        if cfg:
            item.enable.set(not item.enable())
            config.save_config()
        if kwargs.get("table"):
            raise Raiser(self.__root)
        else:
            raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def add_rss_feed(self, **kwargs):
        """ Add one new RSS feed definition """
        feed = Strip(kwargs.get("feed")).strip("[]")
        uri = Strip(kwargs.get("uri"))
        if feed and uri:
            try:
                cfg = config.get_rss()[feed]
            except KeyError:
                cfg = None
            if (not cfg) and uri:
                kwargs["feed"] = feed
                kwargs["uri"] = uri
                config.ConfigRSS(feed, kwargs)
                # Clear out any existing reference to this feed name
                # Otherwise first-run detection can fail
                sabnzbd.rss.clear_feed(feed)
                config.save_config()
                self.__refresh_readout = feed
                self.__refresh_download = False
                self.__refresh_force = False
                self.__refresh_ignore = True
                self.__evaluate = True
                raise rssRaiser(self.__root, kwargs)
            else:
                raise Raiser(self.__root)
        else:
            raise Raiser(self.__root)

    @secured_expose(check_api_key=True, check_configlock=True)
    def upd_rss_filter(self, **kwargs):
        """ Wrapper, so we can call from api.py """
        self.internal_upd_rss_filter(**kwargs)

    def internal_upd_rss_filter(self, **kwargs):
        """ Save updated filter definition """
        try:
            feed_cfg = config.get_rss()[kwargs.get("feed")]
        except KeyError:
            raise rssRaiser(self.__root, kwargs)

        pp = kwargs.get("pp")
        if IsNone(pp):
            pp = ""
        script = ConvertSpecials(kwargs.get("script"))
        cat = ConvertSpecials(kwargs.get("cat"))
        prio = ConvertSpecials(kwargs.get("priority"))
        filt = kwargs.get("filter_text")
        enabled = kwargs.get("enabled", "0")

        if filt:
            feed_cfg.filters.update(
                int(kwargs.get("index", 0)), (cat, pp, script, kwargs.get("filter_type"), filt, prio, enabled)
            )

            # Move filter if requested
            index = int_conv(kwargs.get("index", ""))
            new_index = kwargs.get("new_index", "")
            if new_index and int_conv(new_index) != index:
                feed_cfg.filters.move(int(index), int_conv(new_index))

            config.save_config()
        self.__evaluate = False
        self.__show_eval_button = True
        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def del_rss_feed(self, *args, **kwargs):
        """ Remove complete RSS feed """
        kwargs["section"] = "rss"
        kwargs["keyword"] = kwargs.get("feed")
        del_from_section(kwargs)
        sabnzbd.rss.clear_feed(kwargs.get("feed"))
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True, check_configlock=True)
    def del_rss_filter(self, **kwargs):
        """ Wrapper, so we can call from api.py """
        self.internal_del_rss_filter(**kwargs)

    def internal_del_rss_filter(self, **kwargs):
        """ Remove one RSS filter """
        try:
            feed_cfg = config.get_rss()[kwargs.get("feed")]
        except KeyError:
            raise rssRaiser(self.__root, kwargs)

        feed_cfg.filters.delete(int(kwargs.get("index", 0)))
        config.save_config()
        self.__evaluate = False
        self.__show_eval_button = True
        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def download_rss_feed(self, *args, **kwargs):
        """ Force download of all matching jobs in a feed """
        if "feed" in kwargs:
            feed = kwargs["feed"]
            self.__refresh_readout = feed
            self.__refresh_download = True
            self.__refresh_force = True
            self.__refresh_ignore = False
            self.__evaluate = True
        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def clean_rss_jobs(self, *args, **kwargs):
        """ Remove processed RSS jobs from UI """
        sabnzbd.rss.clear_downloaded(kwargs["feed"])
        self.__evaluate = True
        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def test_rss_feed(self, *args, **kwargs):
        """ Read the feed content again and show results """
        if "feed" in kwargs:
            feed = kwargs["feed"]
            self.__refresh_readout = feed
            self.__refresh_download = False
            self.__refresh_force = False
            self.__refresh_ignore = True
            self.__evaluate = True
            self.__show_eval_button = False
        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def eval_rss_feed(self, *args, **kwargs):
        """ Re-apply the filters to the feed """
        if "feed" in kwargs:
            self.__refresh_download = False
            self.__refresh_force = False
            self.__refresh_ignore = False
            self.__show_eval_button = False
            self.__evaluate = True

        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def download(self, **kwargs):
        """ Download NZB from provider (Download button) """
        feed = kwargs.get("feed")
        url = kwargs.get("url")
        nzbname = kwargs.get("nzbname")
        att = sabnzbd.rss.lookup_url(feed, url)
        if att:
            pp = att.get("pp")
            cat = att.get("cat")
            script = att.get("script")
            prio = att.get("prio")

            if url:
                sabnzbd.add_url(url, pp, script, cat, prio, nzbname)
            # Need to pass the title instead
            sabnzbd.rss.flag_downloaded(feed, url)
        raise rssRaiser(self.__root, kwargs)

    @secured_expose(check_api_key=True, check_configlock=True)
    def rss_now(self, *args, **kwargs):
        """ Run an automatic RSS run now """
        scheduler.force_rss()
        raise rssRaiser(self.__root, kwargs)


def ConvertSpecials(p):
    """ Convert None to 'None' and 'Default' to '' """
    if p is None:
        p = "None"
    elif p.lower() == T("Default").lower():
        p = ""
    return p


def IsNone(value):
    """ Return True if either None, 'None' or '' """
    return value is None or value == "" or value.lower() == "none"


def Strip(txt):
    """ Return stripped string, can handle None """
    try:
        return txt.strip()
    except:
        return None


##############################################################################
_SCHED_ACTIONS = (
    "resume",
    "pause",
    "pause_all",
    "shutdown",
    "restart",
    "speedlimit",
    "pause_post",
    "resume_post",
    "scan_folder",
    "rss_scan",
    "remove_failed",
    "remove_completed",
    "pause_all_low",
    "pause_all_normal",
    "pause_all_high",
    "resume_all_low",
    "resume_all_normal",
    "resume_all_high",
    "enable_quota",
    "disable_quota",
)


class ConfigScheduling:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        def get_days():
            days = {
                "*": T("Daily"),
                "1": T("Monday"),
                "2": T("Tuesday"),
                "3": T("Wednesday"),
                "4": T("Thursday"),
                "5": T("Friday"),
                "6": T("Saturday"),
                "7": T("Sunday"),
            }
            return days

        conf = build_header(sabnzbd.WEB_DIR_CONFIG)

        actions = []
        actions.extend(_SCHED_ACTIONS)
        day_names = get_days()
        categories = list_cats(False)
        snum = 1
        conf["schedlines"] = []
        conf["taskinfo"] = []
        for ev in scheduler.sort_schedules(all_events=False):
            line = ev[3]
            conf["schedlines"].append(line)
            try:
                enabled, m, h, day_numbers, action = line.split(" ", 4)
            except:
                continue
            action = action.strip()
            try:
                action, value = action.split(" ", 1)
            except:
                value = ""
            value = value.strip()
            if value and not value.lower().strip("0123456789kmgtp%."):
                if "%" not in value and from_units(value) < 1.0:
                    value = T("off")  # : "Off" value for speedlimit in scheduler
                else:
                    if "%" not in value and 1 < int_conv(value) < 101:
                        value += "%"
                    value = value.upper()
            if action in actions:
                action = Ttemplate("sch-" + action)
            else:
                if action in ("enable_server", "disable_server"):
                    try:
                        value = '"%s"' % config.get_servers()[value].displayname()
                    except KeyError:
                        value = '"%s" <<< %s' % (value, T("Undefined server!"))
                    action = Ttemplate("sch-" + action)
                if action in ("pause_cat", "resume_cat"):
                    action = Ttemplate("sch-" + action)
                    if value not in categories:
                        # Category name change
                        value = '"%s" <<< %s' % (value, T("Incorrect parameter"))
                    else:
                        value = '"%s"' % value

            if day_numbers == "1234567":
                days_of_week = "Daily"
            elif day_numbers == "12345":
                days_of_week = "Weekdays"
            elif day_numbers == "67":
                days_of_week = "Weekends"
            else:
                days_of_week = ", ".join([day_names.get(i, "**") for i in day_numbers])

            item = (snum, "%02d" % int(h), "%02d" % int(m), days_of_week, "%s %s" % (action, value), enabled)

            conf["taskinfo"].append(item)
            snum += 1

        actions_lng = {}
        for action in actions:
            actions_lng[action] = Ttemplate("sch-" + action)

        actions_servers = {}
        servers = config.get_servers()
        for srv in servers:
            actions_servers[srv] = servers[srv].displayname()

        conf["actions_servers"] = actions_servers
        conf["actions"] = actions
        conf["actions_lng"] = actions_lng
        conf["categories"] = categories

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_scheduling.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def addSchedule(self, **kwargs):
        servers = config.get_servers()
        minute = kwargs.get("minute")
        hour = kwargs.get("hour")
        days_of_week = "".join([str(x) for x in kwargs.get("daysofweek", "")])
        if not days_of_week:
            days_of_week = "1234567"
        action = kwargs.get("action")
        arguments = kwargs.get("arguments")

        arguments = arguments.strip().lower()
        if arguments in ("on", "enable"):
            arguments = "1"
        elif arguments in ("off", "disable"):
            arguments = "0"

        if minute and hour and days_of_week and action:
            if action == "speedlimit":
                if not arguments or arguments.strip("0123456789kmgtp%."):
                    arguments = 0
            elif action in _SCHED_ACTIONS:
                arguments = ""
            elif action in servers:
                if arguments == "1":
                    arguments = action
                    action = "enable_server"
                else:
                    arguments = action
                    action = "disable_server"

            elif action in ("pause_cat", "resume_cat"):
                # Need original category name, not lowercased
                arguments = arguments.strip()
            else:
                # Something else, leave empty
                action = None

            if action:
                sched = cfg.schedules()
                sched.append("%s %s %s %s %s %s" % (1, minute, hour, days_of_week, action, arguments))
                cfg.schedules.set(sched)

        config.save_config()
        scheduler.restart(force=True)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True, check_configlock=True)
    def delSchedule(self, **kwargs):
        schedules = cfg.schedules()
        line = kwargs.get("line")
        if line and line in schedules:
            schedules.remove(line)
            cfg.schedules.set(schedules)
            config.save_config()
            scheduler.restart(force=True)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True, check_configlock=True)
    def toggleSchedule(self, **kwargs):
        schedules = cfg.schedules()
        line = kwargs.get("line")
        if line:
            for i, schedule in enumerate(schedules):
                if schedule == line:
                    # Toggle the schedule
                    schedule_split = schedule.split()
                    schedule_split[0] = "%d" % (schedule_split[0] == "0")
                    schedules[i] = " ".join(schedule_split)
                    break
            cfg.schedules.set(schedules)
            config.save_config()
            scheduler.restart(force=True)
        raise Raiser(self.__root)


##############################################################################
class ConfigCats:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        conf = build_header(sabnzbd.WEB_DIR_CONFIG)

        conf["scripts"] = list_scripts(default=True)
        conf["defdir"] = cfg.complete_dir.get_clipped_path()

        categories = config.get_ordered_categories()
        conf["have_cats"] = len(categories) > 1

        slotinfo = []
        for cat in categories:
            cat["newzbin"] = cat["newzbin"].replace('"', "&quot;")
            slotinfo.append(cat)

        # Add empty line
        empty = {
            "name": "",
            "order": "0",
            "pp": "-1",
            "script": "",
            "dir": "",
            "newzbin": "",
            "priority": DEFAULT_PRIORITY,
        }
        slotinfo.insert(1, empty)
        conf["slotinfo"] = slotinfo

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_cat.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def delete(self, **kwargs):
        kwargs["section"] = "categories"
        kwargs["keyword"] = kwargs.get("name")
        del_from_section(kwargs)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True, check_configlock=True)
    def save(self, **kwargs):
        name = kwargs.get("name", "*")
        if name == "*":
            newname = name
        else:
            newname = re.sub('"', "", kwargs.get("newname", ""))
        if newname:
            # Check if this cat-dir is not sub-folder of incomplete
            if same_file(cfg.download_dir.get_path(), real_path(cfg.complete_dir.get_path(), kwargs["dir"])):
                return T("Category folder cannot be a subfolder of the Temporary Download Folder.")

            # Delete current one and replace with new one
            if name:
                config.delete("categories", name)
            config.ConfigCat(newname.lower(), kwargs)

        config.save_config()
        raise Raiser(self.__root)


##############################################################################
SORT_LIST = (
    "enable_tv_sorting",
    "tv_sort_string",
    "tv_categories",
    "enable_movie_sorting",
    "movie_sort_string",
    "movie_sort_extra",
    "movie_extra_folder",
    "enable_date_sorting",
    "date_sort_string",
    "movie_categories",
    "date_categories",
)


class ConfigSorting:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        conf = build_header(sabnzbd.WEB_DIR_CONFIG)
        conf["complete_dir"] = cfg.complete_dir.get_clipped_path()

        for kw in SORT_LIST:
            conf[kw] = config.get_config("misc", kw)()
        conf["categories"] = list_cats(False)

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_sorting.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def saveSorting(self, **kwargs):
        try:
            kwargs["movie_categories"] = kwargs["movie_cat"]
        except:
            pass
        try:
            kwargs["date_categories"] = kwargs["date_cat"]
        except:
            pass
        try:
            kwargs["tv_categories"] = kwargs["tv_cat"]
        except:
            pass

        for kw in SORT_LIST:
            item = config.get_config("misc", kw)
            value = kwargs.get(kw)
            msg = item.set(value)
            if msg:
                return badParameterResponse(msg)

        config.save_config()
        raise Raiser(self.__root)


##############################################################################
LOG_API_RE = re.compile(rb"(apikey|api)(=|:)[\w]+", re.I)
LOG_API_JSON_RE = re.compile(rb"'(apikey|api)': '[\w]+'", re.I)
LOG_USER_RE = re.compile(rb"(user|username)\s?=\s?[\S]+", re.I)
LOG_PASS_RE = re.compile(rb"(password)\s?=\s?[\S]+", re.I)
LOG_INI_HIDE_RE = re.compile(
    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]+",
    re.I,
)
LOG_HASH_RE = re.compile(rb"([a-fA-F\d]{25})", re.I)


class Status:
    def __init__(self, root):
        self.__root = root

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        header = build_status(skip_dashboard=kwargs.get("skip_dashboard"))
        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR, "status.tmpl"), searchList=[header], compilerSettings=CHEETAH_DIRECTIVES
        )
        return template.respond()

    @secured_expose(check_api_key=True)
    def reset_quota(self, **kwargs):
        BPSMeter.do.reset_quota(force=True)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def disconnect(self, **kwargs):
        Downloader.do.disconnect()
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def refresh_conn(self, **kwargs):
        # No real action, just reload the page
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def showlog(self, **kwargs):
        try:
            sabnzbd.LOGHANDLER.flush()
        except:
            pass

        # Fetch the INI and the log-data and add a message at the top
        log_data = b"--------------------------------\n\n"
        log_data += b"The log includes a copy of your sabnzbd.ini with\nall usernames, passwords and API-keys removed."
        log_data += b"\n\n--------------------------------\n"

        with open(sabnzbd.LOGFILE, "rb") as f:
            log_data += f.read()

        with open(config.get_filename(), "rb") as f:
            log_data += f.read()

        # We need to remove all passwords/usernames/api-keys
        log_data = LOG_API_RE.sub(b"apikey=<APIKEY>", log_data)
        log_data = LOG_API_JSON_RE.sub(b"'apikey':<APIKEY>'", log_data)
        log_data = LOG_USER_RE.sub(b"\\g<1>=<USER>", log_data)
        log_data = LOG_PASS_RE.sub(b"password=<PASSWORD>", log_data)
        log_data = LOG_INI_HIDE_RE.sub(b"\\1 = <REMOVED>", log_data)
        log_data = LOG_HASH_RE.sub(b"<HASH>", log_data)

        # Try to replace the username
        try:
            import getpass

            cur_user = getpass.getuser()
            if cur_user:
                log_data = log_data.replace(utob(cur_user), b"<USERNAME>")
        except:
            pass
        # Set headers
        cherrypy.response.headers["Content-Type"] = "application/x-download;charset=utf-8"
        cherrypy.response.headers["Content-Disposition"] = 'attachment;filename="sabnzbd.log"'
        return log_data

    @secured_expose(check_api_key=True)
    def clearwarnings(self, **kwargs):
        sabnzbd.GUIHANDLER.clear()
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def change_loglevel(self, **kwargs):
        cfg.log_level.set(kwargs.get("loglevel"))
        config.save_config()

        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def unblock_server(self, **kwargs):
        Downloader.do.unblock(kwargs.get("server"))
        # Short sleep so that UI shows new server status
        time.sleep(1.0)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def delete(self, **kwargs):
        orphan_delete(kwargs)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def delete_all(self, **kwargs):
        orphan_delete_all()
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def add(self, **kwargs):
        orphan_add(kwargs)
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def add_all(self, **kwargs):
        orphan_add_all()
        raise Raiser(self.__root)

    @secured_expose(check_api_key=True)
    def dashrefresh(self, **kwargs):
        # This function is run when Refresh button on Dashboard is clicked
        # Put the time consuming dashboard functions here; they only get executed when the user clicks the Refresh button

        # PyStone
        sabnzbd.PYSTONE_SCORE = getpystone()

        # Diskspeed of download (aka incomplete) directory:
        dir_speed = diskspeedmeasure(sabnzbd.cfg.download_dir.get_path())
        if dir_speed:
            sabnzbd.DOWNLOAD_DIR_SPEED = round(dir_speed, 1)
        else:
            sabnzbd.DOWNLOAD_DIR_SPEED = 0

        time.sleep(1.0)
        # Diskspeed of complete directory:
        dir_speed = diskspeedmeasure(sabnzbd.cfg.complete_dir.get_path())
        if dir_speed:
            sabnzbd.COMPLETE_DIR_SPEED = round(dir_speed, 1)
        else:
            sabnzbd.COMPLETE_DIR_SPEED = 0

        # Internet bandwidth
        sabnzbd.INTERNET_BANDWIDTH = round(internetspeed(), 1)

        raise Raiser(self.__root)  # Refresh screen


def orphan_delete(kwargs):
    path = kwargs.get("name")
    if path:
        path = os.path.join(long_path(cfg.download_dir.get_path()), path)
        logging.info("Removing orphaned job %s", path)
        remove_all(path, recursive=True)


def orphan_delete_all():
    paths = NzbQueue.do.scan_jobs(all_jobs=False, action=False)
    for path in paths:
        kwargs = {"name": path}
        orphan_delete(kwargs)


def orphan_add(kwargs):
    path = kwargs.get("name")
    if path:
        path = os.path.join(long_path(cfg.download_dir.get_path()), path)
        logging.info("Re-adding orphaned job %s", path)
        NzbQueue.do.repair_job(path, None, None)


def orphan_add_all():
    paths = NzbQueue.do.scan_jobs(all_jobs=False, action=False)
    for path in paths:
        kwargs = {"name": path}
        orphan_add(kwargs)


def badParameterResponse(msg, ajax=None):
    """ Return a html page with error message and a 'back' button """
    if ajax:
        return sabnzbd.api.report("json", error=msg)
    else:
        return """
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<html>
<head>
           <title>SABnzbd %s - %s</title>
</head>
<body>
<h3>%s</h3>
%s
<br><br>
<FORM><INPUT TYPE="BUTTON" VALUE="%s" ONCLICK="history.go(-1)"></FORM>
</body>
</html>
""" % (
            sabnzbd.__version__,
            T("ERROR:"),
            T("Incorrect parameter"),
            msg,
            T("Back"),
        )


def ShowString(name, msg):
    """ Return a html page listing a file and a 'back' button """
    return """
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
<html>
<head>
           <title>%s</title>
</head>
<body>
           <FORM><INPUT TYPE="BUTTON" VALUE="%s" ONCLICK="history.go(-1)"></FORM>
           <h3>%s</h3>
           <code><pre>%s</pre></code>
</body>
</html>
""" % (
        xml_name(name),
        T("Back"),
        xml_name(name),
        escape(msg),
    )


def GetRssLog(feed):
    def make_item(job):
        # Make a copy
        job = job.copy()

        # Now we apply some formatting
        job["title"] = job["title"]
        job["skip"] = "*" * int(job.get("status", "").endswith("*"))
        # These fields could be empty
        job["cat"] = job.get("cat", "")
        job["size"] = job.get("size", "")
        job["infourl"] = job.get("infourl", "")

        # Auto-fetched jobs didn't have these fields set
        if job.get("url"):
            job["baselink"] = get_base_url(job.get("url"))
            if sabnzbd.rss.special_rss_site(job.get("url")):
                job["nzbname"] = ""
            else:
                job["nzbname"] = job["title"]
        else:
            job["baselink"] = ""
            job["nzbname"] = job["title"]

        if job.get("size", 0):
            job["size_units"] = to_units(job["size"])
        else:
            job["size_units"] = "-"

        # And we add extra fields for sorting
        if job.get("age", 0):
            job["age_ms"] = (job["age"] - datetime.utcfromtimestamp(0)).total_seconds()
            job["age"] = calc_age(job["age"], True)
        else:
            job["age_ms"] = ""
            job["age"] = ""

        if job.get("time_downloaded"):
            job["time_downloaded_ms"] = time.mktime(job["time_downloaded"])
            job["time_downloaded"] = time.strftime(time_format("%H:%M %a %d %b"), job["time_downloaded"])
        else:
            job["time_downloaded_ms"] = ""
            job["time_downloaded"] = ""

        return job

    jobs = list(sabnzbd.rss.show_result(feed).values())
    good, bad, done = ([], [], [])
    for job in jobs:
        if job["status"][0] == "G":
            good.append(make_item(job))
        elif job["status"][0] == "B":
            bad.append(make_item(job))
        elif job["status"] == "D":
            done.append(make_item(job))

    try:
        # Sort based on actual age, in try-catch just to be sure
        good.sort(key=lambda job: job["age_ms"], reverse=True)
        bad.sort(key=lambda job: job["age_ms"], reverse=True)
        done.sort(key=lambda job: job["time_downloaded_ms"], reverse=True)
    except:
        # Let the javascript do it then..
        pass

    return done, good, bad


##############################################################################
LIST_EMAIL = (
    "email_endjob",
    "email_cats",
    "email_full",
    "email_server",
    "email_to",
    "email_from",
    "email_account",
    "email_pwd",
    "email_rss",
)
LIST_NCENTER = (
    "ncenter_enable",
    "ncenter_cats",
    "ncenter_prio_startup",
    "ncenter_prio_download",
    "ncenter_prio_pp",
    "ncenter_prio_complete",
    "ncenter_prio_failed",
    "ncenter_prio_disk_full",
    "ncenter_prio_warning",
    "ncenter_prio_error",
    "ncenter_prio_queue_done",
    "ncenter_prio_other",
    "ncenter_prio_new_login",
)
LIST_ACENTER = (
    "acenter_enable",
    "acenter_cats",
    "acenter_prio_startup",
    "acenter_prio_download",
    "acenter_prio_pp",
    "acenter_prio_complete",
    "acenter_prio_failed",
    "acenter_prio_disk_full",
    "acenter_prio_warning",
    "acenter_prio_error",
    "acenter_prio_queue_done",
    "acenter_prio_other",
    "acenter_prio_new_login",
)
LIST_NTFOSD = (
    "ntfosd_enable",
    "ntfosd_cats",
    "ntfosd_prio_startup",
    "ntfosd_prio_download",
    "ntfosd_prio_pp",
    "ntfosd_prio_complete",
    "ntfosd_prio_failed",
    "ntfosd_prio_disk_full",
    "ntfosd_prio_warning",
    "ntfosd_prio_error",
    "ntfosd_prio_queue_done",
    "ntfosd_prio_other",
    "ntfosd_prio_new_login",
)
LIST_PROWL = (
    "prowl_enable",
    "prowl_cats",
    "prowl_apikey",
    "prowl_prio_startup",
    "prowl_prio_download",
    "prowl_prio_pp",
    "prowl_prio_complete",
    "prowl_prio_failed",
    "prowl_prio_disk_full",
    "prowl_prio_warning",
    "prowl_prio_error",
    "prowl_prio_queue_done",
    "prowl_prio_other",
    "prowl_prio_new_login",
)
LIST_PUSHOVER = (
    "pushover_enable",
    "pushover_cats",
    "pushover_token",
    "pushover_userkey",
    "pushover_device",
    "pushover_prio_startup",
    "pushover_prio_download",
    "pushover_prio_pp",
    "pushover_prio_complete",
    "pushover_prio_failed",
    "pushover_prio_disk_full",
    "pushover_prio_warning",
    "pushover_prio_error",
    "pushover_prio_queue_done",
    "pushover_prio_other",
    "pushover_prio_new_login",
    "pushover_emergency_retry",
    "pushover_emergency_expire",
)
LIST_PUSHBULLET = (
    "pushbullet_enable",
    "pushbullet_cats",
    "pushbullet_apikey",
    "pushbullet_device",
    "pushbullet_prio_startup",
    "pushbullet_prio_download",
    "pushbullet_prio_pp",
    "pushbullet_prio_complete",
    "pushbullet_prio_failed",
    "pushbullet_prio_disk_full",
    "pushbullet_prio_warning",
    "pushbullet_prio_error",
    "pushbullet_prio_queue_done",
    "pushbullet_prio_other",
    "pushbullet_prio_new_login",
)
LIST_NSCRIPT = (
    "nscript_enable",
    "nscript_cats",
    "nscript_script",
    "nscript_parameters",
    "nscript_prio_startup",
    "nscript_prio_download",
    "nscript_prio_pp",
    "nscript_prio_complete",
    "nscript_prio_failed",
    "nscript_prio_disk_full",
    "nscript_prio_warning",
    "nscript_prio_error",
    "nscript_prio_queue_done",
    "nscript_prio_other",
    "nscript_prio_new_login",
)


class ConfigNotify:
    def __init__(self, root):
        self.__root = root
        self.__lastmail = None

    @secured_expose(check_configlock=True)
    def index(self, **kwargs):
        conf = build_header(sabnzbd.WEB_DIR_CONFIG)

        conf["categories"] = list_cats(False)
        conf["lastmail"] = self.__lastmail
        conf["have_ntfosd"] = sabnzbd.notifier.have_ntfosd()
        conf["have_ncenter"] = sabnzbd.DARWIN and sabnzbd.FOUNDATION
        conf["scripts"] = list_scripts(default=False, none=True)

        for kw in LIST_EMAIL:
            conf[kw] = config.get_config("misc", kw).get_string()
        for kw in LIST_PROWL:
            conf[kw] = config.get_config("prowl", kw)()
        for kw in LIST_PUSHOVER:
            conf[kw] = config.get_config("pushover", kw)()
        for kw in LIST_PUSHBULLET:
            conf[kw] = config.get_config("pushbullet", kw)()
        for kw in LIST_NCENTER:
            conf[kw] = config.get_config("ncenter", kw)()
        for kw in LIST_ACENTER:
            conf[kw] = config.get_config("acenter", kw)()
        for kw in LIST_NTFOSD:
            conf[kw] = config.get_config("ntfosd", kw)()
        for kw in LIST_NSCRIPT:
            conf[kw] = config.get_config("nscript", kw)()
        conf["notify_keys"] = sabnzbd.constants.NOTIFY_KEYS
        conf["notify_texts"] = sabnzbd.notifier.NOTIFICATION

        template = Template(
            file=os.path.join(sabnzbd.WEB_DIR_CONFIG, "config_notify.tmpl"),
            searchList=[conf],
            compilerSettings=CHEETAH_DIRECTIVES,
        )
        return template.respond()

    @secured_expose(check_api_key=True, check_configlock=True)
    def saveEmail(self, **kwargs):
        ajax = kwargs.get("ajax")

        for kw in LIST_EMAIL:
            msg = config.get_config("misc", kw).set(kwargs.get(kw))
            if msg:
                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
        for kw in LIST_NCENTER:
            msg = config.get_config("ncenter", kw).set(kwargs.get(kw))
            if msg:
                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
        for kw in LIST_ACENTER:
            msg = config.get_config("acenter", kw).set(kwargs.get(kw))
            if msg:
                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
        for kw in LIST_NTFOSD:
            msg = config.get_config("ntfosd", kw).set(kwargs.get(kw))
            if msg:
                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
        for kw in LIST_PROWL:
            msg = config.get_config("prowl", kw).set(kwargs.get(kw))
            if msg:
                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
        for kw in LIST_PUSHOVER:
            msg = config.get_config("pushover", kw).set(kwargs.get(kw))
            if msg:
                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
        for kw in LIST_PUSHBULLET:
            msg = config.get_config("pushbullet", kw).set(kwargs.get(kw, 0))
            if msg:
                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)
        for kw in LIST_NSCRIPT:
            msg = config.get_config("nscript", kw).set(kwargs.get(kw, 0))
            if msg:
                return badParameterResponse(T("Incorrect value for %s: %s") % (kw, msg), ajax)

        config.save_config()
        self.__lastmail = None
        if ajax:
            return sabnzbd.api.report("json")
        else:
            raise Raiser(self.__root)
