Mini Shell

Direktori : /usr/share/cagefs/
Upload File :
Current File : //usr/share/cagefs/cagefslib.py

# -*- coding: utf-8 -*-

# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT


from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals

import locale
from typing import AnyStr, Dict, List, Optional
from future import standard_library
standard_library.install_aliases()
from builtins import *
import errno
import re
import sys
import shutil
import glob
import subprocess
import inspect
import pickle
import configparser
import time
import filecmp
import syslog
import traceback
import io
import datetime
import stat
import os
import functools

# provides functions for secure filesystem and I/O operations
from secureio import read_file_secure, write_file_secure, set_user_perm, open_file_not_symlink, set_root_perm
from secureio import create_dir_secure, closefd, set_owner_dir_secure, set_perm_dir_secure
from secureio import root_flag, print_error, get_groups, clpwd, SILENT_FLAG, logging, get_perm

# Provides functions to detect different control panels
import cldetectlib

from clcagefslib.const import CL_ALT_NAME, ETC_CL_ALT_PATH, BASEDIR
from clcagefslib.fs import get_linksafe_gid, get_user_prefix
from clcagefslib.io import make_userdir, read_file, read_file_cached
from clcagefslib.selector.configure import is_ea4_enabled
from clcagefslib.selector.paths import get_alt_dirs
from clcommon.clfunc import byteify, unicodeify
from clcommon import ClPwd, clcaptain
from clcommon.const import Feature
from clcommon.cpapi import is_panel_feature_supported
import phpinivalidator
from signals_handlers import sigterm_check
from clcommon.utils import (
    ExternalProgramFailed,
    is_socket_file,
    mod_makedirs,
)
from cldetectlib import get_boolean_param, CL_CONFIG_FILE
from logs import logger


class CageFSException(Exception):
    def __init__(self, *args, **kwargs):
        Exception.__init__(self, *args, **kwargs)


# This variables are set by cagefsctl module

ETC_VERSION_NAME = ".etc.version"
ETC_VERSION = "/" + ETC_VERSION_NAME

# CageFS global settings & values
CAGEFS_INI = '/etc/cagefs/cagefs.ini'
PHP_CONF = '/etc/cl.selector/php.conf'

ETC_TEMPLATE_DIR = "/usr/share/cagefs"
ETC_TEMPLATE_NEW_DIR = "/usr/share/cagefs/etc.new"

VAR_RUN_CAGEFS = '/var/run/cagefs'

VERBOSE_FLAG = 0

# Validate PHP options or not
validate_alt_php_ini = False

PHP_OPTIONS_LOGFILE = "/var/log/cagefs-php-opt-check.log"
php_log_opt = None

GLOBAL_PLESK_CFG = '/etc/psa/psa.conf'
FALLBACK_PLESK_VHOSTS_D = '/var/www/vhosts'

SYSTEMD_JOURNAL_SOCKET = '/run/systemd/journal/dev-log'
LOG_SOCKET = '/usr/share/cagefs-skeleton/dev/log'
DEV_LOG_SOCKET = '/dev/log'

SYSCONFIG_SYSLOG = "/etc/sysconfig/syslog"
CAGEFS_SOCKET = " -a " + LOG_SOCKET
RSYSLOG_CONF = '/etc/rsyslog.conf'
CHROOT_CONF = '/etc/rsyslog.d/cagefs-syslog-socket.conf'
CHROOT_OLD_CONF = '/etc/rsyslog.d/schroot.conf'

def touch(fname):
    """
    /bin/touch analog - update timestamp of a file if it exists
    or create a file otherwise
    :param fname: file path
    :type fname: string
    """
    try:
        os.utime(fname, None)
    except OSError:
        open(fname, 'a').close()


def _add_syslog_socket_for_syslog_pkg() -> None:
    """
    Add syslog socket into CageFS, add it to syslog config and restart
    syslog service
    """
    def _insert(original, new, pos):
        """
        Inserts new inside original at pos.
        """
        return original[:pos] + new + original[pos:]

    lines = read_file(SYSCONFIG_SYSLOG)
    for i, _ in enumerate(lines):
        if lines[i].startswith("SYSLOGD_OPTIONS"):
            # CageFS socket is not added yet ?
            if lines[i].find(CAGEFS_SOCKET) == -1:
                if lines[i][-2] == '"' or lines[i][-2] == "'":
                    tmp = _insert(lines[i], CAGEFS_SOCKET, -2)
                    lines[i] = tmp
                else:
                    tmp = _insert(lines[i], CAGEFS_SOCKET, -1)
                    lines[i] = tmp
            break

    write_file(SYSCONFIG_SYSLOG, lines, make_backup=True)
    ExecuteSimple('/sbin/service syslog restart &> /dev/null')


def _add_syslog_socket_for_rsyslog_pkg() -> None:
    """
    Add syslog socket into CageFS, add it to rsyslog config and restart
    rsyslog service
    """
    chroot_conf_content = f'$AddUnixListenSocket {LOG_SOCKET}\n'
    lines = read_file(RSYSLOG_CONF)
    for i, _ in enumerate(lines):
        pos = lines[i].find('$ModLoad imuxsock')
        if pos not in (-1, 0):
            # enable module `imuxsock` if it was disabled
            lines[i] = lines[i][pos:]
            break

    # if exists old rsyslog config file then unlink it
    if os.path.isfile(CHROOT_OLD_CONF):
        with open(CHROOT_OLD_CONF, "r") as f:
            old_content = f.read()
        if old_content == chroot_conf_content:
            os.unlink(CHROOT_OLD_CONF)

    write_file(
        RSYSLOG_CONF,
        lines,
        make_backup=True,
    )
    write_file(
        CHROOT_CONF,
        [chroot_conf_content],
        make_backup=True,
    )
    ExecuteSimple('/sbin/service rsyslog restart &> /dev/null')


def add_syslog_socket() -> None:
    """
    Add cagefs skeleton syslog socket to syslog config file.
    Create .conf file for rsyslog
    Restart syslog/rsyslog service
    """
    if is_new_syslog_socket_used():
        if is_old_syslog_socket_in_cage():
            # We should disable using of older version of socket in CageFS
            # if server has systemd-journal package which uses socket by path
            # SYSTEMD_JOURNAL_SOCKET.
            # The newer version of socket is mounted in function
            # cagefsctl._mount_systemd_journal_socket which is used at moment
            # mounting of CageFS skeleton
            # see for details: CAG-1062: Mount socket of systemd-journal into CageFS
            remove_syslog_socket()
            # touch /usr/share/cagefs/need.remount for future remounting already
            # mounted users because we want to provide changes to them
            touch('/usr/share/cagefs/need.remount')
    elif os.path.isfile(SYSCONFIG_SYSLOG):
        _add_syslog_socket_for_syslog_pkg()
    elif os.path.isfile(RSYSLOG_CONF):
        _add_syslog_socket_for_rsyslog_pkg()


def remove_syslog_socket():
    """
    Remove syslog socket info for cagefs from system syslog configs
    Restart syslog/rsyslog service
    """
    if os.path.isfile(SYSCONFIG_SYSLOG):
        lines = read_file(SYSCONFIG_SYSLOG)

        for i, _ in enumerate(lines):
            if lines[i].startswith("SYSLOGD_OPTIONS"):
                tmp = lines[i].replace(CAGEFS_SOCKET, "")
                lines[i] = tmp
                break

        write_file(
            SYSCONFIG_SYSLOG,
            lines,
            make_backup=True,
        )
        ExecuteSimple('/sbin/service syslog restart &> /dev/null')

    if os.path.isfile(CHROOT_CONF):
        try:
            os.unlink(CHROOT_CONF)
        except OSError as e:
            print_error('removing', CHROOT_CONF, ':', str(e))

        ExecuteSimple('/sbin/service rsyslog restart &> /dev/null')


def is_new_syslog_socket_used() -> bool:
    """
    File `/dev/log` is symlink to socket `/run/systemd/journal/dev-log` if
    server uses the newer version of syslog socket
    """
    return os.path.islink(DEV_LOG_SOCKET) and \
        os.path.realpath(DEV_LOG_SOCKET) == SYSTEMD_JOURNAL_SOCKET and \
        is_socket_file(SYSTEMD_JOURNAL_SOCKET)


def is_old_syslog_socket_in_cage() -> bool:
    """
    Return True if CageFS has into self an old syslog socket
    """
    return is_socket_file(LOG_SOCKET)


def getItem(txt1, txt2, op):
    try:
        i1 = int(txt1)
    except ValueError:
        i1 = -1
    try:
        i2 = int(txt2)
    except ValueError:
        i2 = -1
    if i1 == -1 or i2 == -1:
        if op == 0:
            return txt1 > txt2
        else:
            return txt1 < txt2
    else:
        if op == 0:
            return i1 > i2
        else:
            return i1 < i2

# Compare version of types xx.xx.xxx... and yyy.yy.yy.y..
# if xxx and yyy is numbers, than comapre as numbers
# else - compare as strings
def verCompare(base, test):
    base = base.split(".")
    test = test.split(".")
    if (len(base) > len(test)):
        ln = len(test)
    else:
        ln = len(base)
    for i in range(ln):
        if getItem(base[i], test[i], 0):
            return 1
        if getItem(base[i], test[i], 1):
            return -1
    if len(base) == len(test):
        return 0
    elif len(base) > len(test):
        return 1
    else:
        return -1

def unlink(path):
    try:
        os.unlink(path)
    except OSError:
        pass


# Write message to php options log file
def php_options_log_write(msg, unknown_options_list, invalid_values_options_list, invalid_options_list):
    global php_log_opt
    root_flag_saved = root_flag
    if not root_flag:
        uid, gid = get_perm()
        set_root_perm()
    try:
        if php_log_opt is None:
            umask_saved = os.umask(0o77)
            # log_file is opened in "line buffered" mode
            php_log_opt = open(PHP_OPTIONS_LOGFILE, 'a', 1)
            os.umask(umask_saved)
        php_log_opt.write(datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S") + ": " + msg + "\n")
        if unknown_options_list:
            php_log_opt.write(" - The following options have been disabled as unknown:\n")
            for option in unknown_options_list:
                php_log_opt.write("     * " + option + "\n")
        if invalid_values_options_list:
            php_log_opt.write(" - The following options have been disabled as have incorrect values:\n")
            for option in invalid_values_options_list:
                php_log_opt.write("     * " + option + "\n")
        if invalid_options_list:
            php_log_opt.write(" - The following options have been disabled as invalid (have no values):\n")
            for option in invalid_options_list:
                php_log_opt.write("     * " + option + "\n")
    except (OSError, IOError) as e:
        print_error("writing to ", PHP_OPTIONS_LOGFILE, str(e))
        sys.exit(1)
    if not root_flag_saved:
        set_user_perm(uid, gid)


def print_exception(level = syslog.LOG_ERR, includetraceback = False):
    exctype, exception, exctraceback = sys.exc_info()
    excclass = str(exception.__class__)
    message = str(exception)

    if not includetraceback:
        msg = "%s: %s" % (excclass, message)
        msg = msg.replace('Errno', 'Err code')
        syslog.syslog(level, msg)
        print(msg, file=sys.stderr)
    else:
        try:
            import StringIO
        except ImportError:
            # for python 3
            excfd = io.StringIO()
        else:
            # for python 2
            excfd = StringIO.StringIO()
        traceback.print_exception(exctype, exception, exctraceback, None, excfd)
        for line in excfd.getvalue().split("\n"):
            syslog.syslog(level, line)
            print(line, file=sys.stderr)


def _read_vhosts_dir():
    try:
        with open(GLOBAL_PLESK_CFG, 'rt') as f:
            data = f.read()
    except Exception:
        return None             # e.g. case when Plesk is not installed
    match = re.search(r'^HTTPD_VHOSTS_D[ \t]+(\S+)$', data, re.MULTILINE)
    if not match:
        return None
    return match.groups()[0].rstrip("/")

# CAG-673
PLESK_VHOSTS_D = _read_vhosts_dir() or FALLBACK_PLESK_VHOSTS_D


# Black list of files and directories (that should not exist in skeleton)
black_list = []


def is_in_black_list(_file):
    rfile = strip_path(_file)
    rfile = addslash(rfile)
    for path in black_list:
        path = addslash(path)
        if rfile.startswith(path):
            return True
    return False


def lineno():
    """Returns the current line number in program"""
    return inspect.currentframe().f_back.f_lineno


def stripslash(_dir):
    if _dir != '':
        if (_dir[-1] == '/'):
            return _dir[:-1]
    return _dir


def addslash(_dir):
    if _dir == '':
        return '/'
    if (_dir[-1] != '/'):
        return '%s/' % (_dir,)
    return _dir


# Turns on/off additional checks and debug output
# This variable is set by cagefsctl module
debug_option = False

# Path to skeleton set by cagefsctl module
SKELETON = ''

# Pathes to white and safe lists used by cagefs-fuse
# This variables are set by cagefsctl module
FUSE_WHITE_LIST = ''
FUSE_SAFE_LIST = ''

# Save (overwrite) safe list for cagefs-fuse
def save_etc_safe_list(safe_list):
    sigterm_check()
    umask_saved = os.umask(0o22)
    _file = open(FUSE_SAFE_LIST, 'w')
    for filename in safe_list:
        _file.write('%s\n' % filename)
    _file.close()
    os.umask(umask_saved)
    os.chmod(FUSE_SAFE_LIST, 0o600)



def strip_path(path: str) -> str:
    """
    Remove leading path to skeleton from the specified path.
    """
    path = path.removeprefix(SKELETON)
    return path or '/'



# White list of files in system's /etc directory
white_list = {}


# Function adds directory tree rooted in src to list
def add_tree_to_list(src, _list, follow_symlinks=False, cut_path=None, add_path=None):
    for name in os.listdir(src):
        srcname = os.path.join(src, name)
        path = srcname
        if cut_path != None:
            path = path[len(cut_path):]
        if add_path != None:
            path = add_path + path
        _list[path] = 1
        if os.path.isdir(srcname) and (follow_symlinks or (not os.path.islink(srcname))):
            add_tree_to_list(srcname, _list, cut_path=cut_path, add_path=add_path)



def get_real_path(path):
    return os.path.join(os.path.realpath(os.path.dirname(path)), os.path.basename(path))



def copy2etc(path: str) -> None:
    if not path.startswith('/etc/'):
        return

    if move_to_alternatives(path, etc=True):
        return

    if path.startswith('/etc/cl.php.d/') or path.startswith('/etc/cl.selector/'):
        return

    if path in ['/etc/passwd', '/etc/group', '/etc/shadow', '/etc/cl.php.d', '/etc/cl.selector']:
        return

    if not os.path.exists(path):
        return

    destination = ETC_TEMPLATE_NEW_DIR + path
    if os.path.isfile(path) or (os.path.islink(path)
                                and is_path_read_only_mounted(os.path.realpath(path))):
        copy_file(path, destination)
    else:
        copytree(path, destination, True)


# Returns True if path refers to object in /etc directory
# and adds to white_list directory tree rooted in path
# Also copies path to the template of /etc directory
def add_to_white_list(path):
    global white_list
    path = stripslash(path)
    # Strip path to skeleton from path
    path = strip_path(path)
    # path = get_real_path(path)
    copy2etc(path)
    if path.startswith('/etc/'):
        if (path not in white_list) and os.path.exists(path):
            white_list[path] = 1
            if os.path.isdir(path):
                add_tree_to_list(path, white_list, True)
        return True
    return False




# Function copies paths (list of paths) to the template of /etc directory
def copy_to_etc(paths):
    for path in paths:
        path = stripslash(path)
        # Strip path to skeleton from path
        path = strip_path(path)
        copy2etc(path)
        path2 = get_real_path(path)
        if path2 != path:
            copy2etc(path2)
        path3 = os.path.realpath(path)
        if path3 != path and path3 != path2:
            copy2etc(path3)
        if os.path.islink(path):
            linkto = os.readlink(path)
            copy2etc(linkto)




# lines of mp-file (set by cagefsctl module)
mounts = []


def path_includes_mount_point_comparator(path, mount):
    return mount.startswith(path)

def path_is_mounted_comparator(path, mount):
    return path.startswith(mount)

def mounts_are_found_comparator(path, mount):
    return path_includes_mount_point_comparator(path, mount) or path_is_mounted_comparator(path, mount)


# path == path to directory or file in skeleton
def mounts_are_found(path, comparator, mounts_list=None):
    if mounts_list is None:
        mounts_list = mounts
    # Strip path to skeleton from path
    path = strip_path(path)
    # path = os.path.realpath(path)
    path = addslash(path)
    if path.startswith('/etc/') or path.startswith('/var/log/'):
        return True
    for line in mounts_list:
        if line != '' and line[0] == '/':
            line = line.rstrip()
            line = addslash(line)
            if comparator(path, line):
                return True
    return False



# path == path to directory or file in skeleton
def path_is_mounted(path):
    return mounts_are_found(path, path_is_mounted_comparator)



# path == path to directory or file in skeleton
def path_includes_mount_point(path):
    return mounts_are_found(path, path_includes_mount_point_comparator)


def is_path_read_only_mounted(path: str) -> bool:
    from cagefsctl import MountpointConfig
    read_only_mounts = MountpointConfig().read_only_mounts
    return mounts_are_found(path,
                            path_is_mounted_comparator,
                            mounts_list=read_only_mounts)


# List of libraries for each binary file in cagefs-skeleton directory
# key = binary file
# value = list of libraries
libs_list = {}


def add_libs_to_list(binary, libs):
    global libs_list
    libs_list[binary] = libs


def get_libs_from_list(binary):
    try:
        return libs_list[binary]
    except KeyError:
        return None


def del_libs_from_list(binary):
    try:
        del libs_list[binary]
    except KeyError:
        pass


# Save libs_list to file
def save_libs(filename):
    sigterm_check()
    try:
        umask_saved = os.umask(0o77)
        _file = open(filename, "wb")
        pickle.dump(byteify(libs_list), _file, protocol=2)
        _file.close()
        os.umask(umask_saved)
        os.chmod(filename, 0o600)
    except Exception as err:
        print_error("while saving", filename, "-", err)


# Load libs_list from file
def load_libs(filename):
    global libs_list
    if os.path.isfile(filename):
        try:
            _file = open(filename, "rb")
            libs_list = unicodeify(pickle.load(_file, encoding=locale.getpreferredencoding()))
            _file.close()
        except Exception as err:
            print_error("loading", filename, "-", err)



# List of files and directories in cagefs-skeleton directory
files_list = {}


def path_is_in_list(path):
    path = stripslash(path)
    # Strip path to skeleton from path
    path = strip_path(path)
    path = get_real_path(path)
    return path in files_list



def check_error(path, linenum):
    if (not debug_option) or path_is_mounted(path):
        return
    # Strip path to skeleton from path
    path = strip_path(path)
    if os.path.lexists(path):
        if not path_is_in_list(path):
            print_error('Error in line', linenum, ': ', path, 'is not in list')



# Adds to list directory tree rooted in path
def add_to_list(path, add_tree = True):
    global files_list
    path = stripslash(path)
    if debug_option:
        rpath = get_real_path(path)
        if path != rpath:
            print_error('Error in line', lineno(), ': ', path, '!=', rpath)
    if os.path.lexists(path):
        # Strip path to skeleton from path
        path = strip_path(path)
        # if not path_is_mounted(path):
        if path not in files_list:
            files_list[path] = 1
            if add_tree and os.path.isdir(path) and (not os.path.islink(path)):
                add_tree_to_list(path, files_list, False)
    else:
        if debug_option:
            print_error('line', lineno(), 'path does not exist:', path)



stat_cache = {}

def cached_lstat(path, use_cache=True):
    global stat_cache
    if not use_cache:
        return os.lstat(path)
    try:
        res = stat_cache[path]
    except KeyError:
        stat_cache[path] = res = os.lstat(path)
    return res


def clear_stat_cache(path):
    try:
        del stat_cache[path]
    except KeyError:
        pass



# Returns True if files are "equal", False if not
def is_same_metadata(fileA, fileB, sbA=None, sbB=None, use_cache=True, relative_symlinks=False):
    if (sbA==None):
        sbA = cached_lstat(fileA, use_cache=use_cache)
    if (sbB==None):
        sbB = cached_lstat(fileB, use_cache=use_cache)

    if (stat.S_ISLNK(sbA[stat.ST_MODE]) != stat.S_ISLNK(sbB[stat.ST_MODE])):
        return False

    if (stat.S_ISLNK(sbA[stat.ST_MODE])):
        realfileA = os.readlink(fileA)
        realfileB = os.readlink(fileB)
        if relative_symlinks:
            relative_path = get_relative_path(realfileA, fileB)
            return realfileB == relative_path
        return realfileA == realfileB

    modeA = sbA[stat.ST_MODE]
    modeB = sbB[stat.ST_MODE]

    # clear SUID & SGID before comparing
    modeA = (modeA & ~stat.S_ISUID) & ~stat.S_ISGID
    modeB = (modeB & ~stat.S_ISUID) & ~stat.S_ISGID

    # Compare permissions and file type bits
    if modeA != modeB:
        return False

    # Comapre mtime, size, owner, group
    if (sbA[stat.ST_MTIME] != sbB[stat.ST_MTIME]) or (sbA[stat.ST_SIZE] != sbB[stat.ST_SIZE])\
                    or (sbA[stat.ST_UID] != sbB[stat.ST_UID]) or (sbA[stat.ST_GID] != sbB[stat.ST_GID]):
        return False

    return True



def is_update_needed(original, injail, origstatbuf=None, injailstatbuf=None, use_cache=True, relative_symlinks=False):
    """
    Returns: True if update of "injail" file is needed
             False if update is NOT needed (file in jail has same metadata)
    """
    try :
        return not is_same_metadata(original, injail, sbA=origstatbuf, sbB=injailstatbuf,
                                    use_cache=use_cache, relative_symlinks=relative_symlinks)
    except OSError as e:
        # file does not exist in cagefs-skeleton - update is needed
        return (e.errno == 2)


class StaticallyLinkedError(Exception):
    pass


def _parse_lib_path(line: str, executable: str) -> Optional[str]:
    if "no version information available" in line:
        return

    splitted = line.split()
    if not splitted:
        print_error('failed to parse ldd output', line[:-1])
        return

    if (splitted[0:2] == ['statically', 'linked']) \
            or (splitted[0:1] + splitted[2:4] == ['not', 'dynamic', 'executable']):
        raise StaticallyLinkedError()

    if splitted[0] in ('linux-gate.so.1', 'linux-vdso.so.1') \
            or (len(splitted) == 4 and splitted[2:4] == ['not', 'found']):
        return

    if len(splitted) >= 3:
        # the line containing dynamic linker may vary,
        # but we want it to be added to CageFS anyway
        dynamic_linker = '/lib64/ld-linux-x86-64.so.2'
        lib_path = dynamic_linker if dynamic_linker in splitted else splitted[2]
    elif len(splitted) >= 1 and splitted[0][0] == '/':
        lib_path = splitted[0]
    else:
        print_error('failed to parse ldd output', line[:-1])
        return

    if not os.path.exists(lib_path):
        print_error('ldd returns non existing library', lib_path, 'for', executable)
        return

    return lib_path


def get_ldd_libs(executable: str) -> List[str]:
    """
    Returns list of libraries for the executable
    """
    import struct
    retval = []
    try:
        # check if executable is binary
        f = open(executable, 'rb')
        # read 4 byte signature
        signature = struct.unpack('<I', f.read(4))[0]
        f.close()
    except:
        return retval

    # binary executables have signature 464C457Fh,
    # e.g. ELF (in little-endian format)
    if signature != 0x464C457F:
        return retval

    ldd_path = '/usr/bin/ldd'
    # ldd now prints warnings in stdout, they should be omitted
    # This array stores all error patterns for skip
    # Sample of line with such pattern:
    # <lib>: <other_lib>: no version information available (required by <lib>)
    # This warning doesn't produce any crucial errors, but can break parse

    # Note (gponomarenko): ldd can't load libraries from fs mounted with noexec option (e.g. /tmp)
    # When it tries then there is an error in stdout:
    # "error while loading shared libraries: <lib.so>: failed to map segment from shared object"
    p = subprocess.Popen([ldd_path, executable], shell=False,
                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE, close_fds=True, text=True)
    for line in p.stdout.readlines():
        try:
            lib_path = _parse_lib_path(line, executable)
        except StaticallyLinkedError:
            break
        if lib_path is not None:
            retval.append(lib_path)

    return retval



# os.path.realpath() seems to do the same:
# NO: it cannot handle symlink resolving WITH a chroot
def resolve_realpath(path, chroot='', include_file=0):
    if (path=='/'):
        return '/'
    spath = split_path(path)
    basename = ''
    if (not include_file):
        basename = spath[-1]
        spath = spath[:-1]
    ret = '/'
    doscounter=0# a symlink loop may otherwise hang this script
    #print 'path',path,'spath',spath
    for entry in spath:
        ret = os.path.join(ret,entry)
        #print 'lstat',ret
        sb = cached_lstat(ret)
        if (stat.S_ISLNK(sb.st_mode)):
            doscounter+=1
            realpath = os.readlink(ret)
            if (realpath[0]=='/'):
                ret = os.path.normpath(chroot+realpath)
            else:
                tmp = os.path.normpath(os.path.join(os.path.dirname(ret),realpath))
                if (len(chroot)>0 and tmp[:len(chroot)]!=chroot):
                    print_error('symlink ', tmp, ' points outside jail, ABORT')
                    raise Exception("Symlink points outside jail")
                ret = tmp
    return os.path.join(ret,basename)



# similar to shutil.copymode(src, dst) but we do not copy any SUID bits
# the caller should catch any exceptions!
def copy_time_and_permissions(src, dst, be_verbose=0, allow_suid=0, copy_ownership=0):
    sbuf = os.stat(src)
#       in python 2.1 the return value is a tuple, not an object, st_mode is field 0
#       mode = stat.S_IMODE(sbuf.st_mode)
    mode = stat.S_IMODE(sbuf[stat.ST_MODE])
    if (not allow_suid):
        if (mode & (stat.S_ISUID | stat.S_ISGID)):
            logging('removing setuid and setgid permissions from '+dst, SILENT_FLAG, be_verbose)
            mode = (mode & ~stat.S_ISUID) & ~stat.S_ISGID
    os.utime(dst, (sbuf[stat.ST_ATIME], sbuf[stat.ST_MTIME]))
    if (copy_ownership):
        os.chown(dst, sbuf[stat.ST_UID], sbuf[stat.ST_GID])
    os.chmod(dst, mode)



def split_path(path):
    spath = path.split('/')
    res = []
    for item in spath:
        if item:
            res.append(item)
    return res


def join_path(spath):
    if (len(spath)==0):
        return '/'
    ret = ''
    for entry in spath:
        ret += '/'+entry
    return ret

handled_dir = {}
def gen_path_key(path, copy_permissions, copy_ownership):
    return path + "_"+str(copy_permissions) + "_" + str(copy_ownership)

def create_parent_path(chroot,path,be_verbose=0, copy_permissions=1, allow_suid=0, copy_ownership=0):
    sigterm_check()
    # Do not create path if it is in black list
    if is_in_black_list(path):
        return chroot+path
    # check if we already processed that directory
    global handled_dir
    key = gen_path_key(path, copy_permissions, copy_ownership)
    if key in handled_dir:
        return handled_dir[key]
    # the first part of the function checks the already existing paths in the jail
    # and follows any symlinks relative to the jail
    spath = split_path(path)
    existpath = chroot
    i=0
    while (i<len(spath)):
        sigterm_check()
        origpath = join_path(spath[0:i+1])
        origkey = gen_path_key(origpath, copy_permissions, copy_ownership)
        if origkey not in handled_dir:
            tmp1 = os.path.join(existpath,spath[i])
            if not os.path.exists(tmp1):
                break
            tmp = resolve_realpath(tmp1,chroot,1)
            if not os.path.exists(tmp):
                break
            existpath = tmp
            if copy_permissions and not path_is_mounted(existpath):
                try:
                    copy_time_and_permissions(origpath, existpath, be_verbose, allow_suid, copy_ownership)
                except OSError as e:
                    print_error('failed to copy time/permissions/owner from', origpath, 'to', existpath, ':', e.strerror)
            handled_dir[origkey]=existpath
        else:
            existpath = handled_dir[origkey]
        i+=1

    # the second part of the function creates the missing parts in the jail
    # according to the original directory names, including any symlinks
    while (i<len(spath)):
        sigterm_check()
        origpath = join_path(spath[0:i+1])
        jailpath = os.path.join(existpath,spath[i])
        try:
            sb = cached_lstat(origpath)
        except OSError as e:
            print_error('failed to lstat('+origpath+'):', e.strerror)
            return None

        if (stat.S_ISDIR(sb.st_mode)):

            try:
                injailsb = cached_lstat(jailpath)
                if not stat.S_ISDIR(injailsb.st_mode):
                    clear_stat_cache(jailpath)
                    os.unlink(jailpath)
            except OSError:
                pass

            logging('Create directory '+jailpath,SILENT_FLAG,be_verbose)

            try:
                os.mkdir(jailpath, 0o755)
                add_to_list(jailpath, False)
            except OSError as e:
                logging('Warning: failed to create directory ' + jailpath + ' -- ' + e.strerror, SILENT_FLAG, be_verbose)

            if (copy_permissions):
                try:
                    copy_time_and_permissions(origpath, jailpath, be_verbose, allow_suid, copy_ownership)
                except OSError as e:
                    print_error('failed to copy time/permissions/owner from', origpath, 'to', jailpath, ':', e.strerror)
        elif (stat.S_ISLNK(sb.st_mode)):
            realfile = update_symlink_in_skeleton(origpath, jailpath)
            add_to_list(jailpath, False)
            if (realfile[0]=='/'):
                jailpath = create_parent_path(chroot, realfile, be_verbose,
                                              copy_permissions, allow_suid, copy_ownership)
                check_error(jailpath, lineno())
            else:
                tmp = os.path.normpath(os.path.join(os.path.dirname(jailpath), realfile))
                if (len(chroot)>0 and tmp[:len(chroot)]!=chroot):
                    print_error('symlink '+tmp+' points outside jail, ABORT')
                    raise Exception("Symlink points outside jail")
                realfile = tmp[len(chroot):]
                jailpath = create_parent_path(chroot, realfile,
                                              be_verbose, copy_permissions, allow_suid, copy_ownership)
                check_error(jailpath, lineno())
        existpath = jailpath
        i+=1
    add_to_list(existpath, False)
    # save directory as processed so we don't test it twice.
    handled_dir[key]=existpath
    return existpath



def copy_with_permissions(src, dst, be_verbose=0, try_hardlink=1, retain_owner=0):
    """copies/links the file and the permissions, except any setuid or setgid bits"""
    if is_in_black_list(dst):
        return

    try:
        injailsb = cached_lstat(dst)
        if stat.S_ISDIR(injailsb.st_mode):
            clear_stat_cache(dst)
            shutil.rmtree(dst)
    except (IOError, OSError, shutil.Error):
        pass

    do_normal_copy = 1
    if (try_hardlink==1):
        try:
            os.link(src,dst)
            do_normal_copy = 0
            add_to_list(dst)
        except:
            print_error('Linking '+src+' to '+dst+' failed, will revert to copying')
            pass
    if (do_normal_copy == 1):
        try:
            shutil.copyfile(src,dst)
            add_to_list(dst, add_tree = False)
            copy_time_and_permissions(src, dst, be_verbose, allow_suid=0, copy_ownership=retain_owner)
        except (IOError, OSError, shutil.Error) as e:
            print_error('ERROR: copying file and permissions ', src, ' to ', dst, ': ', e.strerror)



def copy_device(chroot, path, be_verbose=1, retain_owner=1):
    if path_is_mounted(path):
        return
    try:
        sb = os.lstat(path)
    except OSError:
        logging('Device ' + path+ ' does NOT exist in real system',SILENT_FLAG,be_verbose)
        return
    create_parent_path(chroot, os.path.dirname(path), be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=1)
    chrootpath = resolve_realpath(chroot+path,chroot)
    if (stat.S_ISCHR(sb.st_mode)):
        mode = 'c'
    elif (stat.S_ISBLK(sb.st_mode)):
        mode = 'b'
    elif (stat.S_ISLNK(sb.st_mode)):
        try:
            realfile = os.readlink(path)
            logging('Creating symlink '+chrootpath+' to '+realfile,SILENT_FLAG,be_verbose)
            remove_file_or_dir(chrootpath, check_mounts = True)
            os.symlink(realfile,chrootpath)
        except OSError:
            logging('Failed to create symlink '+chrootpath+' to '+realfile,SILENT_FLAG, 1)
        if realfile[0] != '/':
            realfile = os.path.normpath(os.path.join(os.path.dirname(path), realfile))
        if not realfile.startswith('/proc/') and os.path.exists(realfile):
            copy_device(chroot, realfile, be_verbose, retain_owner)
        return
    else:
        return
    # major = st_rdev divided by 256 (8bit reserved for the minor number)
    # minor = remainder of st_rdev divided by 256
    major, minor = divmod(sb.st_rdev, 256)
    try:
        if not os.path.lexists(chrootpath):
            logging('Creating device '+chroot+path,SILENT_FLAG,be_verbose)
            os.spawnlp(os.P_WAIT, 'mknod','mknod', chrootpath, str(mode), str(major), str(minor))
        else:
            logging('Device '+chrootpath+' does exist already',SILENT_FLAG,be_verbose)
        copy_time_and_permissions(path, chrootpath, allow_suid=0, copy_ownership=retain_owner)
    except OSError:
        logging('Failed to create device '+chrootpath,SILENT_FLAG, 1)



def copy_dir_recursive(chroot,_dir,force_overwrite=0, be_verbose=0, check_libs=1, try_hardlink=1, retain_owner=0, handledfiles=[], update=0):
    """copies a directory and the permissions recursive, except any setuid or setgid bits"""
    sigterm_check()
    if is_in_black_list(_dir):
        return handledfiles

    files2 = ()
    for entry in os.listdir(_dir):
        sigterm_check()
        tmp = os.path.join(_dir, entry)
        try:
            sbuf = cached_lstat(tmp)
            if (stat.S_ISDIR(sbuf.st_mode)):
                epath = create_parent_path(chroot, tmp, be_verbose=be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner)
                check_error(epath, lineno())
                # add_to_list(epath, False)
                handledfiles = copy_dir_recursive(chroot,tmp,force_overwrite, be_verbose, check_libs, try_hardlink, retain_owner, handledfiles, update=update)
            else:
                files2 += os.path.join(_dir, entry),
        except OSError as e:
            print_error('failed to investigate source file',tmp,':',e.strerror)
    handledfiles = copy_binaries_and_libs(chroot,files2,force_overwrite, be_verbose, check_libs, try_hardlink, retain_owner, handledfiles, update=update)
    if debug_option:
        for item in files2:
            check_error(item, lineno())
    return handledfiles




def libs_check_is_needed(_file, mode):
    return (_file.find('/lib') != -1 or _file.find('.so') != -1\
                            or (mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)))


def write_file(filename, lines, add_eol=False, make_backup=False):
    """
    Helper for write lines to file
    :param: filename `str` filename for write
    :param: lines `list` list with content lines
    :param: add_eol `bool` if True than add \n to end each line
    """
    sigterm_check()
    try:
        if make_backup:
            try:
                backup_name = "{}.bak".format(filename)
                shutil.copyfile(filename, backup_name)
                os.chmod(backup_name, stat.S_IMODE(os.lstat(filename).st_mode))
            except (IOError, OSError, shutil.Error):
                pass

        with open(filename, "w") as f:
            splitter = "" if not add_eol else "\n"
            f.write(splitter.join(lines))
    except (OSError, IOError):
        logging('Error: failed to write ' + filename, SILENT_FLAG, 1)
        sys.exit(1)


def isdigit(n):
    return n in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']


# Function returns True if string contains digits only
def isdigits(s):
    if not s:
        return False
    for char in s:
        if not isdigit(char):
            return False
    return True



def get_version(line, sign):
    length = len(sign)
    pos = line.find(sign)
    if pos != -1:
        end = line[pos+length:]
        pos2 = 0
        while pos2 < len(end):
            if not isdigit(end[pos2]):
                break
            pos2 += 1
        ver = end[:pos2]
        return int(ver)
    return 0



# command = path to wrapper inside skeleton
def update_wrapper(program, alias, command):
    # copy content of the program
    script = program[:]

    # replace "ALIAS" with alias
    for i in range(len(script)):
        tmp = script[i].replace("ALIAS", alias)
        script[i] = tmp

    if os.path.isfile(command):
        try:
            os.unlink(command)
        except (OSError, IOError):
            logging('Error: failed to delete ' + command, SILENT_FLAG, 1)
            sys.exit(1)

    umask_saved = os.umask(0o22)
    write_file(command, script)
    os.umask(umask_saved)



# Returns True if wrapper is not installed or its version is older than required
# command = path to wrapper inside skeleton
def wrapper_not_installed(command, version, sign):
    if not os.path.isfile(command):
        # wrapper is NOT installed
        return True

    GREP = "/bin/grep"

    try:
        # run the "grep" command and suppress it's output
        p = subprocess.Popen([GREP, "-m", "1", sign, command],\
                                                        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        (out, _) = p.communicate()
        # check return code of the child
        if p.returncode == 2:
            # trouble
            logging('Error while executing ' + GREP + " -m 1 " + sign + " " + command, SILENT_FLAG, 1)
            # assume that wrapper is NOT installed
            return True
        elif p.returncode != 0:
            # signature is NOT found - wrapper is NOT installed yet
            return True
    except OSError:
        logging('Error: failed to run ' + GREP + " -m 1 " + sign + " " + command, SILENT_FLAG, 1)
        # assume that wrapper is NOT installed
        return True

    if out != None:
        ver = get_version(out, sign)
        if ver >= version:
            # wrapper is installed already
            return False

    # wrapper is NOT installed
    return True



# Path to wrapper scripts (sources)
PROXY_PATH = "/usr/share/cagefs/safeprograms/"

# Signature of proxy script
SIGNATURE = "#CageFS proxyexec wrapper - ver "

# pairs "Path to wrapper" : "Alias"
wrappers = {}

# pairs "Path to wrapper" : "Name of wrapper"
wrappers_names = {}


def get_proxy_version(lines: List[str]) -> int:
    """
    Detect wrapper version from the file lines.
    If unable to detect, return -1.
    """
    for line in lines:
        version = get_version(line, SIGNATURE)
        if version != 0:
            return version
    return -1


def install_wrapper(_file):
    # Strip path to skeleton from path to file
    _file = strip_path(_file)

    if not os.path.isfile(_file):
        return

    # Read lines of proxy script
    proxy = read_file(PROXY_PATH + wrappers_names[_file])

    # Determine version of proxy script
    proxy_ver = get_proxy_version(proxy)

    # Installing if version equals -1 as it's most likely user's custom wrapper
    if proxy_ver == -1 or wrapper_not_installed(SKELETON+_file, proxy_ver, SIGNATURE):
        update_wrapper(proxy, wrappers[_file], SKELETON+_file)

    try:
        # os.chmod(command, 0755)
        copy_time_and_permissions(_file, SKELETON+_file, be_verbose=0, allow_suid=0, copy_ownership=1)
    except (OSError, IOError):
        logging('Error: failed to set permissions/owner to ' + SKELETON+_file, SILENT_FLAG, 1)
        sys.exit(1)


# Returns True if file is wrapper
def update_proxy_wrapper(_file):
    sigterm_check()
    # Strip path to skeleton from path to file
    _file = strip_path(_file)

    if _file in wrappers:
        add_to_list(_file, add_tree = False)
        install_wrapper(_file)
        return True

    return False


def remove_file_or_dir(path, check_mounts = False):
    try:
        os.unlink(path)
    except (OSError, IOError):
        pass

    if os.path.isdir(path):
        if (not check_mounts) or (not path_includes_mount_point(path)):
            shutil.rmtree(path, True)
        else:
            logging('Error: failed to remove directory ' + path + 'because it includes mount points', SILENT_FLAG, 1)



# Paths to binaries (with appropriate alias) that can be replaced by alternatives
# alias -> path
orig_binaries = \
{
        'php' : '/usr/bin/php-cgi',
'php-cli' : '/usr/bin/php',
'php.ini' : '/etc/php.ini',
  'lsphp' : '/usr/local/bin/lsphp',
'php-fpm' : '/usr/local/sbin/php-fpm'
}

# original binaries are moved to this directory (in skeleton)
ALT_DEST_PATH = '/usr/selector'

# original php.ini is moved to this directory (in skeleton)
ALT_DEST_ETC_PATH = '/usr/selector.etc'

# User's "selector" directory that contains symlinks to alternatives
NATIVE_CONF = ETC_CL_ALT_PATH + '/' + 'native.conf'


def is_mandatory(alias):
    """
    Returns True if php file for appropriate alias is mandatory
    for proper work of PHP Selector (i.e the file should exist
    and should be replaced with symlink successfully)
    :param alias: alias for php file
    :type alias: string
    """
    if is_ea4_enabled():
        return False
    return alias in ('php', 'php-cli', 'php.ini')



config_loaded = False

def read_native_conf():
    global orig_binaries, config_loaded
    if not config_loaded:
        if os.path.isfile(NATIVE_CONF):
            f = open(NATIVE_CONF, 'r')
            for line in f:
                if not line.startswith('#'):
                    line = line.strip()
                    ar = line.split('=', 1)
                    if len(ar) == 2:
                        alias = ar[0].strip()
                        path = ar[1].strip()
                        if alias in orig_binaries:
                            orig_binaries[alias] = path
            f.close()
            config_loaded = True


def is_etc_in_native_conf():
    read_native_conf()
    for path in orig_binaries.values():
        path2 = os.path.realpath(path)
        if path2.startswith('/etc/'):
            return True
    return False



def get_usr_selector_path(alias):
    if alias == 'php.ini':
        return ALT_DEST_ETC_PATH+'/'+alias
    return ALT_DEST_PATH+'/'+alias


def kill_php(file_name):
    if file_name != '' and (not file_name.endswith('.ini')):
        Execute(['/usr/bin/killall', '-q', file_name], check_return_code=False)


def create_php_stub(alias):
    """
    Create stub (empty file) for php file.
    Return True when error has occured
    """
    if alias == 'php.ini':
        selector_dir = SKELETON + ALT_DEST_ETC_PATH
    else:
        selector_dir = SKELETON + ALT_DEST_PATH
    stub_path = os.path.join(selector_dir, alias)
    if not os.path.lexists(stub_path):
        try:
            if not os.path.isdir(selector_dir):
                mod_makedirs(selector_dir, 0o755)
            umask_saved = os.umask(0o22)
            open(stub_path, 'w').close()
            os.umask(umask_saved)
        except (OSError, IOError) as e:
            print_error("Failed to write:", stub_path, ':', str(e))
            return True
    if (linksafe_gid := get_linksafe_gid()) is not None:
        os.chown(stub_path, -1, linksafe_gid)
    return False


def move_to_alternatives(path, etc = False):
    """
    Move php file to /usr/selector* directory inside cagefs-skeleton and create symlink to it
    Return True if php binary has been moved successfully, False otherwise
    :param path: path to original php file
    :type path: string
    :param etc: True = /etc directory is being processed, False otherwise
    :type etc: bool
    """
    sigterm_check()
    if etc:
        if not path.startswith('/etc/'):
            return False
        spath = path
    else:
        spath = strip_path(path)

    read_native_conf()

    for alias in orig_binaries:
        sigterm_check()
        orig_path = os.path.realpath(orig_binaries[alias])
        if (spath == orig_path) and os.path.isfile(orig_path) and (not os.path.islink(orig_path)):
            if alias in ('php', 'php-cli', 'lsphp', 'php.ini'):
                if is_ea4_enabled():
                    create_php_stub(alias)
                    # do not move php file when EA4 is used
                    return False
            filename = alias
            DEST_PATH = get_usr_selector_path(alias)
            LINK_TO = ETC_CL_ALT_PATH+'/'+filename
            dest_file = SKELETON + DEST_PATH
            dest_dir = os.path.dirname(dest_file)
            if etc:
                orig_file = ETC_TEMPLATE_NEW_DIR + orig_path
            else:
                orig_file = SKELETON + orig_path

            for parent_path in (dest_dir, os.path.dirname(orig_file)):
                if not os.path.isdir(parent_path):
                    try:
                        mod_makedirs(parent_path, 0o755)
                    except OSError as e:
                        msg = f'Error: failed to create directory {parent_path} : {str(e).replace("Errno", "Err code")}'
                        logger.error(msg, exc_info=e)
                        logging(msg, SILENT_FLAG, 1)
                        return False

            if is_update_needed(orig_path, dest_file, use_cache=False):
                if copy_file(orig_path, dest_file, create_parent_dir = False):
                    file_name = os.path.basename(orig_path)
                    kill_php(file_name)
                    if copy_file(orig_path, dest_file, create_parent_dir = False):
                        logging('Error copying '+orig_path+' to '+dest_file, SILENT_FLAG, 1)
                        return False
            if (linksafe_gid := get_linksafe_gid()) is not None:
                os.chown(dest_file, -1, linksafe_gid)

            try:
                if os.path.islink(orig_file):
                    if os.readlink(orig_file) != LINK_TO:
                        os.unlink(orig_file)
                        os.symlink(LINK_TO, orig_file)
                else:
                    remove_file_or_dir(orig_file)
                    os.symlink(LINK_TO, orig_file)
            except OSError as e:
                msg = f'Error: failed to create symlink {orig_file} : {str(e).replace("Errno", "Err code")}'
                logger.error(msg, exc_info=e)
                logging(msg, SILENT_FLAG, 1)
                return False

            if not etc:
                add_to_list(orig_file, add_tree = False)
            return True

    return False


def is_path_in_exclusions(path):
    if path == '/':
        return True
    path = stripslash(path)
    return path in {
            '/bin',
            '/boot',
            '/dev',
            '/etc',
            '/lib',
            '/lost+found',
            '/mnt',
            '/proc',
            '/root',
            '/sbin',
            '/sys',
            '/tmp',
            '/usr',
            '/var',
            '/home',
            # Keep both dirs just to be sure for cases when two dirs exists
            # simultaneously, which is itself wrong configuration, but we don't
            # want to fail even in such case
            '/var/www/vhosts',
            PLESK_VHOSTS_D,
    }


PLESK_ORIG_WRAPPER_FILENAME = "/var/www/cgi-bin/cgi_wrapper/cgi_wrapper"

def __copy_wrapper(A, B, C=None):
    logging( 'Copying ' + A + ' to ' + B, SILENT_FLAG, 1)
    shutil.copyfile(A, B)
    if C:
        copy_time_and_permissions(C, B)
    else:
        copy_time_and_permissions(A, B)


def install_plesk_wrapper():
    sigterm_check()
    try:
        if cldetectlib.is_plesk():
            # copy Plesk wrappers
            CLOUDLINUX_WRAPPER = "/var/www/cgi-bin/cgi_wrapper/cloudlinux_wrapper"
            CLOUDLINUX_WRAPPER_PACKAGE = "/usr/share/cagefs-plugins/plesk-cagefs/cloudlinux_wrapper"
            WRAPPERS = (
                    (PLESK_ORIG_WRAPPER_FILENAME, SKELETON+"/var/www/cgi-bin/cgi_wrapper/cgi_wrapper.orig.cagefs", PLESK_ORIG_WRAPPER_FILENAME),
                    ("/usr/share/cagefs-plugins/plesk-cagefs/cgi_wrapper", SKELETON+PLESK_ORIG_WRAPPER_FILENAME, PLESK_ORIG_WRAPPER_FILENAME),
                    (CLOUDLINUX_WRAPPER_PACKAGE, SKELETON+CLOUDLINUX_WRAPPER, PLESK_ORIG_WRAPPER_FILENAME),
                    (CLOUDLINUX_WRAPPER_PACKAGE, CLOUDLINUX_WRAPPER, PLESK_ORIG_WRAPPER_FILENAME)
            )
            # create parent directory (/usr/share/cagefs-skeleton/var/www/cgi-bin/cgi_wrapper)
            dirpath = os.path.dirname(SKELETON+PLESK_ORIG_WRAPPER_FILENAME)
            if not os.path.lexists(dirpath):
                mod_makedirs(dirpath, 0o755)
            # Copy wrappers to cagefs skeleton
            for src, dst, perm in WRAPPERS:
                __copy_wrapper(src, dst, perm)
    except (OSError, IOError) as e:
        print_error('failed to install Plesk wrapper: ' + str(e))


# there is a very tricky situation for this function:
# suppose /srv/jail/opt/bin is a symlink to /usr/bin
# try to lstat(/srv/jail/opt/bin/foo) and you get the result for /usr/bin/foo
# so use resolve_realpath to find you want lstat(/srv/jail/usr/bin/foo)
#
def copy_binaries_and_libs(chroot, binarieslist, force_overwrite=0, be_verbose=0, check_libs=1, try_hardlink=1, retain_owner=0, try_glob_matching=0, handledfiles=[], update=0):
    """copies a list of executables and their libraries to the chroot"""
    sigterm_check()

    if (chroot[-1] == '/'):
        chroot = chroot[:-1]

    for _file in binarieslist:
        sigterm_check()
        if _file in handledfiles:
            continue

        # Build white list of files in etc directory and do nothing else
        if add_to_white_list(_file):
            if os.path.isfile(_file) or os.path.islink(_file):
                handledfiles.append(_file)
            continue

        # Do nothing if path is mounted to skeleton from real system
        if path_is_mounted(_file):
            if os.path.isfile(_file) or os.path.islink(_file):
                handledfiles.append(_file)
            continue

        try:
            sb = cached_lstat(_file)
        except OSError as e:
            if (e.errno == 2):
                if (try_glob_matching == 1):
                    ret = glob.glob(_file)
                    if (len(ret)>0):
                        handledfiles = copy_binaries_and_libs(chroot, ret, force_overwrite, be_verbose, check_libs,
                                                              try_hardlink=try_hardlink, retain_owner=retain_owner,
                                                              try_glob_matching=0, handledfiles=handledfiles, update=update)
                        if debug_option:
                            for item in ret:
                                check_error(item, lineno())
                    else:
                        logging('Source file(s) '+_file+' do not exist',SILENT_FLAG,be_verbose)
                else:
                    logging('Source file(s) '+_file+' do not exist',SILENT_FLAG,be_verbose)
            else:
                print_error('failed to investigate source file',_file,':',e.strerror)
            continue

        # source file exists, resolve the chroot realfile
        create_parent_path(chroot,os.path.dirname(_file), be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner)
        try:
            chrootrfile = resolve_realpath(os.path.normpath(chroot+'/'+_file),chroot)
        except OSError:
            continue

        # Check again after resolve_realpath...
        # Do nothing if path is mounted to skeleton from real system
        if path_is_mounted(chrootrfile):
            if os.path.isfile(_file) or os.path.islink(_file):
                handledfiles.append(_file)
            continue

        # Install wrapper and do nothing else
        if update_proxy_wrapper(chrootrfile):
            handledfiles.append(_file)
            continue

        if _file == PLESK_ORIG_WRAPPER_FILENAME:
            install_plesk_wrapper()
            handledfiles.append(_file)
            continue

        # Replace php binary with symlink (move php binary to /usr/selector)
        if move_to_alternatives(chrootrfile):
            php_libs = get_ldd_libs(_file)
            handledfiles = copy_binaries_and_libs(chroot, php_libs, force_overwrite, be_verbose,
                    check_libs = 0, try_hardlink = try_hardlink, handledfiles = handledfiles, update = update)
            handledfiles.append(_file)
            continue

        # Do nothing if path is blacklisted
        if is_in_black_list(chrootrfile):
            if os.path.isfile(_file) or os.path.islink(_file):
                handledfiles.append(_file)
            continue

        try:
            chrootsb = cached_lstat(chrootrfile)
            chrootfile_exists = 1
            add_to_list(chrootrfile)
        except OSError as e:
            if (e.errno == 2):
                chrootfile_exists = 0
            else:
                print_error('failed to investigate destination file ',chroot,_file,':',e.strerror)

        if ((force_overwrite == 0) and (update == 0) and chrootfile_exists and not stat.S_ISDIR(chrootsb.st_mode)):
            logging(''+chrootrfile+' already exists, will not touch it',SILENT_FLAG,be_verbose)
            check_error(chrootrfile, lineno())
        else:
            if (chrootfile_exists):
                if (force_overwrite):
                    if (stat.S_ISREG(chrootsb.st_mode) or stat.S_ISLNK(chrootsb.st_mode)):
                        logging('Destination file '+chrootrfile+' exists, will delete to force update',SILENT_FLAG,be_verbose)
                        try:
                            os.unlink(chrootrfile)
                        except OSError as e:
                            print_error('ERROR: failed to delete',chrootrfile,':',e.strerror)
                    elif (stat.S_ISDIR(chrootsb.st_mode)):
                        logging('Destination dir '+chrootrfile+' exists',SILENT_FLAG,be_verbose)
                elif (update):
                    if stat.S_ISREG(chrootsb.st_mode) or stat.S_ISLNK(chrootsb.st_mode):
                        if (is_update_needed(_file, chrootrfile, sb, chrootsb, relative_symlinks=True)):
                            logging('Destination file '+chrootrfile+' needs update',SILENT_FLAG,be_verbose)
                            try:
                                os.unlink(chrootrfile)
                            except OSError as e:
                                print_error('failed to delete',chrootrfile,':',e.strerror)
                        else:
                            logging('Destination file '+chrootrfile+' does NOT need update',SILENT_FLAG,be_verbose)
                            add_to_list(chrootrfile, add_tree = False)
                            check_error(chrootrfile, lineno())
                            handledfiles.append(_file)

                            if stat.S_ISLNK(chrootsb.st_mode):
                                try:
                                    realfile = os.readlink(_file)
                                except (OSError, IOError):
                                    realfile = None
                                if realfile != None and (not is_path_in_exclusions(realfile)):
                                    if (realfile[0] != '/'):
                                        realfile = os.path.normpath(os.path.join(os.path.dirname(_file),realfile))
                                    handledfiles = copy_binaries_and_libs(chroot, [realfile], force_overwrite, be_verbose, check_libs=check_libs, try_hardlink=try_hardlink,
                                                                                                              retain_owner=retain_owner, handledfiles=handledfiles, update=update)
                                    check_error(realfile, lineno())

                            # in python 2.1 the return value is a tuple, not an object, st_mode is field 0
                            # mode = stat.S_IMODE(sbuf.st_mode)
                            mode = stat.S_IMODE(sb[stat.ST_MODE])
                            if (check_libs and libs_check_is_needed(_file, mode)):
                                libs = get_libs_from_list(_file)
                                # file is NOT found in list of libs (key is NOT found in dict)?
                                if libs == None:
                                    libs = get_ldd_libs(_file)
                                    add_libs_to_list(_file, libs)
                                handledfiles = copy_binaries_and_libs(chroot, libs, force_overwrite, be_verbose,
                                                                                                          check_libs=0, try_hardlink=try_hardlink,
                                                                                                          handledfiles=handledfiles, update=update)
                                if debug_option:
                                    for item in libs:
                                        check_error(item, lineno())
                            continue
                    elif (stat.S_ISDIR(chrootsb.st_mode)):
                        logging('Destination dir '+chrootrfile+' exists',SILENT_FLAG,be_verbose)
                else:
                    if (stat.S_ISDIR(chrootsb.st_mode)):
                        pass
                        # for a directory we also should inspect all the contents, so we do not
                        # skip to the next item of the loop
                    else:
                        logging('Destination file '+chrootrfile+' exists',SILENT_FLAG,be_verbose)
                        continue
            # endif (chrootfile_exists)

            create_parent_path(chroot,os.path.dirname(_file), be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner)
            if (stat.S_ISLNK(sb.st_mode)):
                realfile = update_symlink_in_skeleton(_file, chrootrfile)
                add_to_list(chrootrfile, add_tree = False)
                handledfiles.append(_file)
                if not is_path_in_exclusions(realfile):
                    if (realfile[0] != '/'):
                        realfile = os.path.normpath(os.path.join(os.path.dirname(_file),realfile))
                    handledfiles = copy_binaries_and_libs(chroot, [realfile], force_overwrite, be_verbose,
                                                                                      check_libs=check_libs, try_hardlink=try_hardlink,
                                                                                      retain_owner=retain_owner,
                                                                                      handledfiles=handledfiles, update=update)
                    check_error(realfile, lineno())
            elif (stat.S_ISDIR(sb.st_mode)):
                epath = create_parent_path(chroot, _file, be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner)
                epath = stripslash(epath)
                if debug_option and (epath != chrootrfile):
                    print_error('line', lineno(), ': ', epath, '!=', chrootrfile)
                # add_to_list(chrootrfile)
                handledfiles = copy_dir_recursive(chroot,_file,force_overwrite, be_verbose,
                                                                                  check_libs=check_libs, try_hardlink=try_hardlink,
                                                                                  retain_owner=retain_owner,
                                                                                  handledfiles=handledfiles, update=update)
                check_error(chrootrfile, lineno())
            elif (stat.S_ISREG(sb.st_mode)):
                if (try_hardlink):
                    logging ('Trying to link '+_file+' to '+chrootrfile ,SILENT_FLAG,1)
                else:
                    logging ('Copying '+_file+' to '+chrootrfile,SILENT_FLAG,1)
                copy_with_permissions(_file,chrootrfile,be_verbose, try_hardlink=try_hardlink, retain_owner=retain_owner)
                handledfiles.append(_file)
                check_error(chrootrfile, lineno())
            elif (stat.S_ISCHR(sb.st_mode) or stat.S_ISBLK(sb.st_mode)):
                copy_device(chroot, _file, be_verbose, retain_owner)

            # in python 2.1 the return value is a tuple, not an object, st_mode is field 0
            # mode = stat.S_IMODE(sbuf.st_mode)
            mode = stat.S_IMODE(sb[stat.ST_MODE])
            if (check_libs and libs_check_is_needed(_file, mode)):
                if stat.S_ISLNK(sb.st_mode) or stat.S_ISREG(sb.st_mode):
                    libs = get_ldd_libs(_file)
                    add_libs_to_list(_file, libs)
                    handledfiles = copy_binaries_and_libs(chroot, libs, force_overwrite, be_verbose,
                                                                                              check_libs=0, try_hardlink=try_hardlink,
                                                                                              handledfiles=handledfiles, update=update)
                    if debug_option:
                        for item in libs:
                            check_error(item, lineno())

    return handledfiles



def config_get_option_as_list(cfgparser, sectionname, optionname):
    """retrieves a comma separated option from the configparser and splits it into a list, returning an empty list if it does not exist"""
    retval = []
    if (cfgparser.has_option(sectionname,optionname)):
        inputstr = cfgparser.get(sectionname,optionname)
        for tmp in inputstr.split(','):
            item = tmp.strip()
            if item != '':
                retval += [item]
    return retval



def init_passwd_and_group(dest_dir, users_list, groups_list, be_verbose=0):
    if (dest_dir[-1] == '/'):
        dest_dir = dest_dir[:-1]

    if not os.path.isdir(dest_dir):
        try:
            mod_makedirs(dest_dir, 0o755)
        except OSError:
            print_error('creating', dest_dir)
            sys.exit(1)

    os.chmod(dest_dir, 0o755)

    users = []
    users.extend(users_list)
    groups = []
    groups.extend(groups_list)

    if (sys.platform[4:7] == 'bsd'):
        open(dest_dir+'/passwd','a').close()
        open(dest_dir+'/spwd.db','a').close()
        open(dest_dir+'/pwd.db','a').close()
        open(dest_dir+'/master.passwd','a').close()
    else:
        if (not os.path.isfile(dest_dir+'/passwd')):
            fd2 = open(dest_dir+'/passwd','w')
        else:
            # the passwds file exists, check if any of the users exist already
            fd2 = open(dest_dir+'/passwd','r+')
            line = fd2.readline()
            while (len(line)>0):
                pwstruct = line.split(':')
                if (len(pwstruct) >=3):
                    if ((pwstruct[0] in users) or (pwstruct[2] in users)):
                        logging('user '+pwstruct[0]+' exists in '+dest_dir+'/passwd',SILENT_FLAG,be_verbose)
                        try:
                            users.remove(pwstruct[0])
                        except ValueError:
                            pass
                        try:
                            users.remove(pwstruct[2])
                        except ValueError:
                            pass
                line = fd2.readline()
            fd2.seek(0,2)
        if (len(users) > 0):
            fd = open('/etc/passwd','r')
            line = fd.readline()
            while (len(line)>0):
                pwstruct = line.split(':')
                if (len(pwstruct) >=3):
                    if ((pwstruct[0] in users) or (pwstruct[2] in users)):
                        fd2.write(line)
                        logging('writing user '+pwstruct[0]+' to '+dest_dir+'/passwd',SILENT_FLAG,be_verbose)
                        if (not pwstruct[3] in groups):
                            groups += [pwstruct[3]]
                line = fd.readline()
            fd.close()
        fd2.close()

    # Copy permissions from real system to template file
    copy_time_and_permissions('/etc/passwd', dest_dir+'/passwd', be_verbose=0, allow_suid=0, copy_ownership=1)

    # do the same sequence for the group files
    if (not os.path.isfile(dest_dir+'/group')):
        fd2 = open(dest_dir+'/group','w')
    else:
        fd2 = open(dest_dir+'/group','r+')
        line = fd2.readline()
        while (len(line)>0):
            groupstruct = line.split(':')
            if (len(groupstruct) >=2):
                if ((groupstruct[0] in groups) or (groupstruct[2] in groups)):
                    logging('group '+groupstruct[0]+' exists in '+dest_dir+'/group',SILENT_FLAG,be_verbose)
                    try:
                        groups.remove(groupstruct[0])
                    except ValueError:
                        pass
                    try:
                        groups.remove(groupstruct[2])
                    except ValueError:
                        pass
            line = fd2.readline()
        fd2.seek(0,2)
    if (len(groups) > 0):
        fd = open('/etc/group','r')
        line = fd.readline()
        while (len(line)>0):
            groupstruct = line.split(':')
            if (len(groupstruct) >=2):
                if ((groupstruct[0] in groups) or (groupstruct[2] in groups)):
                    fd2.write(line)
                    logging('writing group '+groupstruct[0]+' to '+dest_dir+'/group',SILENT_FLAG,be_verbose)
            line = fd.readline()
        fd.close()
    fd2.close()

    # Copy permissions from real system to template file
    copy_time_and_permissions('/etc/group', dest_dir+'/group', be_verbose=0, allow_suid=0, copy_ownership=1)




def init_safe_users_and_groups(dest_dir, users_list ,groups_list, be_verbose=0):
    if (dest_dir[-1] == '/'):
        dest_dir = dest_dir[:-1]

    umask_saved = os.umask(0o22)

    if not os.path.isdir(dest_dir):
        try:
            mod_makedirs(dest_dir, 0o751)
        except OSError:
            print_error('creating', dest_dir)
            sys.exit(1)

    os.chmod(dest_dir, 0o751)

    users = []
    users.extend(users_list)
    groups = []
    groups.extend(groups_list)

    if (not os.path.isfile(dest_dir+'/safe.users')):
        fd2 = open(dest_dir+'/safe.users','w')
    else:
        # the file exists, check if any of the users exist already
        fd2 = open(dest_dir+'/safe.users','r+')
        line = fd2.readline()
        line = line.rstrip()
        while (len(line)>0):
            if line in users:
                logging('user '+line+' exists in '+dest_dir+'/safe.users',SILENT_FLAG,be_verbose)
                try:
                    users.remove(line)
                except ValueError:
                    pass
            line = fd2.readline()
            line = line.rstrip()
        fd2.seek(0,2)

    if (len(users) > 0):
        fd = open('/etc/passwd','r')
        line = fd.readline()
        while (len(line)>0):
            pwstruct = line.split(':')
            if (len(pwstruct) >=3):
                if ((pwstruct[0] in users) or (pwstruct[2] in users)):
                    fd2.write(pwstruct[0]+"\n")
                    logging('writing user '+pwstruct[0]+' to '+dest_dir+'/safe.users',SILENT_FLAG,be_verbose)
            line = fd.readline()
        fd.close()
    fd2.close()
    try:
        os.chmod(dest_dir+'/safe.users', 0o600)
    except (OSError, IOError):
        logging("Error: failed to set permissions to "+dest_dir+'/safe.users',SILENT_FLAG, 1)

    # do the same sequence for the group files
    if (not os.path.isfile(dest_dir+'/safe.groups')):
        fd2 = open(dest_dir+'/safe.groups','w')
    else:
        fd2 = open(dest_dir+'/safe.groups','r+')
        line = fd2.readline()
        line = line.rstrip()
        while (len(line)>0):
            if line in groups:
                logging('group '+line+' exists in '+dest_dir+'/safe.groups',SILENT_FLAG,be_verbose)
                try:
                    groups.remove(line)
                except ValueError:
                    pass
            line = fd2.readline()
            line = line.rstrip()
        fd2.seek(0,2)
    if (len(groups) > 0):
        fd = open('/etc/group','r')
        line = fd.readline()
        while (len(line)>0):
            groupstruct = line.split(':')
            if (len(groupstruct) >=2):
                if ((groupstruct[0] in groups) or (groupstruct[2] in groups)):
                    fd2.write(groupstruct[0]+"\n")
                    logging('writing group '+groupstruct[0]+' to '+dest_dir+'/safe.groups',SILENT_FLAG,be_verbose)
            line = fd.readline()
        fd.close()
    fd2.close()
    try:
        os.chmod(dest_dir+'/safe.groups', 0o600)
    except (OSError, IOError):
        logging("Error: failed to set permissions to "+dest_dir+'/safe.groups',SILENT_FLAG, 1)

    os.umask(umask_saved)




def init_shadow(dest_dir, users_list, be_verbose=0):
    if (dest_dir[-1] == '/'):
        dest_dir = dest_dir[:-1]

    if not os.path.isdir(dest_dir):
        try:
            mod_makedirs(dest_dir, 0o755)
        except OSError:
            print_error('Error while creating', dest_dir)
            sys.exit(1)

    os.chmod(dest_dir, 0o755)

    users = []
    users.extend(users_list)

    if (not os.path.isfile(dest_dir+'/shadow')):
        fd2 = open(dest_dir+'/shadow','w')
    else:
        # the shadow file exists, check if any of the users exist already
        fd2 = open(dest_dir+'/shadow','r+')
        line = fd2.readline()
        while (len(line)>0):
            pwstruct = line.split(':')
            if (len(pwstruct) >=1):
                if pwstruct[0] in users:
                    logging('user '+pwstruct[0]+' exists in '+dest_dir+'/shadow',SILENT_FLAG,be_verbose)
                    try:
                        users.remove(pwstruct[0])
                    except ValueError:
                        pass
            line = fd2.readline()
        fd2.seek(0,2)
    if (len(users) > 0):
        fd = open('/etc/shadow','r')
        line = fd.readline()
        while (len(line)>0):
            pwstruct = line.split(':')
            if (len(pwstruct) >=1):
                if pwstruct[0] in users:
                    fd2.write(line)
                    logging('writing user '+pwstruct[0]+' to '+dest_dir+'/shadow',SILENT_FLAG,be_verbose)
            line = fd.readline()
        fd.close()
    fd2.close()

    # Copy permissions from real system to template file
    copy_time_and_permissions('/etc/shadow', dest_dir+'/shadow', be_verbose=0, allow_suid=0, copy_ownership=1)




def add_user_to_shadow(dest_dir, user, be_verbose = 0):
    dest_dir = stripslash(dest_dir)
    if not os.path.isfile(dest_dir+'/shadow'):
        logging("Error: "+dest_dir+'/shadow does not exist', SILENT_FLAG, 1)
        return
    if os.path.islink(dest_dir+'/shadow'):
        logging("Error: "+dest_dir+'/shadow is a symlink', SILENT_FLAG, 1)
        return
    fd = open('/etc/shadow', 'r')
    dest = open(dest_dir+'/shadow', 'a')
    line = fd.readline()
    while len(line) > 0:
        pwstruct = line.split(':', 1)
        if len(pwstruct) >= 1:
            if pwstruct[0] == user:
                dest.write(line)
                logging('Writing user '+pwstruct[0]+' to '+dest_dir+'/shadow', SILENT_FLAG, be_verbose)
                break
        line = fd.readline()
    fd.close()
    dest.close()





# Returns common end of two lists (or strings)
def get_common_end(s1, s2):
    if len(s1) == 0 or len(s2) == 0:
        return None
    min_len = min(len(s1), len(s2))
    pos = -1
    while pos >= -min_len:
        if s1[pos] != s2[pos]:
            break
        pos -= 1
    if pos == -1:
        return None
    return s1[pos+1:]


def copy_path(src: str, dst: str) -> int:
    """
    Copy a path from a source to a destination.

    If there are shared ending directories between source and destination paths,
    iterates over the common ending directories,
    creating each corresponding directory in a destination path.
    Copies timestamp and permissions from source subdirectories.

    For example, if src = '/root/dir1/dir2' and dst = '/usr/share/cagefs-skeleton/dir1/dir2',
    running this function will result in creating directories 'dir1' and 'dir2'
    within the '/usr/share/cagefs-skeleton' path.
    """
    src = os.path.normpath(src)
    dst = os.path.normpath(dst)
    src = stripslash(src)
    dst = stripslash(dst)

    if (src == '') or (dst == '') or (src[0] != '/') or (dst[0] != '/'):
        logging("Error: invalid paths src = "+src+" dst = "+dst, SILENT_FLAG, 1)
        # error
        return 1

    common = get_common_end(split_path(src), split_path(dst))
    if common is None:
        try:
            mod_makedirs(dst, 0o755)
        except (IOError, OSError):
            # logging("Error: failed to create "+dst, SILENT_FLAG, 1)
            # error
            # return 1
            pass
        # success
        return 0

    common_str = join_path(common)
    dst_path = dst[:-len(common_str)]
    src_path = src[:-len(common_str)]
    for _dir in common:
        dst_path = dst_path+'/'+_dir
        src_path = src_path+'/'+_dir
        try:
            mod_makedirs(dst_path, 0o755)
        except (IOError, OSError):
            # logging("Error: failed to create path "+dst_path, SILENT_FLAG, 1)
            # return 1
            pass

        try:
            copy_time_and_permissions(src_path, dst_path, be_verbose=0, allow_suid=0, copy_ownership=1)
        except (IOError, OSError) as e:
            logging('ERROR: while copying permissions '+src_path+' to '+dst_path+': '+e.strerror, SILENT_FLAG, 1)
            return 1

    # success
    return 0


def oslstat(path: AnyStr) -> os.stat_result | None:
    """
    Securely get status of a file or a file descriptor.

    Returns None if unable to retrieve the status.
    """
    try:
        return os.lstat(path)
    except (IOError, OSError):
        return None


def copytree(src: str,
             dst: str,
             symlinks: bool = True,
             overwrite: bool = True,
             skip_src_dirs: list[str] | None = None,
             update: bool = False,
             skip_dst_files: list[str] | None = None) -> int:
    """
    Recursively copy an entire directory tree.

    This function acts like shutil.copytree, but works
    if destination directory already exists and does not fail if symlink exists.
    Copies timestamp and permissions from source subdirectories.
    """
    if skip_src_dirs is None:
         skip_src_dirs = []
    if skip_dst_files is None:
         skip_dst_files = []

    if src in skip_src_dirs:
        return 0

    names = os.listdir(src)

    # If destination is a directory, do nothing,
    # otherwise (file or symlink), remove it
    try:
        dstbuf = os.lstat(dst)
        if stat.S_ISDIR(dstbuf.st_mode):
            dst_exists = True
        else:
            if dst in skip_dst_files:
                return 0
            try:
                os.unlink(dst)
            except (IOError, OSError) as e:
                logging('ERROR: failed to delete file '+dst+' : '+str(e), SILENT_FLAG, 1)
                return 1
            dst_exists = False
    except (IOError, OSError):
        dst_exists = False

    error = 0

    # If destination does not exist (or removed on the previous step)
    # create required subdirectories
    if not dst_exists:
        if copy_path(src, dst) == 1:
            error = 1

    # Copy contents of the source directory to the destination one
    for name in names:
        srcname = os.path.join(src, name)
        dstname = os.path.join(dst, name)
        try:
            srcbuf = cached_lstat(srcname)
            dstbuf = oslstat(dstname)
            dstname_exists = dstbuf is not None
            if symlinks and stat.S_ISLNK(srcbuf.st_mode):
                if (not overwrite) and dstname_exists:
                    continue
                if dstname_exists and (dstname in skip_dst_files):
                    continue
                linkto = os.readlink(srcname)
                if dstname_exists:
                    if stat.S_ISDIR(dstbuf.st_mode):
                        shutil.rmtree(dstname, True)
                    else:
                        os.unlink(dstname)
                os.symlink(linkto, dstname)
            elif stat.S_ISDIR(srcbuf.st_mode):
                if copytree(srcname, dstname, symlinks, overwrite, skip_src_dirs, update) == 1:
                    error = 1
            else:
                if (not overwrite) and dstname_exists:
                    continue
                if dstname_exists and (dstname in skip_dst_files):
                    continue
                if dstname_exists and update and (not is_update_needed(srcname, dstname, srcbuf, dstbuf)):
                    continue
                if dstname_exists:
                    if stat.S_ISLNK(dstbuf.st_mode):
                        os.unlink(dstname)
                    elif stat.S_ISDIR(dstbuf.st_mode):
                        shutil.rmtree(dstname, True)
                shutil.copyfile(srcname, dstname)
                copy_time_and_permissions(srcname, dstname, be_verbose=0, allow_suid=0, copy_ownership=1)
        except (IOError, OSError, shutil.Error) as err:
            logging('ERROR: while copying '+srcname+' to '+dstname+': '+str(err), SILENT_FLAG, 1)
            error = 1
    try:
        copy_time_and_permissions(src, dst, be_verbose=0, allow_suid=0, copy_ownership=1)
    except (IOError, OSError) as err:
        logging('ERROR: while copying permissions '+src+' to '+dst+': '+str(err), SILENT_FLAG, 1)
        error = 1

    return error


def copy_file(srcname: str,
              dstname: str,
              create_parent_dir: bool = True,
              update: bool = False) -> int:
    """
    Copy a source file to a specified destination.

    The algorithm is as follows:
    - if the source is a directory - fail;
    - if the source is a symlink, remove the current destination
      and create a symlink in its place that points to the same location as the source symlink;
    - otherwise - remove current destination,
      and copy the source file copying its time and permissions as well.
    """
    sigterm_check()
    try:
        srcbuf = cached_lstat(srcname)
        if stat.S_ISDIR(srcbuf.st_mode):
            # error
            return 1
        dstbuf = oslstat(dstname)
        dstname_exists = dstbuf is not None
        if not dstname_exists:
            parent_dir = os.path.dirname(dstname)
            if parent_dir != '/' and create_parent_dir:
                copy_path(os.path.dirname(srcname), parent_dir)
        if stat.S_ISLNK(srcbuf.st_mode):
            linkto = os.readlink(srcname)
            if dstname_exists:
                if stat.S_ISDIR(dstbuf.st_mode):
                    shutil.rmtree(dstname, True)
                else:
                    os.unlink(dstname)
            os.symlink(linkto, dstname)
        else:
            if dstname_exists and update and (not is_update_needed(srcname, dstname, srcbuf, dstbuf)):
                return 0
            if dstname_exists:
                if stat.S_ISDIR(dstbuf.st_mode):
                    shutil.rmtree(dstname, True)
                else:
                    os.unlink(dstname)
            shutil.copyfile(srcname, dstname)
            copy_time_and_permissions(srcname, dstname, be_verbose=0, allow_suid=0, copy_ownership=1)
    except (IOError, OSError, shutil.Error) as e:
        logging('ERROR: while copying '+srcname+' to '+dstname+': '+e.strerror, SILENT_FLAG, 1)
        return 1

    # Success
    return 0



def test_numitem_exist(item,num,filename):
    try:
        fd = open(filename,'r')
    except:
        return 0
    line = fd.readline()
    while (len(line)>0):
        pwstruct = line.split(':')
        if (len(pwstruct) > num and pwstruct[num] == item):
            fd.close()
            return 1
        line = fd.readline()
    return 0

def test_user_exist(user, passwdfile):
    return test_numitem_exist(user,0,passwdfile)

def test_group_exist(group, groupfile):
    return test_numitem_exist(group,0,groupfile)


def get_all_users_with_uid(uid):
    # get all users from /etc/passwd
    return clpwd.get_names(uid)


def ExecuteSimple(command):
    proc = subprocess.Popen(command,
                            shell=True,
                            executable='/bin/bash',
                            stdout=subprocess.PIPE,
                            text=True,
                            bufsize=-1)
    return proc.communicate()[0]


def Execute(command, check_return_code = True, merge_stderr = False, exit_on_error = True):
    try:
        if merge_stderr:
            p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
        else:
            # run the command and suppress it's output
            p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        (out, _) = p.communicate()
        if check_return_code:
            # check return code of the child
            if p.returncode != 0:
                logging('Error while executing ' + ' '.join(command), SILENT_FLAG, 1)
    except OSError:
        logging('Error: failed to run ' + ' '.join(command), SILENT_FLAG, 1)
        if exit_on_error:
            sys.exit(1)
        raise

    return out



def get_version_str(line, sign):
    length = len(sign)
    pos = line.find(sign)
    if pos != -1:
        end = line[pos+length:]
        pos2 = 0
        while pos2 < len(end):
            if not isdigit(end[pos2]):
                break
            pos2 += 1
        ver = end[:pos2]
        return ver
    return ''


# Returns value of specified option from postgresql.conf
def get_postgres_config(option):
    value = ''
    PSQL_CONF = "/var/lib/pgsql/data/postgresql.conf"
    if os.path.isfile(PSQL_CONF):
        psql_conf = read_file(PSQL_CONF)
        for i in range(len(psql_conf)):
            line = psql_conf[i]
            if line and line[0] != '#' and line.find(option) != -1:
                v = line.split('=', 1)
                opt_name = v[0].strip()
                if opt_name == option:
                    val = v[1].strip()
                    pos = val.find("#")
                    if pos != -1:
                        val = val[:pos]
                    val = val.strip()
                    val = val.strip("'")
                    val = val.strip('"')
                    value = val
                    break

    return value


# Returns port of postgresql server (as string)
def get_postgres_port():
    OPTS = '/var/lib/pgsql/data/postmaster.opts'
    if os.path.isfile(OPTS):
        lines = read_file(OPTS)
        for line in lines:
            v = line.split()
            for i in range(len(v)):
                s = v[i]
                s = s.strip("'")
                s = s.strip('"')
                if s == '-p':
                    try:
                        port = v[i+1]
                    except:
                        print_error('Error while parsing', OPTS)
                        sys.exit(1)
                    port = port.strip("'")
                    port = port.strip('"')
                    return port

    return get_postgres_config('port')


def detect_postgres():
    PGSQL_SOCKET_CFG = "/usr/share/cagefs/pgsql.socket.name"
    default_pg_port = '5432'
    port = get_postgres_port()

    if port == '':
        print('Warning: Port of PostgreSQL server is not detected, using default: {}'.format(default_pg_port))
        port = default_pg_port

    socket_name = '/tmp/.s.PGSQL.'+port

    # Not default port ?
    if port != default_pg_port:
        write_file(PGSQL_SOCKET_CFG, [socket_name+'\n'])
    else:
        if os.path.isfile(PGSQL_SOCKET_CFG):
            try:
                os.unlink(PGSQL_SOCKET_CFG)
            except (OSError, IOError):
                # try to write default socket name to config file
                write_file(PGSQL_SOCKET_CFG, [socket_name+'\n'])



# ETC_MPFILE should be read to cagefslib.mounts before call of this function
def print_suids(src):
    if strip_path(src) == '/proc':
        return
    mounted = path_is_mounted(src)
    for name in os.listdir(src):
        srcname = os.path.join(src, name)
        try:
            mode = os.lstat(srcname).st_mode
        except OSError:
            print_error('lstat() failed for path', srcname)
            continue
        if stat.S_ISLNK(mode):
            continue
        if stat.S_ISDIR(mode):
            print_suids(srcname)
        elif (mode & (stat.S_ISUID | stat.S_ISGID)):
            if mounted:
                print('Mounted to skeleton:', srcname)
            else:
                print('Copied  to skeleton:', srcname)



def get_users_from_passwd(path):
    users = {}
    if os.path.isfile(path):
        pf = open(path, 'r')
        for line in pf:
            user = line.split(':')[0]
            users[user] = 1
        pf.close()
    return users



def remove_unwanted_users_from_groups(users = None, path = ETC_TEMPLATE_NEW_DIR):
    sigterm_check()
    group_file = path + '/etc/group'
    if not os.path.isfile(group_file):
        return

    lines = read_file(group_file)

    if users == None:
        users = get_users_from_passwd(path+'/etc/passwd')

    file_changed = False

    for i in range(len(lines)):
        splitted = lines[i].split(':')
        if (len(splitted) == 4) and (splitted[3] not in ('', '\n')):
            group_users = splitted[3][:-1].split(',')
            new_group_users = []
            changed = False
            for user in group_users:
                if user in users:
                    new_group_users.append(user)
                else:
                    changed = True

            if changed:
                tmp = splitted[0]+':x:'+splitted[2]+':'
                tmp2 = ','.join(new_group_users)
                tmp += tmp2+'\n'
                lines[i] = tmp
                file_changed = True

    if file_changed:
        write_file(group_file, lines)
        try:
            shutil.copystat('/etc/group', group_file)
        except (OSError, IOError, shutil.Error):
            pass



def read_symlinks(cl_alt_dir):
    links = {}
    if os.path.isdir(cl_alt_dir):
        for _file in os.listdir(cl_alt_dir):
            path = os.path.join(cl_alt_dir, _file)
            if os.path.islink(path):
                link_to = os.readlink(path)
                links[path] = link_to
    return links


# Returns True if error has occured
def write_symlinks(links):
    error = False
    for path in links:
        try:
            link_to = links[path]
            if os.path.islink(path):
                if link_to == os.readlink(path):
                    continue
            elif os.path.isdir(path):
                continue
            try:
                os.unlink(path)
            except OSError as e:
                if e.errno == errno.ENOENT:  # No such file error
                    logger.info(f'Path {path} does not exist')
                else:
                    logger.error(f'Error: Unable to remove path {path}', exc_info=e)

            os.symlink(link_to, path)
        except OSError as e:
            msg = f'Error: failed to create symlink {path} : {str(e).replace("Errno", "Err code")}'
            logger.error(msg, exc_info=e)
            logging(msg, SILENT_FLAG, 1)
            error = True
    return error




def is_writable(sbuf, uid, gid):
    groups = get_groups(uid, gid)
    mode = sbuf.st_mode

    # owner has write access ?
    if sbuf.st_uid == uid:
        return (mode & stat.S_IWUSR)

    return ( ((mode & stat.S_IWGRP) and (sbuf.st_gid in groups)) or (mode & stat.S_IWOTH) )


def make_dir(path, perm, allow_symlink = False, update_perm = True):
    """
    Create directory if it does not exist. Check for symlink (race conditions are not handled).
    Returns True if error has occured
    :param path: path to directory
    :type path: string
    :param perm: Linux permissions
    :type perm: int
    :param allow_symlink: True = allow path to be symlink, False = delete symlink and create directory
    :type allow_symlink: bool
    :param update_perm: True = set permissions when path exists
    :type update_perm: bool
    """
    path_exists = True
    try:
        sbuf = os.lstat(path)
    except OSError:
        path_exists = False

    if path_exists:
        if stat.S_ISDIR(sbuf.st_mode) or (allow_symlink and os.path.isdir(path)):
            if update_perm:
                return set_perm(path, perm)
            return False
        else:
            try:
                os.unlink(path)
            except (OSError, IOError):
                if os.path.lexists(path):
                    logging('Error: failed to remove ' + path, SILENT_FLAG, 1)
                    return True

    try:
        mod_makedirs(path, perm)
    except OSError as e:
        if not os.path.isdir(path):
            msg = f'Error: failed to create directory {path} : {str(e).replace("Errno", "Err code")}'
            logger.error(msg, exc_info=e)
            logging(msg, SILENT_FLAG, 1)
            return True

    return False



# Returns True if error has occured
def set_perm(path, perm):
    try:
        os.chmod(path, perm)
        return False
    except (OSError, IOError):
        logging('Error: failed to set permissions to ' + path, SILENT_FLAG, 1)
        return True



# Returns True if error has occured
def set_owner(path, uid, gid):
    try:
        os.chown(path, uid, gid)
        return False
    except (OSError, IOError):
        logging('Error: failed to set ownership to ' + path, SILENT_FLAG, 1)
        return True



# basepath == '/home/user/.cagefs'
def fix_owner_of_personal_mounts(basepath, real_homepath, uid, gid, personal_mounts):
    for mount in personal_mounts:
        names = split_path(mount)
        path = ''
        for name in names:
            path = os.path.join(path, name)
            dirpath = os.path.join(basepath, path)
            fd = set_owner_dir_secure(dirpath, uid, gid, real_homepath)
            if fd is None:
                # Error has occured or directory does not exist
                # So, do not process subdirectories
                break
            closefd(fd)



# is_user_enabled = True: users are enabled
# is_user_enabled = False: users are disabled
def update_status(users, is_user_enabled, fix_owner = False):
    if not (fix_owner or cldetectlib.is_ispmanager()):
        return

    import cagefs_ispmanager_lib
    personal_mounts = []
    if fix_owner:
        from cagefsctl import MountpointConfig
        mp_config = MountpointConfig(skip_errors=True, skip_cpanel_check=True)
        personal_mounts = mp_config.personal_mounts

    for user in users:
        try:
            pw = clpwd.get_pw_by_name(user)
        except ClPwd.NoSuchUserException:
            continue
        real_homepath = os.path.realpath(pw.pw_dir)
        path = os.path.join(pw.pw_dir, '.cagefs')
        status_flag = os.path.join(path, '.cagefs.enabled')

        # Set owner
        fd = set_owner_dir_secure(path, pw.pw_uid, pw.pw_gid, real_homepath)
        if fd is None:
            # Error has occured
            continue
        # Set permissions
        fd = set_perm_dir_secure(path, 0o771, real_homepath, fd=fd)
        if fd is None:
            # Error has occured
            continue
        closefd(fd)
        set_user_perm(pw.pw_uid, pw.pw_gid)
        # .cagefs.enabled files are not needed, we remove them unconditionally
        remove_file_or_dir(status_flag)
        set_root_perm()

        # update ISP manager user wrappers
        cagefs_ispmanager_lib.ispmanager_create_user_wrapper_detect_php_ver(pw, is_user_enabled, True)

        if fix_owner:
            fix_owner_of_personal_mounts(path, real_homepath, pw.pw_uid, pw.pw_gid, personal_mounts)


def get_php_info():
    php_path = orig_binaries['php-cli']
    php_ini_path = orig_binaries['php.ini']
    return Execute([php_path, '-c', php_ini_path, '-i'])


def get_list_of_php_modules():
    php_path = orig_binaries['php-cli']
    php_ini_path = orig_binaries['php.ini']
    try:
        if is_ea4_enabled():
            result = Execute([php_path, '-m'], merge_stderr=True, exit_on_error=False)
        else:
            result = Execute([php_path, '-c', php_ini_path, '-qm'], merge_stderr=True, exit_on_error=False)
    except OSError:
        result = ''
    return result



php_modules = None


# Returns list of php extension modules (.so)
def get_php_modules():
    global php_modules
    if php_modules == None:
        read_native_conf()
        lines = get_list_of_php_modules().split('\n')
        php_modules = {}
        for line in lines:
            if line and (not line.startswith('[')):
                module_name = line.replace(' ', '_').lower()
                php_modules[module_name] = 1
    return list(php_modules)


alt_versions = None


# Reads config file and returns versions of alternative php binaries (as dictionary)
def get_alt_versions():
    global alt_versions
    if alt_versions is None:
        CL_ALT_CONF = '/etc/cl.selector/selector.conf'
        alt_versions = {}
        if os.path.isfile(CL_ALT_CONF):
            lines = read_file_cached(CL_ALT_CONF)
            for line in lines:
                line = line.rstrip()
                if line != "":
                    ar = line.split()
                    if len(ar) == 4 and ar[0] == 'php':
                        vers = ar[1]
                        # Key - short version (5.4), value - full version (5.4.39)
                        alt_versions[vers] = ar[2]
    return alt_versions



# version -> alias -> path
alt_conf = None


# Reads config file and returns path to alternative php binary
def get_alt_conf(vers, alias = 'php', get_aliases = False):
    global alt_conf
    if alt_conf == None:
        CL_ALT_CONF = '/etc/cl.selector/selector.conf'
        alt_conf = {}
        if os.path.isfile(CL_ALT_CONF):
            lines = read_file_cached(CL_ALT_CONF)
            for line in lines:
                line = line.rstrip()
                if line != "":
                    ar = line.split()
                    if len(ar) == 4:
                        cur_alias = ar[0]
                        cur_vers = ar[1]
                        cur_path = ar[3]

                        if cur_vers in alt_conf:
                            temp = alt_conf[cur_vers]
                        else:
                            temp = {}

                        temp[cur_alias] = cur_path
                        alt_conf[cur_vers] = temp

    if vers in alt_conf:
        if get_aliases:
            # Return all available aliases for specified version
            return list(alt_conf[vers].keys())
        elif alias in alt_conf[vers]:
            # Return path for version and alias specified
            return alt_conf[vers][alias]

    return None



def get_alt_aliases(vers):
    if vers == 'native':
        return list(orig_binaries)
    else:
        aliases = get_alt_conf(vers, get_aliases = True)
        if aliases == None:
            return []
        return aliases


def get_alt_php_libs():
    FIND = '/usr/bin/find'
    altpaths = []
    binpaths = []
    for altdir in get_alt_dirs():
        path = '/opt/alt/' + altdir
        binpath = path + '/usr/bin'
        sbinpath = path + '/usr/sbin'
        if os.path.isdir(path):
            altpaths.append(path)
        else:
            continue
        if os.path.isdir(binpath):
            binpaths.append(binpath)
        if os.path.isdir(sbinpath):
            binpaths.append(sbinpath)

    paths = []
    if altpaths:
        paths.extend(Execute([FIND] + altpaths + ['-name', '*.so']).split('\n'))
    if binpaths:
        paths.extend(Execute([FIND] + binpaths + ['-type', 'f']).split('\n'))

    libs = set()
    for path in paths:
        if path:
            for lib in get_ldd_libs(path):
                libs.add(lib)
    return list(libs)


# Detect user's PHP version
def get_php_version_for_user(username):
    # read link /var/cagefs/[prefix]/[username]/etc/cl.selector/php
    #  --> /opt/alt/php54/usr/bin/php-cgi
    # or
    #  --> /usr/selector/php
    path = BASEDIR + '/' + get_user_prefix(username) + '/' + username + '/etc/cl.selector/'
    user_php_file = path + 'php'
    if not os.path.islink(user_php_file):
        user_php_file = path + 'lsphp'
        if not os.path.islink(user_php_file):
            return None
    link_to = os.readlink(user_php_file)
    if link_to.startswith('/usr/selector/'):
        return 'native'
    # PHP ver is not native, determine version from link_to
    if link_to.startswith('/opt/alt/php'):
        php_ver = link_to.replace('/opt/alt/php', '')
        php_ver = php_ver[:php_ver.find('/')]
        return php_ver
    # PHP version not determined
    return None



# Name and location of user's php.d directory, where symlinks to .ini files are stored
CL_PHP_DIR_NAME = 'cl.php.d'
ETC_CL_PHP_PATH = '/etc/'+CL_PHP_DIR_NAME


# Backup files that contain user's settings for Cloudlinux Alternatives
CL_ALT_BACKUP_DIR = '.cl.selector'
CL_ALT_DEFAULTS = 'defaults.cfg'


# php_vers 'php5.4'
# php_modules = { 'php5.3' : ['dom', 'xmlreader'], 'php5.4' : ['dom', 'xsl'] }
# dirpath is something like '/var/cagefs/prefix/user/etc/cl.php.d/alt-php54'
# dirname is something like 'php54'
def enable_extensions_symlinks(php_vers, php_modules, dirpath, dirname):
    for mod in php_modules[php_vers]:
        link_name = mod + '.ini'
        link_path = os.path.join(dirpath, link_name)
        link_to = os.path.join('/opt/alt', dirname, 'etc', 'php.d.all', link_name)
        if not os.path.islink(link_path):
            remove_file_or_dir(link_path)
            try:
                os.symlink(link_to, link_path)
            except OSError as e:
                msg = f'Error: failed to create symlink {link_path} : {str(e).replace("Errno", "Err code")}'
                logger.error(msg, exc_info=e)
                logging(msg, SILENT_FLAG, 1)


deps_cache = {}

def get_dependencies(alt_dir):
    """
    Return dependencies of php modules (extensions), determined by parsing of ini files in specified directory
    :param alt_dir: path to directory where ini files are (something like '/opt/alt/php54/etc/php.d.all')
    :type alt_dir: string
    :return: something like { 'mailparse' : ['mbstring'], 'xsl' : ['dom'], 'xmlreader' : ['dom'] }
    :rtype: dict
    """
    global deps_cache
    if alt_dir not in deps_cache:
        deps = {}
        if os.path.isdir(alt_dir):
            for ini_file in os.listdir(alt_dir):
                ini_path = alt_dir + '/' + ini_file
                if ini_path.endswith('.ini') and os.path.isfile(ini_path):
                    extname = ini_file[:-len('.ini')]
                    deps[extname] = []
                    ini_file = read_file_cached(ini_path)
                    for line in ini_file:
                        line = line.rstrip()
                        if line.startswith('extension') or line.startswith('zend_extension'):
                            if line.endswith('"'):
                                line = line.replace('"', '')
                            elif line.endswith("'"):
                                line = line.replace("'", '')
                            ar = line.split('=', 1)
                            if (len(ar) == 2) and ar[1].endswith('.so'):
                                ext = os.path.basename(ar[1].lstrip())
                                ext = ext[:-len('.so')]
                                if extname != ext:
                                    deps[extname].append(ext)
        deps_cache[alt_dir] = deps
    return deps_cache[alt_dir]



# Build load order of modules using recursive algorithm
def get_load_order(load_order, deps, mod):
    if mod in deps:
        for dep in deps[mod]:
            get_load_order(load_order, deps, dep)
        if mod not in load_order:
            load_order.append(mod)



# Build load order of modules NOT using recursive algorithm
def get_load_order_not_recursive(load_order, deps, mod):
    if mod in deps:
        for dep in deps[mod]:
            if dep not in load_order and dep in deps:
                load_order.append(dep)
        if mod not in load_order:
            load_order.append(mod)



def build_load_order(php_vers, php_modules, ini_path, deps = None, quiet = False):
    """

    :param php_vers: something like 'php5.4'
    :type php_vers: string
    :param php_modules: { 'php5.3' : ['dom', 'xmlreader'], 'php5.4' : ['dom', 'xsl'] }
    :type php_modules: dict
    :param ini_path: path to directory where ini files are (something like '/opt/alt/php54/etc/php.d.all')
    :type ini_path: string
    """
    if deps is None:
        deps = get_dependencies(ini_path)
    for func in (get_load_order, get_load_order_not_recursive):
        load_order = []
        try:
            # ioncube loader should be first in the load order
            if 'ioncube_loader' in php_modules[php_vers]:
                func(load_order, deps, 'ioncube_loader')
            elif 'ioncube_loader_4' in php_modules[php_vers]:
                func(load_order, deps, 'ioncube_loader_4')
            for mod in php_modules[php_vers]:
                if mod not in ('ioncube_loader', 'ioncube_loader_4'):
                    func(load_order, deps, mod)
            break
        except RuntimeError:
            if not quiet:
                logging('Error: cyclic dependencies of PHP modules detected. Depth of dependencies will be limited to 1', SILENT_FLAG, 1)
    return load_order


php_ini_validator = None
bad_try_init_phpinivalidator_trigger = False


def read_custom_php_settings(homepath, filename, uid, gid, php_vers=None, user_name=None, alt_php_ini_file=None):
    global php_ini_validator
    global bad_try_init_phpinivalidator_trigger
    global validate_alt_php_ini
    if homepath is None:
        return None
    # Read backup of custom php settings in user's home directory
    backup_path = os.path.join(homepath, CL_ALT_BACKUP_DIR, filename)
    if not os.path.isfile(backup_path):
        return None
    php_ini_lines = read_file_secure(backup_path, uid, gid, exit_on_error = False)
    if validate_alt_php_ini:
        # Do PHP options validation
        if not bad_try_init_phpinivalidator_trigger and php_ini_validator is None:
            try:
                php_ini_validator = phpinivalidator.PHPINIvalidator(phpconf_path=PHP_CONF)
            except (OSError, IOError):
                bad_try_init_phpinivalidator_trigger = True
        if bad_try_init_phpinivalidator_trigger:
            return php_ini_lines
        if not php_vers:
            php_vers = phpinivalidator.get_php_ver(backup_path)
        alt_vers = get_alt_versions()
        output_lines_list = php_ini_validator.validate(input_phpini_lines=php_ini_lines, php_ver=alt_vers[php_vers])
        if php_ini_validator.unknown_options or php_ini_validator.invalid_values_options or php_ini_validator.invalid_options:
            if user_name:
                log_message = "User: " + user_name
            else:
                log_message = "User: Unknown"
            log_message += "; PHP version: " + php_vers + "\n                     Backup file: " + backup_path
            if alt_php_ini_file:
                log_message += "\n                     Destination file: " + alt_php_ini_file
            else:
                log_message += "\n                     Destination file: Unknown"
            php_options_log_write(log_message, php_ini_validator.unknown_options, php_ini_validator.invalid_values_options, php_ini_validator.invalid_options)
    else:
        # Pass PHP options validation
        output_lines_list = php_ini_lines
    return output_lines_list


def enable_extensions(php_vers, php_modules, dirpath, dirname, uid, gid, homepath=None, user_name=None):
    """
    Enable specified extensions for specific php version and user
    :param php_vers: php version, something like 'php5.4'
    :type php_vers: string
    :param php_modules: extesions enabled for different php version for the user specified like { 'php5.3' : ['dom', 'xmlreader'], 'php5.4' : ['dom', 'xsl'] }
    :type php_modules: dict
    :param dirpath: path where generated alt_php.ini file is written to (something like '/var/cagefs/prefix/user/etc/cl.php.d/alt-php54')
    :type dirpath: string
    :param dirname: name of directory for specified php version inside /opt/alt directory (something like 'php54')
    :type dirname: string
    :param uid: uid of user
    :type uid: int
    :param gid: gid of user
    :type gid: int
    :param homepath: path to home directory of user (something like '/home/user')
    :type homepath: string
    :param user_name: name of user
    :type user_name: string
    """
    ini_path = os.path.join('/opt/alt', dirname, 'etc', 'php.d.all')

    # Build load order of php modules
    # example:['bcmath', 'dom', 'gd', 'imap', 'json', 'mcrypt']
    load_order = build_load_order(php_vers, php_modules, ini_path)

    # Build content of alt_php.ini file
    alt_php_ini = []

    for module in load_order:
        alt_php_ini.append(';---'+module+'---\n')

        # module.ini path
        # example: /opt/alt/php54/etc/php.d.all/imap.ini
        module_ini_path = ini_path + '/' + module + '.ini'

        # settings per module.ini
        # example: ['; Enable imap extension module\n', 'extension=imap.so\n']
        module_ini = read_file_cached(module_ini_path)

        for line in module_ini:
            line = line.rstrip()
            # Do not add empty lines and comments to alt_php.ini file
            if line and not line.startswith(';'):
                line += '\n'
                if line not in alt_php_ini:
                    alt_php_ini.append(line)

        alt_php_ini.append('\n')

    # Write generated alt_php.ini file
    user_ini_path = os.path.join(dirpath, 'alt_php.ini')

    try:
        # custom users php settings from /home/user/.cl.selector/alt_phpX.X.cfg
        # example: [';---fileinfo---\n', ';extension=fileinfo.so\n', ';\n',
        # ';---phar---\n', ';extension=phar.so\n', ';\n']
        custom_php_settings = read_custom_php_settings(homepath,
                                                       'alt_'+dirname+'.cfg',
                                                       uid,
                                                       gid,
                                                       php_vers=php_vers[3:],
                                                       user_name=user_name,
                                                       alt_php_ini_file=user_ini_path)
    except (OSError, IOError):
        custom_php_settings = None

    if custom_php_settings is not None:
        alt_php_ini.extend(custom_php_settings)
        alt_php_ini.append('\n')

    write_file_secure(alt_php_ini, user_ini_path, uid, gid)


# userpath is something like '/var/cagefs/prefix/user/etc/cl.php.d' (directory should exist already)
# def_vers = destination php version
# cl_alt_def_modules = default set of php modules from global defaults
# php_modules = set of php modules from user's backup
# vers_changed = True if def_vers != def_vers_old (selected php version has been changed for the user)
# def_vers_old = previous (old) version of php
# force = True: reset selected php modules to global defaults (ignore user's backup)
# rebuild = True: rebuild (regenerate) alt_php.ini files
# user_name = None - user name for logging
def select_default_php_modules(userpath, homepath, uid, gid, def_vers, cl_alt_def_modules, php_modules,
                               vers_changed, def_vers_old, force, rebuild, user_name=None):
    alt_vers = get_alt_versions()

    if def_vers == None:
        def_vers = 'native'
    if def_vers_old == None:
        def_vers_old = 'native'
    if php_modules == None:
        php_modules = {}
    if cl_alt_def_modules == None:
        cl_alt_def_modules = {}

    real_userpath = os.path.realpath(userpath)

    modules_changed = False
    for vers in alt_vers:
        php_vers = 'php' + vers
        dirname = 'php' + vers.replace('.', '')
        altdir = 'alt-' + dirname
        dirpath = os.path.join(userpath, altdir)
        user_ini_path = os.path.join(dirpath, 'alt_php.ini')
        if (not os.path.lexists(user_ini_path)) or force or rebuild:
            if make_userdir(dirpath, 0o755, uid, gid, real_userpath):
                continue
            if (php_vers not in php_modules) or force:
                if php_vers in cl_alt_def_modules:
                    php_modules[php_vers] = cl_alt_def_modules[php_vers]
                else:
                    modules = get_php_modules()
                    php_modules[php_vers] = modules
                modules_changed = True

            # Create symlinks for php modules
            # enable_extensions_symlinks(php_vers, php_modules, dirpath, dirname)

            # Create alt_php.ini file for php modules
            enable_extensions(php_vers,
                              php_modules,
                              dirpath,
                              dirname,
                              uid,
                              gid,
                              homepath=homepath,
                              user_name=user_name)

        # else:
        # Actions commented out are not secure, because user is owner of the parent (/var/cagefs/prefix/user/etc/cl.php.d) directory
        #       make_dir(dirpath, 0755)
        #       set_owner(dirpath, uid, gid)
        #       set_perm(user_ini_path, 0644)
        #       set_owner(user_ini_path, uid, gid)

    if vers_changed:
        new_vers = def_vers
    else:
        new_vers = def_vers_old

    if vers_changed or modules_changed:
        # Save backup
        write_cl_alt_to_backup(homepath, new_vers, php_modules, uid, gid)


def read_cl_alt_backup_as_user(homepath, uid, gid):
    backup_path = os.path.join(homepath, CL_ALT_BACKUP_DIR, CL_ALT_DEFAULTS)
    set_user_perm(uid, gid)
    result = read_cl_alt_backup(backup_path)
    set_root_perm()
    return result


def read_cl_alt_backup(backup_path):
    try:
        backup_file = open_file_not_symlink(backup_path)
    except (OSError, IOError):
        return None, None, None, None

    cfg = configparser.ConfigParser(interpolation=None, strict=False)
    try:
        cfg.readfp(backup_file)
    except configparser.Error:
        # ignore invalid (corrupted) backup file
        return None, None, None, None

    try:
        def_vers = config_get_option_as_list(cfg, 'versions', 'php')[0]
    except IndexError:
        return None, None, None, None

    modules = {}
    php_state = {}
    other = {}
    for section in cfg.sections():
        dirname = section.replace('.', '')
        if dirname.startswith('php') and isdigits(dirname[len('php'):]):
            modules[section] = config_get_option_as_list(cfg, section, 'modules')
            if cfg.has_option(section, 'state') and (cfg.get(section, 'state').strip().lower().startswith('disable')):
                php_state[section[len('php'):]] = False
        elif section not in ('versions',):
            options = {}
            for option in cfg.options(section):
                options[option] = cfg.get(section, option)
            other[section] = options

    if ('phpnative' in other) and ('state' in other['phpnative']) and (other['phpnative']['state'].strip().lower().startswith('disable')):
        php_state['native'] = False

    return def_vers, modules, php_state, other




cl_alt_def_vers = None
cl_alt_def_modules = None
cl_alt_def_php_state = None
cl_alt_def_other = None


def read_cl_alt_defaults():
    global cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other
    if cl_alt_def_vers != None and cl_alt_def_modules != None and cl_alt_def_php_state != None and cl_alt_def_other != None:
        return cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other

    cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other = read_cl_alt_backup(os.path.join(ETC_CL_ALT_PATH, CL_ALT_DEFAULTS))
    return cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other


# homepath = '/home/user'
# def_vers = '5.1'
# modules = {'php5.1': ['json', 'curl'], 'php5.2': ['calendar', 'ctypes']}
def write_cl_alt_to_backup(homepath, def_vers, modules, uid, gid, state = None, other = None):
    if homepath == None:
        # write global defaults
        backup_path = os.path.join(ETC_CL_ALT_PATH, CL_ALT_DEFAULTS)
        drop_perm = False
    else:
        # write user's backup file
        backup_dir = os.path.join(homepath, CL_ALT_BACKUP_DIR)
        real_homepath = os.path.realpath(homepath)
        if make_userdir(backup_dir, 0o755, uid, gid, real_homepath):
            return
        backup_path = os.path.join(homepath, CL_ALT_BACKUP_DIR, CL_ALT_DEFAULTS)
        # Drop privileges
        drop_perm = True

    # Generate file content
    backup = []
    backup.append('[versions]\n')
    backup.append('php='+def_vers+'\n')
    if modules == None:
        modules = {}
    for section in modules:
        backup.append('\n['+section+']\n')
        backup.append('modules='+','.join(modules[section])+'\n')
        if state != None:
            vers = section[len('php'):]
            if (vers in state) and (not state[vers]):
                backup.append('state=disabled\n')
    if other != None:
        for section in other:
            backup.append('\n['+section+']\n')
            for option in other[section]:
                backup.append(option+'='+other[section][option]+'\n')

    # Write content to the file
    write_file_secure(backup, backup_path, uid, gid, drop_perm)



def get_mounted_dirs(all_cagefs_mounts=False, without_nosuid=False, rw_mounts_only=False, all_mounts=False):
    """
    Return list of mounts points
    :param all_cagefs_mounts: return CageFS mounts points only
    :param without_nosuid: return mount points without 'nosuid' attribute
    :param rw_mounts_only: return rw mount points only (i.e. mounts without 'ro' attribute)
    :param all_mounts: return all mount points
    """
    mounts_list = []
    with open("/proc/mounts", "r") as mounts:
        for line in mounts:
            if all_mounts or line.find('cagefs-etcfs') == -1 and line.find('cagefs-varfs') == -1:
                p = line.split()
                mountpoint = p[1]
                opts = p[3].split(',')
                if without_nosuid and 'nosuid' in opts:
                    continue
                if rw_mounts_only and 'ro' in opts:
                    continue
                if all_mounts or mountpoint.find(SKELETON+'/') != -1 \
                    or (all_cagefs_mounts and (mountpoint.find('/var/cagefs/') != -1 or mountpoint.find('/.cagefs/') != -1)):
                    mounts_list.append(mountpoint[mountpoint.find('/'):])
    return mounts_list


def get_homeN_dirs(use_glob=False):
    """
    Returns set of base home directories like {"/home0", "/home1", .., "/home9"} including "/home"
    """
    pattern = re.compile(r'(/home\d?)/')
    dirs = set()
    if use_glob:
        for path in glob.glob('/home*'):
            m = pattern.match(addslash(path))
            if m and os.path.isdir(path):
                dirs.add(m.group(1))
    else:
        pw = clpwd.get_user_dict()
        for user in pw:
            line = pw[user].pw_dir
            m = pattern.match(line)
            if m:
                dirs.add(m.group(1))
    return dirs



listdir_cache = {}


def cached_listdir(path):
    global listdir_cache
    path = stripslash(path)
    if path not in listdir_cache:
        try:
            sbuf = cached_lstat(path)
            if stat.S_ISDIR(sbuf.st_mode):
                res = listdir_cache[path] = os.listdir(path)
            else:
                res = listdir_cache[path] = []
        except (OSError, IOError):
            res = listdir_cache[path] = []
    else:
        res = listdir_cache[path]
    return res




CUSTOM_ETC = '/etc/cagefs/custom.etc/'



def get_custom_etc_list():
    return cached_listdir(CUSTOM_ETC)


def get_additional_etc_files_for_user(username: str,
                                      user_etc_path: str) -> Dict[str, int]:
    """
    Get additional files for a user
    to be placed within their '/etc' directory.

    This includes retrieving files added
    by the 'custom.etc' directory mechanism
    and mount points defined in the 'cagefs.mp' file.

    Args:
        username: The user's name
        user_etc_path: The user etc path, like '/var/cagefs/<prefix>/<username>/etc'
    """
    return {
        **get_custom_etc_files_for_user(username, user_etc_path),
        **get_etc_dirs_from_mounts_for_user(username, user_etc_path),
    }


def get_custom_etc_files_for_user(username: str,
                                  user_etc_path: str) -> Dict[str, int]:
    """
    Get a list of additional files for a user,
    which have been added to the user's '/etc' directory
    by utilizing the 'custom.etc' directory mechanism.

    Args:
        username: The user's name
        user_etc_path: The user etc path, like '/var/cagefs/<prefix>/<username>/etc'
    """
    etc_list = {}
    if username in get_custom_etc_list():
        path = CUSTOM_ETC + username
        try:
            sbuf = cached_lstat(path)
        except (OSError, IOError):
            return etc_list
        if stat.S_ISDIR(sbuf.st_mode):
            add_tree_to_list(path, etc_list, cut_path = path, add_path = user_etc_path)
    return etc_list


def get_etc_dirs_from_mounts_for_user(username: str,
                                      user_etc_path: str) -> Dict[str, int]:
    """
    Get a list of additional directories for a user,
    which have been added to the user's '/etc' by defining
    additional mount points within the 'cagefs.mp' file.

    Process only the mount points splitted by username or UID,
    as only these are mounted to the user's '/var/cagefs/.../etc',
    which is subsequently mounted to the skeleton's '/etc'.

    Args:
        username: The user's name
        user_etc_path: The user etc path, like '/var/cagefs/<prefix>/<username>/etc'
    """
    import cagefsctl
    etc_list = {}

    mp_config = cagefsctl.MountpointConfig(skip_errors=True, skip_cpanel_check=True)
    splitted_by_username_mounts = mp_config.splitted_by_username_mounts
    splitted_by_uid_mounts = mp_config.splitted_by_uid_mounts

    _process_etc_mounts(splitted_by_username_mounts, username, user_etc_path, etc_list)

    try:
        user_uid = str(clpwd.get_uid(username))
    except clpwd.NoSuchUserException:
        return etc_list

    _process_etc_mounts(splitted_by_uid_mounts, user_uid, user_etc_path, etc_list)

    return etc_list


def _process_etc_mounts(mounts: List[str], user_identifier: str,
                        user_etc_path: str, etc_list: Dict[str, int]) -> None:
    """
    Process mount points and construct a list of '/etc' ones
    and their respective user subdirectories.

    Retrieve a list of contents within the mount point,
    and if it contains the user's identifier
    (UID for mount points splitted by UIDs,
    or username for mount points splitted by usernames),
    add all the subdirectories to the resulting list.
    """
    for path in mounts:
        if not path.startswith('/etc/') or user_identifier not in cached_listdir(path):
            continue

        path = path.replace('/etc/', '', 1)
        parts = path.split('/')
        parts.append(user_identifier)
        current_path = user_etc_path
        for part in parts:
            current_path = os.path.join(current_path, part)
            etc_list[current_path] = 1


# user_etc_path = something like '/var/cagefs/prefix/user/etc'
def update_custom_etc_files_for_user(user, user_etc_path):
    if user in get_custom_etc_list():
        _dir = CUSTOM_ETC + user
        try:
            sbuf = cached_lstat(_dir)
        except (OSError, IOError):
            return
        if stat.S_ISDIR(sbuf.st_mode):
            for filename in os.listdir(_dir):
                if filename not in [ CL_ALT_NAME, CL_PHP_DIR_NAME, ETC_VERSION_NAME ]:
                    src = _dir + '/' + filename
                    dest = user_etc_path +'/' + filename
                    try:
                        sbuf = cached_lstat(src)
                    except (OSError, IOError) as e:
                        logging('Error: lstat() failed file ' + src + ' : ' + str(e), SILENT_FLAG, 1)
                        continue
                    if stat.S_ISDIR(sbuf.st_mode):
                        copytree(src, dest, update = True)
                    else:
                        copy_file(src, dest, update = True)




CUSTOM_ETC_LOG = '/usr/share/cagefs/custom.etc/'


def custom_etc_present():
    # Directory is not empty ?
    if cached_listdir(CUSTOM_ETC_LOG):
        return True
    return False



# list_of_files = list of paths without path to etc directory (like ['/hosts'])
def save_custom_etc_log(user, list_of_files):
    try:
        _ = cached_lstat(CUSTOM_ETC_LOG)
    except (OSError, IOError):
        umask_saved = os.umask(0)
        os.mkdir(CUSTOM_ETC_LOG, 0o700)
        os.umask(umask_saved)
    if list_of_files:
        write_file(CUSTOM_ETC_LOG+user, list_of_files, add_eol = True)
    else:
        try:
            os.unlink(CUSTOM_ETC_LOG+user)
        except (OSError, IOError):
            pass



# Returns list of custom etc files (or directories) that has been removed from /etc/cagefs/custom.etc/user directory
# list_of_files = list of paths without path to etc directory (like ['/hosts'])
def get_custom_etc_files_to_delete(user, list_of_files):
    res = []
    if user in cached_listdir(CUSTOM_ETC_LOG):
        old_list = read_file(CUSTOM_ETC_LOG+user)
        for path in old_list:
            path = path.rstrip()
            if path not in list_of_files:
                if not (path.startswith('/'+CL_ALT_NAME) or path.startswith('/'+CL_PHP_DIR_NAME) or path.startswith(ETC_VERSION)):
                    res.append(path)
        res.sort()
    return res



def cut_path(_list, path):
    plen = len(path)
    res = set()
    for p in _list:
        res.add(p[plen:])
    return res



def is_path_secure(path):
    try:
        sbuf = os.lstat(path)
    except OSError:
        return False

    if not stat.S_ISREG(sbuf.st_mode):
        return False

    return not ( (sbuf.st_uid != 0) or (sbuf.st_gid != 0) or (sbuf.st_mode & stat.S_IWOTH) )



cagefs_ini_cfg = None


def read_cagefs_ini():
    global cagefs_ini_cfg
    if cagefs_ini_cfg is not None:
        return

    if (not os.path.isfile(CAGEFS_INI)) or (not is_path_secure(CAGEFS_INI)):
        cagefs_ini_cfg = None
        return

    cagefs_ini_cfg = configparser.ConfigParser(interpolation=None, strict=False)
    try:
        cagefs_ini_cfg.read(CAGEFS_INI)
    except configparser.Error:
        cagefs_ini_cfg = None



def get_update_period():
    seconds_in_24h = 60*60*24

    read_cagefs_ini()
    if cagefs_ini_cfg is None:
        return seconds_in_24h

    res = config_get_option_as_list(cagefs_ini_cfg, 'common', 'update_period_days')
    try:
        days = int(res[0])
        if days < 0:
            days = 1
    except (ValueError, IndexError):
        days = 1
    return seconds_in_24h * days



def set_cagefs_ini_option(section, option, value):
    global cagefs_ini_cfg
    read_cagefs_ini()
    if cagefs_ini_cfg is None:
        cagefs_ini_cfg = configparser.ConfigParser(interpolation=None, strict=False)
    if not cagefs_ini_cfg.has_section(section):
        cagefs_ini_cfg.add_section(section)
    cagefs_ini_cfg.set(section, option, value)
    ini_file = open(CAGEFS_INI, 'w')
    cagefs_ini_cfg.write(ini_file)
    ini_file.close()
    set_owner(CAGEFS_INI, 0, 0)
    set_perm(CAGEFS_INI, 0o600)



def set_update_period(days):
    set_cagefs_ini_option('common', 'update_period_days', str(days))



# Reads parameters for tmpwatch from config file
def get_tmpwatch_params():
    is_ubuntu = os.path.isfile('/opt/alt/tmpreaper/usr/sbin/tmpreaper')
    TMPWATCH = '/opt/alt/tmpreaper/usr/sbin/tmpreaper 720' if is_ubuntu else '/usr/sbin/tmpwatch -umclq 720'
    read_cagefs_ini()
    if cagefs_ini_cfg is None:
        return TMPWATCH

    if cagefs_ini_cfg.has_option('common', 'tmpwatch'):
        return cagefs_ini_cfg.get('common', 'tmpwatch')

    return TMPWATCH



def set_tmpwatch_params(params_str):
    set_cagefs_ini_option('common', 'tmpwatch', params_str)



def get_tmpwatch_dirs():
    read_cagefs_ini()
    if cagefs_ini_cfg is None:
        return []
    return config_get_option_as_list(cagefs_ini_cfg, 'common', 'tmpwatch_dirs')



LAST_UPDATE_TIME = '/usr/share/cagefs/last_update_time.txt'


def save_last_update_time():
    write_file(LAST_UPDATE_TIME, [str(int(time.time()))], add_eol = True)
    os.chmod(LAST_UPDATE_TIME, 0o644)


def read_last_update_time():
    if os.path.isfile(LAST_UPDATE_TIME):
        content = read_file(LAST_UPDATE_TIME)
        try:
            return int(content[0].strip())
        except (ValueError, IndexError):
            pass
    return 0



def update_of_cagefs_skeleton_is_needed():
    update_period = get_update_period()
    if update_period == 0:
        return True
    last_update = read_last_update_time()
    current_time = int(time.time())
    return current_time >= (last_update + update_period)



# Compare directories or files
# Returns True if they are equal, False otherwise
# This function does NOT follow symlinks
# It compares values of symlinks via readlink()
# Unless shallow is given and is false, files with identical os.stat()
# signatures are taken to be equal (st_atime is ignored in comparison).
def are_dirs_equal(dir1, dir2, shallow = True):
    sbuf1 = oslstat(dir1)
    if not sbuf1:
        return False
    sbuf2 = oslstat(dir2)
    if not sbuf1:
        return False

    if stat.S_ISDIR(sbuf1.st_mode):
        if not stat.S_ISDIR(sbuf2.st_mode):
            return False
        listdir1 = os.listdir(dir1)
        listdir1.sort()
        listdir2 = os.listdir(dir2)
        listdir2.sort()
        if listdir1 != listdir2:
            return False
        for name in listdir1:
            path1 = dir1 + '/' + name
            path2 = dir2 + '/' + name
            if not are_dirs_equal(path1, path2, shallow):
                return False
        return True
    elif stat.S_ISDIR(sbuf2.st_mode):
        return False

    if shallow or stat.S_ISLNK(sbuf1.st_mode) or stat.S_ISLNK(sbuf2.st_mode):
        # Compare values of symlinks or metadata of files
        return is_same_metadata(dir1, dir2, sbA = sbuf1, sbB = sbuf2)
    else:
        # Compare content of files
        return filecmp.cmp(dir1, dir2, shallow = False)


def clean_dir_from_old_session_files(dir_path, max_lifetime):
    """
    Clean directories from old files
    :param dir_path: Dir path to clean
    :param max_lifetime: Max lifetime for clean
    :return: None
    """
    sessions = glob.glob(os.path.join(dir_path, "sess_[a-z0-9]*"))
    cur_time = time.time()
    for sess in sessions:
        try:
            s = os.stat(sess)
            ctime = s.st_ctime
            if cur_time - ctime > max_lifetime:
                os.unlink(sess)
        except OSError:
            pass


def get_opts_from_php_ini(path, default_time, default_path='/tmp'):
    """
    Read php.ini and returns session.save_path and session.gc_maxlifitime options
    :param str path: Path to ini file
    :param int default_time: Return that time when can not get value from config
    :param str default_path: Return that path when can not get value from config
    :return: Tuple (session.save_path, session.gc_maxlifitime)
    :rtype: (str, int)
    """
    try:
        with open(path, "r") as config:
            for line in config.readlines():
                l = line.strip()
                if l.startswith(';'):
                    continue
                elif l.startswith("session.save_path") and "=" in l:
                    default_path = (l.split("=")[1]).strip()
                elif l.startswith("session.gc_maxlifetime") and "=" in l:
                    default_time = int(l.split("=")[1])
    except (IndexError, ValueError, IOError):
        pass
    return default_path.strip("\"'"), default_time


def get_relative_path(original, dest):
    """
    Convert symlink value (path) from absolute to relative
    :param original: path to original file
    :param dest: path where symlink will be created
    """
    if original.startswith('/'):
        return os.path.relpath(strip_path(original), strip_path(os.path.dirname(dest)))
    return original


def relative_symlink(original, dest):
    """
    Create relative symlink instead of absolute
    :param original: path to original file
    :param dest: path where symlink will be created
    """
    relative_path = get_relative_path(original, dest)
    os.symlink(relative_path, dest)


def update_symlink_in_skeleton(origpath, jailpath):
    """
    Create symlink or update if changed. Return value of original symlink (destination it points to)
    :param origpath: path to symlink in real file system
    :param jailpath: path to symlink in cagefs-skeleton
    """
    realfile = os.readlink(origpath)
    try:
        relative_path = get_relative_path(realfile, jailpath)
        if os.path.islink(jailpath):
            if os.readlink(jailpath) != relative_path:
                os.unlink(jailpath)
        else:
            remove_file_or_dir(jailpath, check_mounts=True)
        if not os.path.islink(jailpath):
            logging(f'Creating symlink {jailpath} to {relative_path}', SILENT_FLAG, 1)
            os.symlink(relative_path, jailpath)
    except OSError as e:
        logging(f'Failed to create symlink {jailpath} to {relative_path}: {str(e)}', SILENT_FLAG, 1)
    return realfile


def create_utmp_in_skeleton():
    """
    Create symlink /usr/share/cagefs-skeleton/var/run/utmp -> /var/run/cagefs/utmp
    needed for emulation of /var/run/utmp inside CageFS
    For details see CAG-706
    """
    if not os.path.isdir(SKELETON):
        return
    utmp_cagefs = VAR_RUN_CAGEFS + '/utmp'
    skel_cagefs_dir = SKELETON + VAR_RUN_CAGEFS
    skel_utmp = SKELETON + '/var/run/utmp'
    try:
        if not os.path.isdir(skel_cagefs_dir):
            mod_makedirs(skel_cagefs_dir, 0o755)
    except OSError as e:
        logging('Error: failed to create directory ' + skel_cagefs_dir + ' : ' + str(e), SILENT_FLAG, 1)
    try:
        if not os.path.islink(skel_utmp):
            os.symlink(utmp_cagefs, skel_utmp)
    except OSError as e:
        logging('Error: failed to create symlink ' + skel_utmp + ' -> ' + utmp_cagefs + ' : ' + str(e), SILENT_FLAG, 1)


def create_utmp_for_user(user, exit_on_error=True):
    """
    Create user's personal /home/user/.cagefs/var/run/cagefs/utmp file
    For details see CAG-706
    :param user: user name
    :type user: string
    :param exit_on_error: True == execute sys.exit(1) when error has occured
    :type exit_on_error: bool
    """
    try:
        pw = clpwd.get_pw_by_name(user)
    except ClPwd.NoSuchUserException:
        return
    utmp_dir = pw.pw_dir + '/.cagefs' + VAR_RUN_CAGEFS
    utmp_file = utmp_dir + '/utmp'
    if not os.path.lexists(utmp_file):
        set_user_perm(pw.pw_uid, pw.pw_gid)
        try:
            if not os.path.isdir(utmp_dir):
                clcaptain.mkdir(utmp_dir, 0o700, recursive=True)
            clcaptain.write(utmp_file, '')
        except (OSError, IOError, ExternalProgramFailed):
            print_exception()
            if exit_on_error:
                sys.exit(1)
        set_root_perm()


def is_clean_user_php_sessions_enabled():
    """
    Check clean_php_sessions parameter in config file
    By default sessions cleanup is enabled
    """
    if not get_boolean_param(CL_CONFIG_FILE, 'clean_user_php_sessions', default_val=True):
        return False
    return True


@functools.lru_cache(maxsize=None)
def is_running_without_lve():
    return not is_panel_feature_supported(Feature.LVE)

Zerion Mini Shell 1.0