#!/usr/bin/python

from preupg.script_api import *

#END GENERATED SECTION

import sys, os
from difflib import unified_diff
from shutil import copy2

# internal files
import xml.NoisyElementTree as NoET
import xml.ElementTree as ET

##############################################################################
##### FUNC + CONST / GLOBVARS #####
##############################################################################
DEFAULT_DIFF_FILE = os.getcwd()+"/default_diff.diff"
APP_WEB_HOME="/usr/share/tomcat6/webapps"
CONFIG_DIR="/etc/tomcat6/"

GLOBAL_CONFIG_FILE = os.path.join(CONFIG_DIR, "server.xml")
GLOBAL_WEB_XML     = os.path.join(CONFIG_DIR, "web.xml")
GLOBAL_CONTEXT_XML = os.path.join(CONFIG_DIR, "context.xml")
GLOBAL_USER_XML    = os.path.join(CONFIG_DIR, "tomcat-users.xml")

# function will be called at the end of the scipt
_use_exit_func=exit_pass

# dict with all loaded element trees
etreeDict = dict()

# This script will be used during pre-upgrade section of redhat-upgrade-tool
# when not native 'tomcat' it is found.
PRE_UPGRADE_SCRIPT = "remove-tomcat-pre-upgrade.sh"
POST_SCRIPT        = "post-tomcat.py"
POST_PACKAGES      = "packages"
PRE_UPGRADE_DIR    = os.path.join(VALUE_TMP_PREUPGRADE, "preupgrade-scripts")
PRIVATE_POST_DIR   = os.path.join(POSTUPGRADE_DIR, "services/tomcat")


# contians messages which could be printed in solution text
solutionTexts = {
    "info_paths_changed" : (
        "* Paths changed in Red Hat Enterprise Linux 7 because"
        " the packages were renamed (tomcat6* is renamed to tomcat*). Your"
        " configuration files and web applications in the directories mentioned"
        " below will be moved to the new paths automatically:\n"
        "  /etc/tomcat6/ -> /etc/tomcat/\n"
        "  /usr/share/tomcat6/webapps/ -> /usr/share/tomcat/webapps/"
        ),
    "manager_role" : (
        "* The roles that are required to use the Manager application changed from a single manager"
        " role to the manager-gui, manager-script, manager-jmx, and manager-status roles."
        " Assign the roles required for the functionality that you want to access."
        " The manager role will be updated to manager-gui by the postupgrade script automatically,"
        " but a further update might be required if you use the other roles."
        ),
    "admin_role" : (
        "* The roles that are required to use the Host Manager application changed from a single"
        " admin role to the admin-gui and admin-script roles. Assign the roles"
        " required for the functionality that you want to access."
        " The admin role will be updated to admin-gui by the postupgrade script automatically,"
        " but a further update might be required if you use the admin-script role."
        ),
    "copyXML" : (
        "* XML context descriptors (META-INF/context.xml files) are no longer copied from deployed"
        " WAR files and directories to the host's xmlBase. The default Tomcat 6 behavior can be enabled"
        " by setting the 'copyXML' attribute of the Host element to 'True'."
        ),
    "attr_allow_deny" : (
        "* The 'allow' or 'deny' attributes are in use, and they use the tomcat6 regex list instead"
        " of a single expression. Previous versions of Tomcat allowed a list, but this version"
        " takes a single expression only. This needs to be addressed before an update can occur."
        ),
    "attr_proxies" : (
        "* The 'internalProxies' or 'trustedProxies' attributes are in use, and they use the tomcat6"
        " regex list instead of a single expression. Previous versions of Tomcat allowed a list,"
        " but this version takes a single expression only. This needs to be addressed before an update"
        " can occur."
        ),
    "attr_filter" : (
        "* The 'filter' attribute is in use, and it uses the tomcat6 regex list instead of a single"
        " expression. Previous versions of Tomcat allowed a list, but this version takes a single"
        " expression only. This needs to be addressed before an update can occur."
        ),
    "attr_user_agents" : (
        "* The 'retrictedUserAgents' or 'noCompressionUserAgents' attributes are in use, and they use"
        " the tomcat6 regex list instead of a single expression. Previous versions of Tomcat"
        " allowed a list, but this version takes a single expression only. This needs to be addressed"
        " before an update can occur."
        ),
    "randomClass" : (
        "* The 'randomClass' attribute of Manager has changed to 'secureRandomClass' and the provided"
        " class must extend 'java.secure.SecureRandom'. This will be corrected by the postupgrade"
        " script automatically."
        " There is a known issue with SecureRandom having long initialization times. If this"
        " is experienced, you can use the following system property:"
        " '-Djava.security.egd=file:/dev/./urandom'."
        ),
    "algorithm_entropy" : (
        "* The 'algorithm' and 'entropy' attributes have been removed. This will be corrected"
        " by the postupgrade script automatically."
        ),
    "emptySessionPath" : (
        "* The 'emptySessionPath' attribute of Connector has been obsoleted because it is now"
        " configurable from the Servlet 3.0 API. You can set 'sessionCookiePath' to a forward slash (/) of the"
        " 'Context' element in the corresponding context.xml file."
        " The obsoleted attribute will be removed automatically."
        ),
    "disableURLRewriting" : (
        "* The 'disableURLRewriting' attribute has been removed. This will be corrected"
        " by the postupgrade script automatically."
        ),
    "getStrAsCharArray" : (
        "* The 'genStrAsCharArray attribute' has been renamed to 'genStringAsCharArray'. This will"
        " be corrected by the postupgrade script automatically."
        ),
    "tomcatEPEL" : (
        "* A tomcat package from EPEL or a third party is installed and conflicts with the upgrade."
        " Remove the package before the upgrade."
        ),
    "defaultDiff" : (
        "* The difference between the '%s' system configuration file of tomcat6 and the default tomcat6"
        " configuration is stored in the [link:%s] file." % (
                GLOBAL_CONFIG_FILE,
                DEFAULT_DIFF_FILE.replace(get_dest_dir()+"/", "")
            )
        )
}

# When solution text has been used, set True, so next time will not be print
# again. Now init to False
solutionTextsUsed = {}
for key in solutionTexts.iterkeys():
    solutionTextsUsed[key] = False


def _print_solution(key):
    if not solutionTextsUsed[key]:
        solutionTextsUsed[key] = True
        solution_file("%s\n\n" % solutionTexts[key])

def _set_exit_func(efunc):
    "A function to set the correct exit"
    global _use_exit_func
    if efunc is exit_error:
        _use_exit_func = exit_error
    elif efunc is exit_fail and _use_exit_func is not exit_error:
        _use_exit_func = exit_fail
    elif efunc is exit_fixed and _use_exit_func not in [exit_error, exit_fail]:
        _use_exit_func = exit_fixed
    elif efunc is exit_informational and _use_exit_func is exit_pass:
        _use_exit_func = exit_informational


# {fname : ET or None}
### just get file list with given path ###
def get_file_list(path):
    "Return a list of all files in the path, including the files in its subdirectories."
    fList = list()
    for root, dummy_dirs, dummy_files in os.walk(path):
        fList.append(root)
    return fList

def get_lines(fname):
    "Get lines of a file"
    lines = None
    with open(fname, 'r') as handle:
        lines = map(lambda x: x.strip(), handle.readlines())
    return lines

def write_lines(fname, lines):
    """
    Write lines (list) to a file.

    If an item does not end with a line break, the line break will be added automatically.
    """
    fix_ln = lambda x: x if len(x) > 0 and  x[-1] == '\n' else x+'\n'
    with open(fname, 'w') as handle:
        handle.writelines(map(fix_ln, lines))


### print diff between
def print_info_diff():
    """
    Print a diff part against the default tomcat config file if the diff part is not empty.

    Create diff between the default and the current GLOBAL_CONFIG_FILE. When diff
    is empty, log an info message that the default configuration file is
    used.

    Otherwise log an info message and append a relevant info into the solution file.
    """
    curr_file = get_lines(GLOBAL_CONFIG_FILE)
    orig_file = get_lines("server.xml")
    diff = unified_diff(curr_file, orig_file,
                        fromfile=GLOBAL_CONFIG_FILE,
                        tofile="server.xml.orig")
    diff_lines = [line for line in diff]
    with open(DEFAULT_DIFF_FILE, "w") as handle:
        handle.writelines(diff_lines)

    # print must be here! not before write
    if len(diff_lines) > 0:
        _print_solution("defaultDiff")
        log_info("The difference between this system's tomcat6 configuration file"
                 " and the default configuration file is stored in the %s file."
                 % (DEFAULT_DIFF_FILE.replace(get_dest_dir()+"/", ""))
                )
    else:
        log_info("The default tomcat6 configuration file is used.")

### get_obsoleted_list ###
def get_obsoleted_tomcatList():
    """
    Get a list of obsoleted installed tomcat6* packages from common data.

    Each item of the return list is a string in the format:
    <current_package>|<new_package>

    E.g.: tomcat6;tomcat
    """
    installed = lambda x: is_pkg_installed(x) and is_dist_native(x)
    obsoleted_filtr = lambda x: "obsoleted" in x and "debug" not in x
    files = [fl for fl in os.listdir(COMMON_DIR) if obsoleted_filtr(fl)]
    tomcatList = list()
    for fn in files:
        lines = get_lines(os.path.join(COMMON_DIR, fn))
        tomcat_lines = [line for line in lines if line.startswith("tomcat6")]
        for line in tomcat_lines:
            ttmp = line.split()
            if installed(ttmp[0]):
                tomcatList.append("%s|%s" % (ttmp[0], ttmp[2]))
    return tomcatList


##############################################################################
##### check/fix functions #####
##############################################################################
#TODO: check functions as you can see are almost same as in postupgrade script
#      even fix actions are done here. But original files are not replaced
#      by this script. Changed copies:
#         a) will be stored in dirtyconf directory or
#         b) will be thrown
#      for now they will be thrown, but I count that will be used new API for
#      config files in future, to keep info about changes and store them into
#      relevant preupgrade directory (probably dirtyconf will be used).
#      BUT in that case! Think about possible changes after "preupg" by user,
#      so stored files will need be handled in different way. Just think about
#      it before new API will be used.
#####
#TODO: In case that functions below can be applied on any config/context XML
#      file, it can be shrink to one/two functions which will use specific
#      struct to handle proper situations. (That could be much more readable
#      and easier for maintainance in future than now...). Will be discussed
#####
def check_users(fname, verbose=True):
    """Check & Fix roles in a given XML file"""

    def _my_log_medium_risk(fname, role):
        log_medium_risk(
            "%s: The %s role is in use and has been changed in Tomcat 7. It will be"
            " automatically updated to a new %s-gui role, but it might require a further"
            " intervention." % (fname, role, role)
            )

    replace_role = lambda x,y,z: [i if i != y else z for i in x]
    changedAdmin = changedManager = False
    if etreeDict[fname] is None:
        return changed # nothing to do
    root = etreeDict[fname].getroot()
    # at first check and modify role
    for role in root.iter("role"):
        if role.get("rolename") == "admin":
            # set really admin-gui or another alternative?
            role.set("rolename", "admin-gui")
            if not changedAdmin and verbose:
                _print_solution("admin_role")
                _my_log_medium_risk(fname, "admin")
            changedAdmin = True
        elif role.get("rolename") == "manager":
            # set manager-gui or another alternative?
            role.set("rolename", "manager-gui")
            if not changedManager and verbose:
                _print_solution("manager_role")
                _my_log_medium_risk(fname, "manager")
            changedManager = True
    # now modify rolenames
    for user in root.iter("user"):
        roles = user.get("roles").split(",")
        if "admin" in roles:
            # replace admin by alternative
            replace_role(roles, "admin", "admin-gui")
            if not changedAdmin and verbose:
                _print_solution("admin_role")
                _my_log_medium_risk(fname, "admin")
            changedAdmin = True
        if "manager" in roles:
            # replace manager by alternative
            replace_role(roles, "manager", "manager-gui")
            if not changedManager and verbose:
                _print_solution("manager_role")
                _my_log_medium_risk(fname, "manager")
            changedManager = True
        user.set("roles", ",".join(roles))
    if changedManager or changedAdmin:
        _set_exit_func(exit_fail)
        return True
    return False

def check_session_manager(fname, verbose=True):
    changedSec = changedAlg = False
    if etreeDict[fname] is None:
        return changed # nothing to do
    root = etreeDict[fname].getroot()
    for manager in root.iter("Manager"):
        if "randomClass" in manager.attrib:
            # randomClass -> secureRandomClass
            manager.set("secureRandomClass", manager.get("randomClass"))
            manager.attrib.pop("randomClass")
            if not changedSec and verbose:
                _print_solution("randomClass")
                log_info("%s: The 'randomClass' attribute  will be changed to 'secureRandomClass'"
                         " automatically." % fname)
            _set_exit_func(exit_fixed)
            changedSec = True

        for attr in ["algorithm", "entropy"]:
            # remove deprecated attributes
            if attr not in manager.attrib:
                continue
            manager.attrib.pop(attr)
            if not changedAlg and verbose:
                _print_solution("algorithm_entropy")
                log_slight_risk("%s: The 'algorithm' and 'entropy' attributes are not"
                                " supported and have been removed automatically." % fname)
            _set_exit_func(exit_fail)
            changedAlg = True

    return changedAlg or changedSec

def check_session_cookie_connector(fname, verbose=True):
    changed = False
    if etreeDict[fname] is None:
        return changed # nothing to do
    root = etreeDict[fname].getroot()
    for elem in root.iter("Connector"):
        if "emptySessionPath" not in elem.attrib:
            continue
        # remove deprecated attributes
        elem.attrib.pop("emptySessionPath")
        if not changed and verbose:
            _print_solution("emptySessionPath")
            log_medium_risk("%s: The 'emptySessionPath' attribute is not supported in the new tomcat package"
                            " and will be removed automatically. See the remediation"
                            " instructions." % fname)
        _set_exit_func(exit_fail)
        changed = True
    return changed

def check_url_rewriting(fname, verbose=True):
    changed = False
    if etreeDict[fname] is None:
        return changed # nothing to do
    root = etreeDict[fname].getroot()
    for context in root.iter("Context"):
        if "disableURLRewriting" in context.attrib:
            context.attrib.pop("disableURLRewriting")
            if not changed and verbose:
                _print_solution("disableURLRewriting")
                log_medium_risk("%s: The 'disableURLRewriting' attribute is not supported"
                                " and will be removed automatically." % fname)
                _set_exit_func(exit_fail)
            changed = True
    return changed

def check_jsp_compiler(fname, verbose=True):
    changed = False
    if etreeDict[fname] is None:
        return changed # nothing to do
    root = etreeDict[fname].getroot()
    for servlet in root.iter_ignore_ns("servlet"):
        for elem in servlet.iter_ignore_ns("param-name"):
            if elem.text == "genStrAsCharArray":
                elem.text = "getStringAsCharArray"
                if not changed and verbose:
                    _print_solution("genStrAsCharArray")
                    log_info("%s: The 'genStrAsCharArray' attribute has been renamed"
                         " to 'genStringAsCharArray'. It will be corrected automatically." % fname)
                    _set_exit_func(exit_fixed)
            changed = True
    return changed

# Functions below are relevant only for check - no modification is planned
# so verbose parameter is omitted
def check_regex_attr(fname):

    def iter_params(element):
        for servlet in element.iter_ignore_ns("servlet"):
            for elem in servlet.iter_ignore_ns("init-param"):
                paramname = None
                paramval = None
                for elem2 in elem.iter():
                    if elem2 is Comment:
                        continue
                    if elem2.tag == "param-name":
                        paramname = elem2.text
                    elif elem2.tag == "param-value":
                        paramval = elem2.text
                yield paramname, paramval

    def my_log_attr(fname, attr):
        log_high_risk("%s: The %s attribute is in use and it uses"
                      " the tomcat6 regex list instead of a single expression."
                      " See the remediation instructions." % (fname,attr))

    # return True when separator [;,] is included in string x
    has_sep = lambda x: ',' in x or ';' in x
    ##
    #{ <key-in-solutionTexts> : (<tag-name>, <True/False>, [<attrib>, ...] ) }
    # True/False => Can be used as parameter in servlet?
    ##
    #NOTE: for all those attributes are planned just checks and have similar description
    #      maybe solution text for all of them can be same, but for now it's split
    ##
    #TODO: Verify correctness of use on all currently applied files, to be sure
    #      that exception isn't needed, tags are correct (and occurence in different tag
    #      isn't possible/expected) ... IOW: review and another tests expected
    #TODO: check True/False for servlets - now used according to original pseudocode
    ##
    attrDict = { "attr_allow_deny" : ("Valve", True, ["allow","deny"]),
                 "attr_proxies" : ("Valve", True, ["internalProxies", "trustedProxies"]),
                 "attr_user_agents" : ("Connector", False,
                                       ["restrictedUserAgents", "noCompressionUserAgents"]),
                 "attr_filter"  : ("Valve", False, ["filter"])
        }
    found_attr = list()
    if etreeDict[fname] is None:
        return False
    root = etreeDict[fname].getroot()
    for key,item in attrDict.iteritems():
        for elem in root.iter(item[0]):
            for attr in item[2]:
                if attr in found_attr:
                    continue
                data = elem.get(attr, None)
                if data and has_sep(data):
                    _print_solution(key)
                    my_log_attr(fname,attr)
                    found_attr.append(attr)

        # can be used as param in servlet?
        if item[1] is False:
            continue
        # check servlet
        for name, val in iter_params(root):
            if name in found_attr:
                continue # already found in this file, so not another action needed
            if name in item[2] and has_sep(val):
                    #NOTE: I know that "attribute" in text is relishing in this case,
                    #      but still it's understandable information
                    _print_solution(key)
                    my_log_attr(fname,attr)
                    found_attr.append(attr)


    # found at least something?
    if len(found_attr) > 0:
        _set_exit_func(exit_fail)
        return False
    return True

def check_copyXML(fname):
    if etreeDict[fname] is None:
        return False # nothing to do
    root = etreeDict[fname].getroot()
    #FIXME: is this check needed? It seems that this should inform user
    #       always or it could be based on different condition
    #FIXME: -> probably ever fname parameter is useless in that case
    for tag in ["Host", "Context"]: # make Host sense?
        for elem in root.iter_ignore_ns(tag):
            if elem.get("copyXML", None):
                _print_solution("copyXML")
                log_medium_risk("%s: XML context descriptors are no longer copied from deployed"
                                " WAR files and directories to the host's xmlBase by default." % fname)
                _set_exit_func(exit_fail)
                return True
    return False


##############################################################################
##### MAIN #####
##############################################################################
appWebXmlList = [fn for fn in get_file_list(APP_WEB_HOME) if fn.endswith("/WEB-INF/web.xml")]
appContextXmlList = [fn for fn in get_file_list(APP_WEB_HOME) if fn.endswith("/META-INF/context.xml")]

_print_solution("info_paths_changed")
_set_exit_func(exit_fixed)


# parse all relevant XML files
for fname in appWebXmlList + appContextXmlList + [
             GLOBAL_CONFIG_FILE,
             GLOBAL_WEB_XML,
             GLOBAL_CONTEXT_XML,
             GLOBAL_USER_XML
             ]:
    if not os.path.isfile(fname):
        # code below will be more readable
        # "None" value will be checked by check functions
        etreeDict[fname] = None
        continue
    tree = NoET.NoisyElementTree()
    tree.parse(
        fname,
        parser=NoET.CommentTreeBuilder(
            target=ET.TreeBuilder(element_factory=NoET.NSElement))
        )
    etreeDict[fname] = tree

# check if tomcat from EPEL (or different source) is installed
if is_pkg_installed("tomcat"):
    log_high_risk("A tomcat package from EPEL or a third party is installed."
                  " Remove it before the in-place upgrade.")
    _print_solution("tomcatEPEL")
    _set_exit_func(exit_fail)
    # add pre-upgrade check script, which check, that package was really
    # removed before upgrade - otherwise user can't continue in upgrade.
    copy2(PRE_UPGRADE_SCRIPT, PRE_UPGRADE_DIR)
    os.chmod(os.path.join(PRE_UPGRADE_DIR, PRE_UPGRADE_SCRIPT), 0775)


##### now apply all checks implemented above #####
# Check manager|admin application
#NOTE: according to my information just this file
check_users(GLOBAL_USER_XML)

# Check emptySessionPath
check_session_cookie_connector(GLOBAL_CONFIG_FILE)

# Check copyXML
check_copyXML(GLOBAL_CONFIG_FILE)
check_copyXML(GLOBAL_CONTEXT_XML)

for fname in appContextXmlList + [GLOBAL_CONFIG_FILE, GLOBAL_CONTEXT_XML]:
    check_session_manager(fname)
    check_url_rewriting(fname)
    check_jsp_compiler(fname)

#NOTE: some attributes checked in check_regex_attr should be just inside
#      GLOBAL_CONFIG_FILE; but check elsewhere it could be OK because of bound
#      to specific tags
for fname in [GLOBAL_CONFIG_FILE] + appWebXmlList + appContextXmlList:
    check_regex_attr(fname)


try:
    print_info_diff()

    # prepare data & environment for post-upgrade script
    os.makedirs(os.path.join(PRIVATE_POST_DIR, "xml"), 0755)
    write_lines(os.path.join(PRIVATE_POST_DIR, POST_PACKAGES),
                get_obsoleted_tomcatList())
    copy2("xml/NoisyElementTree.py", os.path.join(PRIVATE_POST_DIR, "xml"))
    copy2("xml/ElementTree.py", os.path.join(PRIVATE_POST_DIR, "xml"))
    copy2("xml/__init__.py", os.path.join(PRIVATE_POST_DIR, "xml"))
    copy2(POST_SCRIPT, PRIVATE_POST_DIR)
    os.chmod(os.path.join(PRIVATE_POST_DIR, POST_SCRIPT), 0755)
except IOError as e:
    log_error("IOError: %s %s" % (e.strerror, e.filename))
    _set_exit_func(exit_error)
except Exception as e:
    if getattr(e, "strerror", None):
        log_error("Unknown error: %s" % e.strerror)
    else:
        log_error("Unknown error: %s" % a)
    _set_exit_func(exit_error)

_use_exit_func()

