Current File : //proc/thread-self/root/opt/alt/python37/lib/python3.7/site-packages/clwpos/data_collector_utils.py
# -*- coding: utf-8 -*-

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

from __future__ import absolute_import

import os
import re
import subprocess
from functools import lru_cache
from pathlib import Path
from typing import List

from clcommon.clwpos_lib import find_wp_paths, get_wp_cache_plugin
from clcommon.cpapi import userdomains, get_main_username_by_uid
from clwpos.cl_wpos_exceptions import WposError, PhpBrokenException
from clwpos.daemon import WposDaemon
from clwpos.logsetup import setup_logging
from clwpos.utils import (
    daemon_communicate,
    PHP,
    run_in_cagefs_if_needed,
    _get_data_from_info_json
)

_logger = setup_logging(__name__)


def _get_doc_roots_info() -> dict:
    user = get_main_username_by_uid(os.geteuid())
    result = {}
    for domain, doc_root in userdomains(user):
        result.setdefault(doc_root, []).append(domain)

    return result


def has_wps(username: str) -> bool:
    """Check if user has at least one WordPress installation"""
    _info = userdomains(username)
    # all docroots (like it is done in `_add_wp_path_info`)
    excludes = list(i[1] for i in _info)
    for domain, doc_root in _info:
        wps = list(find_wp_paths(doc_root, excludes=excludes))
        # return immediately once first WP installation found
        if len(wps):
            return True
    return False


def _add_wp_path_info(user_info: dict) -> dict:
    wp_paths = {}
    for doc_root, domains in user_info.items():
        # excludes only affects subpaths of doc_root
        excludes = list(user_info)
        item = {
            "domains": domains,
            "wp_paths": list(find_wp_paths(doc_root, excludes=excludes))
        }
        wp_paths[doc_root] = item
    return wp_paths


def _wp_info(doc_root: str, wp_path: str) -> dict:
    """Convert WP path to {"path": str, "version": str}"""
    absolute_wp_path = Path(doc_root, wp_path)
    version_file = list(absolute_wp_path.glob("wp-includes/version.php"))[0]
    result = subprocess.run(["/bin/grep", "-Po", "(?<=wp_version = ')[^']+", version_file], capture_output=True)
    wp_version = result.stdout.strip().decode()
    return {
        "path": wp_path,
        "version": wp_version,
    }


def _add_wp_info(user_info: dict) -> dict:
    for doc_root, doc_root_info in user_info.items():
        wp_paths = doc_root_info.pop("wp_paths")
        doc_root_info["wps"] = [_wp_info(doc_root, wp_path) for wp_path in wp_paths]
    return user_info


@lru_cache(maxsize=None)
def _php_get_vhost_versions(uid):
    """
    uid param is used for caching each distinct uid
    @return: [
        {
            "account": "rm3",
            "documentroot": "/home/example/public_html",
            "version": "ea-php72",
            "handler": "php-fpm",
            "vhost": "otherchars.rm3.tld"
        }
    ]
    """
    try:
        return daemon_communicate({"command": WposDaemon.DAEMON_PHP_GET_VHOST_VERSIONS_COMMAND})["data"]
    except WposError:
        return _get_data_from_info_json("vhost_versions")


def php_info():
    """
    Returns php info, example:
    [{'vhost': 'sub.wposuser.com', 'account': 'stackoverflow',
    'version': 'ea-php80', 'handler': 'php-fpm',
    'documentroot': '/home/stackoverflow/public_html'},
    ...................................................................]
    """
    result = _php_get_vhost_versions(os.geteuid())
    for elem in result:
        elem["version"] = _normalized_php_version(PHP(elem["version"]))
    return result


def _add_php(user_info: dict) -> dict:
    """
    Updates user_info dict with php data
    """
    result = php_info()
    for item in result:
        user_info[item["documentroot"]]["php"] = {
            "version": item["version"],
            "handler": item["handler"]
        }

    return user_info


def _add_object_cache_info(user_info: dict) -> dict:
    """
    Search for 'object-cache.php' files in 'wp-content/plugins' directory
    in order to find what plugin is being used for object caching.
    """
    for doc_root, doc_root_info in user_info.items():
        for wp in doc_root_info["wps"]:
            plugin = get_wp_cache_plugin(Path(doc_root).joinpath(wp["path"]), "object-cache")
            wp["object_cache"] = plugin

    return user_info


def get_user_info() -> dict:
    """
    Collect info about user.
    @return {
        '/home/user/public_html': {
            'domains': ['domain.com'],
            'wps': [
                {
                    'path': 'wp_path_1',
                    'version': '5.7.2',
                    'object_cache': 'redis-cache'
                }
            ],
            'php': {
                'version': 'ea-php74',
                'handler': 'cgi',
                'redis_extension': False
            }
        }
    }
    """
    user_info = _get_doc_roots_info()

    for func in (_add_wp_path_info, _add_wp_info, _add_php):
        user_info = func(user_info)

    return user_info


def _normalized_php_version(version: PHP) -> PHP:
    """
    PHP selector can replace path with symlink. It's a reason why we need normalization.
    """
    command = f"{version.bin()} -i " \
              f" | /bin/grep 'Loaded Configuration File'"
    result = run_in_cagefs_if_needed(command, shell=True, executable='/bin/bash', env={})

    # example:
    # 'Loaded Configuration File => /opt/cpanel/ea-php74/root/etc/php.ini'
    if result.stderr and not result.stdout:
        raise PhpBrokenException(str(version.bin()), result.stderr)

    # check for alt version and replace if found
    alt_pattern = re.compile(r"alt.*php[^/]*/")
    captured_version = alt_pattern.search(result.stdout)
    if captured_version:
        return PHP(captured_version[0].strip("/").replace("/", "-"))

    return version


def _php_get_installed_versions():
    """
    @return: [
        "ea-php74"
    ]
    """
    try:
        return daemon_communicate({"command": WposDaemon.DAEMON_PHP_GET_INSTALLED_VERSIONS_COMMAND})["data"]
    except WposError:
        return _get_data_from_info_json("installed_versions")


@lru_cache(maxsize=None)
def get_cached_php_installed_versions() -> List[PHP]:
    """
    List all installed php version on the system
    :return: installed php version
    """
    result = _php_get_installed_versions()
    return [PHP(version) for version in result]