#!/usr/bin/python
# -*- Mode: Python; python-indent: 8; indent-tabs-mode: t -*-
"""
"""

import sys
import os
import re
from collections import namedtuple
from preupg.script_api import *

check_applies_to (check_applies="bind")
check_rpm_to (check_rpm="", check_bin="python")

#END GENERATED SECTION
# exit functions are exit_{pass,not_applicable, fixed, fail, etc.}
# logging functions are log_{error, warning, info, etc.}
# for logging in-place risk use functions log_{extreme, high, medium, slight}_risk
ConfFile = namedtuple("ConfFile", ["path", "buffer"])
CONFIG_FILE = "/etc/named.conf"
FILES_TO_CHECK = []

FIXED_CONFIGS = {}

# Exit codes
EXIT_NOT_APPLICABLE = 0
EXIT_PASS = 1
EXIT_INFORMATIONAL = 2
EXIT_FIXED = 3
EXIT_FAIL = 4
EXIT_ERROR = 5


class SolutionText(object):
    """
    A class for handling the construction of the solution text
    """
    def __init__(self):
        self.header = """Some issues have been found in your BIND9 configuration.
Use the following solutions to fix them:"""
        self.tail = """For more information, see the BIND9 Administrator Reference
Manual located in the /usr/share/doc/bind-9.9.4/Bv9ARM.pdf file, and in the 'DNS Servers'
section of the Red Hat Enterprise Linux 7 Networking Guide."""
        self.solutions = []

    def add_solution(self, solution=""):
        if solution:
            self.solutions.append(solution)

    def get_text(self):
        text = self.header + "\n\n\n"
        for solution in self.solutions:
            text += solution + "\n\n\n"
        text += self.tail
        return text


# object used for creating solution text
sol_text = SolutionText()

###########################################################
### function for parsing of config files
###########################################################
def is_comment_start(istr, index=0):
    if istr[index] == "#" or (
            index+1 < len(istr) and istr[index:index+2] in ["//", "/*"]):
        return True
    return False

def find_end_of_comment(istr, index=0):
    """
    Returns index where the comment ends.

    :param istr: input string
    :param index: begin search from the index; from the start by default

    Support usual comments till the end of line (//, #) and block comment
    like (/* comment */). In case that index is outside of the string or end
    of the comment is not found, return -1.

    In case of block comment, returned index is position of slash after star.
    """
    length = len(istr)

    if index >= length or index < 0:
        return -1

    if istr[index] == "#" or istr[index:].startswith("//"):
        return istr.find("\n", index)

    if index+2 < length and istr[index:index+2] == "/*":
        res = istr.find("*/", index+2)
        if res != -1:
            return res + 1

    return -1

def is_opening_char(c):
     return c in "\"'{(["

def find_next_token(istr,index=0, end_index=-1):
    """
    Return index of another interesting token or -1 when there is not next.

    :param istr: input string
    :param index: begin search from the index; from the start by default
    :param end_index: stop searching at the end_index or end of the string

    In case that initial index contains already some token, skip to another.
    But when searching starts on whitespace or beginning of the comment,
    choose the first one.

    The function would be confusing in case of brackets, but content between
    brackets is not evaulated as new tokens.
    E.g.:

    "find { me };"      : 5
    " me"               : 1
    "find /* me */ me " : 13
    "/* me */ me"       : 9
    "me;"               : 2
    "{ me }; me"        : 6
    "{ me }  me"        : 8
    "me }  me"          : 3
    "}} me"             : 1
    "me"                : -1
    "{ me } "           : -1
    """
    length = len(istr)
    if length < end_index or end_index < 0:
        end_index = length

    if index >= end_index or index < 0:
        return -1

    #skip to the end of the current token
    if is_opening_char(istr[index]):
        index2 = find_closing_char(istr, index)
        if index2 == -1:
            return -1
        index = index2 +1;
    elif is_comment_start(istr, index):
        index2 = find_end_of_comment(istr, index)
        if index2 == -1:
            return -1
        index = index2 +1
    elif istr[index] not in "\n\t ;})]":
        # so we have to skip to the end of the current token
        index += 1
        while index < end_index:
            if (istr[index] in "\n\t ;})]"
                    or is_comment_start(istr, index)
                    or is_opening_char(istr[index])):
                break
            index += 1
    elif istr[index] in ";)]}":
        index += 1

    # find next token (can be already under the current index)
    while index < end_index:
        if is_comment_start(istr, index):
            index = find_end_of_comment(istr, index)
            if index == -1:
                break
        elif is_opening_char(istr[index]) or istr[index] not in "\t\n ":
            return index
        index += 1
    return -1


def find_closing_char(istr, index=0):
    """
    Returns index of equivalent closing character.

    :param istr: input string

    It's similar to the "find" method that returns index of the first character
    of the searched character or -1. But in this function the corresponding
    closing character is looked up, ignoring characters inside strings
    and comments. E.g. for
        "(hello (world) /* ) */ ), he would say"
    index of the third ")" is returned.
    """
    important_chars = { #TODO: should be that rather global var?
        "{" : "}",
        "(" : ")",
        "[" : "]",
        "\"" : "\"",
        }
    length = len(istr)

    if length < 2:
        return -1

    if index >= length or index < 0:
        return -1

    closing_char = important_chars.get(istr[index], None)
    if closing_char is None:
        return -1

    isString = istr[index] in "\""
    index += 1
    curr_c = ""
    while index < length:
        curr_c = istr[index]
        if is_comment_start(istr, index) and not isString:
            index = find_end_of_comment(istr, index)
            if index == -1:
                return -1
        elif not isString and is_opening_char(curr_c):
            deep_close = find_closing_char(istr[index:])
            if deep_close == -1:
                break
            index += deep_close
        elif curr_c == closing_char:
            return index
        index += 1

    return -1

def remove_comments(istr):
    """
    Removes all comments from the given string.

    :param istr: input string
    :return: return
    """

    isCommented = False
    isBlockComment = False
    str_open = "\""
    ostr = ""

    length = len(istr)
    index = 0

    while index < length:
        if is_comment_start(istr, index):
            index = find_end_of_comment(istr,index)
            if index == -1:
                # comment till EOF
                break
            if istr[index] == "\n":
                ostr += "\n"
        elif istr[index] in str_open:
            end_str = find_closing_char(istr, index)
            if end_str == -1:
                ostr += istr[index:]
                break
            ostr += istr[index:end_str+1]
            index = end_str
        else:
            ostr += istr[index]
        index += 1

    return ostr

def find_key(istr, key, index=0, end_index=-1):
    """
    Return index of the key or -1.

    :param istr: input string; it could be whole file or content of a section
    :param key: name of the searched key in the current scope
    :param index: start searching from the index
    :param end_index: stop searching at the end_index or end of the string

    Funtion is not recursive. Searched key has to be in the current scope.
    Attention:

    In case that input string contains data outside of section by mistake,
    the closing character is ignored and the key outside of scope could be
    found. Example of such wrong input could be:
          key1 "val"
          key2 { key-ignored "val-ignored" };
        };
        controls { ... };
    In this case, the key "controls" is outside of original scope. But for this
    cases you can set end_index to value, where searching should end. In case
    you set end_index higher then length of the string, end_index will be
    automatically corrected to the end of the input string.
    """
    length = len(istr)
    keylen = len(key)
    notFirstKey = False

    if length < end_index or end_index < 0:
        end_index = length

    if index >= end_index or index < 0:
        return -1

    while index != -1:
        if istr.startswith(key, index):
            if index+keylen < end_index and istr[index+keylen] in "\n\t {;":
                # key has been found
                return index

        while notFirstKey and index != -1 and istr[index] != ";":
            index = find_next_token(istr, index)
        index = find_next_token(istr, index)

    return -1

def find_val_bounds_of_key(config, key, index=0, end_index=-1):
    """
    Return indexes of beginning and end of the value of the key.

    Otherwise return pair -1, -1.
    """
    index = find_next_token(config, find_key(config, key, index, end_index))
    if index == -1 or config[index] not in "\"{":
        return -1, -1
    close_index = find_closing_char(config, index)
    if close_index == -1 or (close_index > end_index and end_index > 0):
        return -1, -1
    return index, close_index

#######################################################
### CONFIGURATION CHECKS PART - BEGIN
#######################################################


CONFIG_CHECKS = []


def register_check(check):
    """
    Function decorator that adds configuration check into a list of checks.
    """
    CONFIG_CHECKS.append(check)
    return check


def run_checks(files_to_check):
    """
    Runs all available checks on the files loaded into the files_to_check list.
    """
    gl_result = EXIT_PASS

    for check in CONFIG_CHECKS:
        log_info("Running check: \"" + check.__name__ + "\"")
        for fpath, buff in FILES_TO_CHECK:
            log_info("checking: \"" + fpath + "\"")
            result = check(fpath, buff)
            if result > gl_result:
                gl_result = result

    log_info("Running check: \"check_empty_zones_complex\"")
    result = check_empty_zones_complex()
    if result > gl_result:
        gl_result = result

    log_info("Running check: \"check_default_runtime_dir\"")
    result = check_default_runtime_dir()
    if result > gl_result:
        gl_result = result

    return gl_result


@register_check
def check_tcp_listen_queue(file_path, buff):
    """
    3581.	[bug]		Changed the tcp-listen-queue default to 10. [RT #33029]

    The default and minimum value changed from 3 to 10.

    From bind-9.9.4 ARM:
    The listen queue depth. The default and minimum is 10. If the kernel supports the
    accept filter 'dataready' this also controls how many TCP connections that will be queued in
    kernel space waiting for some data before being passed to accept. Nonzero values less than 10
    will be silently raised. A value of 0 may also be used; on most platforms this sets the listen queue
    length to a system-defined default value.
    """
    pattern = re.compile("tcp-listen-queue\s*([0-9]+)\s*;")
    match_iter = pattern.finditer(buff)
    status = EXIT_PASS

    for match in match_iter:
        try:
            number = int(match.group(1))
        except ValueError:
            log_error("Value \"" + match.group(1) + "\" cannot be converted")
            return EXIT_ERROR
        # the new default and minimum value is "10"
        if number > 0 and number < 10:
            log_slight_risk("Found \"" + match.group(0) + "\" in \"" +
                            file_path + "\"")
            sol_text.add_solution(
"""The tcp-listen-queue statement with a value less than 10:
The value specified in the tcp-listen-queue statement is less than 10.
Change your configuration to use at least the value of 10. BIND9
will silently ignore values less than 10, and use 10 instead.""")
            status = EXIT_INFORMATIONAL

    return status


@register_check
def check_zone_statistics(file_path, buff):
    """
    3501.	[func]   zone-statistics now takes three options: full,
                    terse, and none. "yes" and "no" are retained as
                    synonyms for "full" and "terse", respectively. [RT #29165]

    The options changed, but they are still compatible, and can be used in the new version.

    From bind-9.9.4 ARM:
    If full, the server will collect statistical data on all zones (unless specifically turned off
    on a per-zone basis by specifying zone-statistics terse or zone-statistics none in the zone state-
    ment). The default is terse, providing minimal statistics on zones (including name and current
    serial number, but not query type counters).

    For the compatibility with earlier versions of BIND9, the 'zone-statistics' option can also
    accept "yes" or "no", which have the same effect as "full" and "terse", respectively.
    """
    pattern = re.compile("zone-statistics\s*(yes|no)\s*;")
    match_iter = pattern.finditer(buff)
    status = EXIT_PASS

    for match in match_iter:
        log_slight_risk("Found \"" + match.group(0) + "\" in \"" +
                        file_path + "\"")
        sol_text.add_solution(
"""The 'zone-statistics' arguments changed:
Arguments of the 'zone-statistics' option changed in the new version of BIND9.
Replace the argument 'yes' with 'full', and replace the argument 'no' with 'terse'. The original options are still recognised by BIND9, and silently converted.""")
        status = EXIT_INFORMATIONAL

    return status


@register_check
def check_masterfile_format(file_path, buff):
    """
    3180.	[func]		Local copies of slave zones are now saved in a raw
                            format by default to improve the startup performance.
                            'masterfile-format text;' can be used to override
                            the default if desired. [RT #25867]

    The default format of the saved slave zone changed from 'text' to 'raw'.

    From bind-9.9.4 ARM:
    masterfile-format specifies the file format of zone files (see Section 6.3.7). The default value is text,
    which is a standard textual representation, except for slave zones, in which the default value
    is raw. Files in other formats than text are typically expected to be generated by the named-
    compilezone tool, or dumped by named.
    """
    pattern_zone_str = "zone\s+\"(.+?)\"(\s|.)*?{(\s|.)*?}"
    pattern_slave_str = "type\s+slave"
    pattern_mff_str = "masterfile-format"
    status = EXIT_PASS

    # find slave zones without masterfile-format statement
    pattern_zone = re.compile(pattern_zone_str)
    pattern_sl_zone = re.compile(pattern_slave_str)
    pattern_mff = re.compile(pattern_mff_str)
    pattern_zone_iter = pattern_zone.finditer(buff)

    for zone in pattern_zone_iter:
        slave_statement = pattern_sl_zone.search(zone.group(0))
        # if slave zone
        if slave_statement:
            mff_statement = pattern_mff.search(zone.group(0))
            # if no masterfile-format statement
            if not mff_statement:
                log_medium_risk("Found slave zone \"" + zone.group(1) + "\" in \"" +
                                file_path + "\" without \"masterfile-format\" statement.")
                status = EXIT_FAIL

    if status == EXIT_FAIL:
        sol_text.add_solution(
"""Slave zone definition without the 'masterfile-format' statement:
In the new version of BIND9, slave zones are saved by default as a 'raw'
format after the zone transfer. Previously, the default format was 'text'.
Use one of the following solutions:
- Remove saved slave zones files so that they are saved in the 'raw'
  format when transferred next time.
- Convert zones files to the 'raw' format by using the 'named-compilezone'
  tool.
- Include the 'masterfile-format text;' statement in the slave zone
  definition statement.""")

    return status


################################################################
# These checks can not be run as the rest, as they need to check
# all configuration files at once.

def check_empty_zones_complex():
    """
    Check if there are any zones defined that are now included in empty zones.
    """
    status = EXIT_PASS

    new_ez = ["64.100.IN-ADDR.ARPA",
              "65.100.IN-ADDR.ARPA",
              "66.100.IN-ADDR.ARPA",
              "67.100.IN-ADDR.ARPA",
              "68.100.IN-ADDR.ARPA",
              "69.100.IN-ADDR.ARPA",
              "70.100.IN-ADDR.ARPA",
              "71.100.IN-ADDR.ARPA",
              "72.100.IN-ADDR.ARPA",
              "73.100.IN-ADDR.ARPA",
              "74.100.IN-ADDR.ARPA",
              "75.100.IN-ADDR.ARPA",
              "76.100.IN-ADDR.ARPA",
              "77.100.IN-ADDR.ARPA",
              "78.100.IN-ADDR.ARPA",
              "79.100.IN-ADDR.ARPA",
              "80.100.IN-ADDR.ARPA",
              "81.100.IN-ADDR.ARPA",
              "82.100.IN-ADDR.ARPA",
              "83.100.IN-ADDR.ARPA",
              "84.100.IN-ADDR.ARPA",
              "85.100.IN-ADDR.ARPA",
              "86.100.IN-ADDR.ARPA",
              "87.100.IN-ADDR.ARPA",
              "88.100.IN-ADDR.ARPA",
              "89.100.IN-ADDR.ARPA",
              "90.100.IN-ADDR.ARPA",
              "91.100.IN-ADDR.ARPA",
              "92.100.IN-ADDR.ARPA",
              "93.100.IN-ADDR.ARPA",
              "94.100.IN-ADDR.ARPA",
              "95.100.IN-ADDR.ARPA",
              "96.100.IN-ADDR.ARPA",
              "97.100.IN-ADDR.ARPA",
              "98.100.IN-ADDR.ARPA",
              "99.100.IN-ADDR.ARPA",
              "100.100.IN-ADDR.ARPA",
              "101.100.IN-ADDR.ARPA",
              "102.100.IN-ADDR.ARPA",
              "103.100.IN-ADDR.ARPA",
              "104.100.IN-ADDR.ARPA",
              "105.100.IN-ADDR.ARPA",
              "106.100.IN-ADDR.ARPA",
              "107.100.IN-ADDR.ARPA",
              "108.100.IN-ADDR.ARPA",
              "109.100.IN-ADDR.ARPA",
              "110.100.IN-ADDR.ARPA",
              "111.100.IN-ADDR.ARPA",
              "112.100.IN-ADDR.ARPA",
              "113.100.IN-ADDR.ARPA",
              "114.100.IN-ADDR.ARPA",
              "115.100.IN-ADDR.ARPA",
              "116.100.IN-ADDR.ARPA",
              "117.100.IN-ADDR.ARPA",
              "118.100.IN-ADDR.ARPA",
              "119.100.IN-ADDR.ARPA",
              "120.100.IN-ADDR.ARPA",
              "121.100.IN-ADDR.ARPA",
              "122.100.IN-ADDR.ARPA",
              "123.100.IN-ADDR.ARPA",
              "124.100.IN-ADDR.ARPA",
              "125.100.IN-ADDR.ARPA",
              "126.100.IN-ADDR.ARPA",
              "127.100.IN-ADDR.ARPA",
              ]

    # Create a global config
    configuration = ""
    for fpath, buff in FILES_TO_CHECK:
        configuration += buff + "\n"

    ez_disable_pattern = re.compile("empty-zones-enable\s+no")
    # Check if empty zones are not disabled globally
    found = ez_disable_pattern.findall(configuration)
    if found:
        return status

    # Check new empty zones
    for empty_zone in new_ez:
        pattern = re.compile("zone\s+\"" + empty_zone + "\"", re.IGNORECASE)
        pattern_dis = re.compile(
            "disable-empty-zone\s+\"" + empty_zone + "\"", re.IGNORECASE)
        found = pattern.findall(configuration)
        if found:
            # check if the empty zone is not disabled individually
            found_dis = pattern_dis.findall(configuration)
            if found_dis:
                continue
            status = EXIT_FAIL
            log_high_risk("Found a zone \"" + empty_zone + "\" in BIND9 " +
                          "configuration. This zone will be overridden by a built-in " +
                          "empty zone if not disabled.")

    if status == EXIT_FAIL:
        sol_text.add_solution(
"""Zone declaration that conflicts with built-in empty zones:
In the new version of BIND9, the list of automatically created empty
zones expanded. Your configuration contains a zone that is conflicting
with a built-in empty zone. Use one of the following solutions:
- Disable the specific empty zone by using the 'disable-empty-zone <zone>;'
  statement.
- Disable empty zones globally by using the 'empty-zones-enable no;'
  statement.""")

    return status


def check_default_runtime_dir():
    """
    Check if there are any statements needed for the /var/run -> /run move in 'options'.
    """
    status = EXIT_PASS

    # find configuration file with the options section
    config_file = config_content = ""
    opt_sec_start = opt_sec_end = -1
    for fpath, fcontent in FILES_TO_CHECK:
        opt_sec_start, opt_sec_end = find_val_bounds_of_key(fcontent, "options")
        if opt_sec_start >= 0:
            config_file = fpath
            config_content = fcontent
            break

    if -1 in [opt_sec_start, opt_sec_end]:
        # unexpected error when BIND is used and works correctly
        log_error("The options statement has not been found or the configuration "
                  "is broken.")
        return EXIT_ERROR

    for (key, val) in [("pid-file", '"/run/named/named.pid"'),
                       ("session-keyfile", '"/run/named/session.key"')]:
        val_start, val_end = find_val_bounds_of_key(config_content, key,
                opt_sec_start +1, opt_sec_end)
        if -1 in [val_start, val_end] or val != config_content[val_start:val_end+1]:
            ret = fix_statement(config_file, "options", key, val)
            if ret is False:
                log_slight_risk("The \"%s\" statement cannot be fixed in the "
                                "BIND9 configuration automatically." % key)
                status = EXIT_FAIL
            else:
                log_info("The %s statement has been set to the expected value: %s"
                         % (key, val))
                if status < EXIT_FIXED:
                    status = EXIT_FIXED
    if status == EXIT_PASS:
        return status

    sol_msg = (
"""New values of the 'pid-file' and the 'session-keyfile' statement:
The directory used by named for runtime data has been moved from the BIND
default location, which was the /var/run/named/ directory, to the /run/named/ directory.
As a result, the PID file has been moved from its default location
'/var/run/named/named.pid' to a new location '/run/named/named.pid'.
In addition, the session-key file has been moved to '/run/named/session.key'.
These locations need to be specified by statements in the options section.\n""")

    if status == EXIT_FAIL:
        sol_msg += ("To fix this, add the following statements into"
                    " the options section of your BIND9 configuration:")
    else:
        sol_msg += ("To fix this, the following statements have been set"
                    " (or added) inside the options section of your BIND9"
                    " configuration (modified files will be applied"
                    " automatically on the target system):")

    sol_msg += ("""
- 'pid-file  "/run/named/named.pid";'
- 'session-keyfile  "/run/named/session.key";'""")
    sol_text.add_solution("[FIXED] %s" % sol_msg)
    return status


#######################################################
### CONFIGURATION CHECKS PART - END
#######################################################
### CONFIGURATION fixes PART - BEGIN
#######################################################
def change_val(config, section, key, val):
    """
    Change value of key inside the section.

    val has to include all characters, including even curly brackets or quotes.
    Return modified string or None.
    """
    index = find_key(config, section)
    if index == -1:
        return None

    # find boundaries of the section
    index = find_next_token(config, index)
    if index == -1 or config[index] != "{":
        # that's really unexpected situation - maybe wrong config file
        return None
    end_index = find_closing_char(config, index)
    if end_index == -1 or config[end_index] != "}":
        # invalid config file?
        return None

    # find boundaries of value
    index, end_index = find_val_bounds_of_key(config, key, index+1, end_index)
    if -1 in [index, end_index] or config[index] != "\"":
        return None

    return config[:index] + val + config[end_index+1:]

def add_keyval(config, section, key, val):
    """
    Add key with value to the section.

    val has to include all characters, including even curly brackets or quotes.
    Return modified string or None.
    """
    index = find_key(config, section)
    if index == -1:
        return None

    # find end of the section
    index = find_next_token(config, index)
    if index == -1 or config[index] != "{":
        # that's really unexpected situation - maybe wrong config file
        return None
    index = find_closing_char(config, index)
    if index == -1 or config[index] != "}":
        # invalid config file?
        return None

    new_config = "%s\n\t%s %s;\n%s" % (config[:index], key, val, config[index:])
    return new_config

def fix_statement(confile, section, key, val):
    """Add or change statement into the section of the config file."""
    try:
        config = FIXED_CONFIGS[confile]
    except KeyError:
        with open(confile, "r") as f:
            config = f.read()

    fixed_config = change_val(config, section, key, val)
    if fixed_config is None:
        fixed_config = add_keyval(config, section, key, val)
    if fixed_config is None:
        return False

    FIXED_CONFIGS[confile] = fixed_config
    return True


#######################################################
### CONFIGURATION fixes PART - END
#######################################################

def write_fixed_configs_to_disk(result):
    """
    Writes fixed configs in the respective directories.
    """
    if result > EXIT_FIXED:
        output_dir = os.path.join(VALUE_TMP_PREUPGRADE, "dirtyconf")
        sol_text.add_solution("The configuration files could not be fixed completely as there are still some issues that need a review.")
    else:
        output_dir = os.path.join(VALUE_TMP_PREUPGRADE, "cleanconf")
        sol_text.add_solution("The configuration files have been completely fixed.")

    for path, buff in FIXED_CONFIGS.iteritems():
        curr_path = os.path.join(output_dir, path[1:])

        # create dirs to make sure they exist
        try:
            os.makedirs(os.path.dirname(curr_path))
        except OSError as e:
            # if the dir already exist (errno 17), pass
            if e.errno == 17:
                pass
            else:
                raise e

        with open(curr_path, "w") as f:
            f.write(buff)
        msg = "Written the fixed config file to '" + curr_path + "'"
        log_info(msg)
        sol_text.add_solution(msg)

def is_config_changed():
    """
    Checks if the configuration files changed.
    """
    with open(VALUE_ALLCHANGED, "r") as f:
        files = f.read()
        for fpath, buff in FILES_TO_CHECK:
            found = re.findall(fpath, files)
            if found:
                return True
    return False


def return_with_code(code):
    if code == EXIT_FAIL:
        exit_fail()
    elif code == EXIT_FIXED:
        exit_fixed()
    elif code == EXIT_NOT_APPLICABLE:
        exit_not_applicable()
    elif code == EXIT_PASS:
        exit_pass()
    elif code == EXIT_INFORMATIONAL:
        exit_informational()
    else:
        exit_error()


def check_user(uid=0):
    """
    Checks if the effective user ID is the one passed as an argument.
    """
    if os.geteuid() != uid:
        sys.stdout.write("Needs to be root.\n")
        log_error("The script needs to be run under root.")
        exit_error()


def is_file_loaded(path=""):
    """
    Checks if the file with a given 'path' is already loaded in FILES_TO_CHECK.
    """
    for f in FILES_TO_CHECK:
        if f.path == path:
            return True
    return False


def load_included_files():
    """
    Finds the configuration files that are included in some configuration
    file, reads it, closes and adds into the FILES_TO_CHECK list.
    """
    #TODO: use parser instead of regexp
    pattern = re.compile("include\s*\"(.+?)\"\s*;")
    # find includes in all files
    for ch_file in FILES_TO_CHECK:
        includes = re.findall(pattern, ch_file.buffer)
        for include in includes:
            # don't include already loaded files -> prevent loops
            if is_file_loaded(include):
                continue
            try:
                f = open(include, 'r')
            except IOError:
                log_error("Cannot open the configuration file: \"" + include +
                          "\"" + "included by \"" + ch_file.path + "\"")
                exit_error()
            else:
                log_info("Include statement found in \"" + ch_file.path + "\": " +
                         "loading file \"" + include + "\"")
                filtered_string = remove_comments(f.read())
                f.close()
                FILES_TO_CHECK.append(ConfFile(buffer=filtered_string,
                                               path=include))


def load_main_config():
    """
    Loads main CONFIG_FILE.
    """
    try:
        f = open(CONFIG_FILE, 'r')
    except IOError:
        log_error(
            "Cannot open the configuration file: \"" + CONFIG_FILE + "\"")
        exit_error()
    else:
        log_info("Loading the configuration file: \"" + CONFIG_FILE + "\"")
        filtered_string = remove_comments(f.read())
        f.close()
        FILES_TO_CHECK.append(ConfFile(buffer=filtered_string,
                                       path=CONFIG_FILE))


def main():
    check_user()
    load_main_config()
    load_included_files()
    # need to check also paths of included files
    if not is_config_changed():
        return_with_code(EXIT_PASS)
    result = run_checks(FILES_TO_CHECK)
    # write the config into the respective dir
    write_fixed_configs_to_disk(result)
    # if there was some issue, write a solution text
    if result > EXIT_PASS:
        solution_file(sol_text.get_text())
    return_with_code(result)


if __name__ == "__main__":
    main()
