# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2024 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT
import functools
import logging
import os
import pwd
import sys
from typing import TypeAlias
import yaml
import secureio
from clcagefslib.const import BASEDIR, ETC_CL_PHP_PATH, ETC_CL_ALT_PATH, ETC_CL_ALT_CAGEFS_PATH, SYMLINKS
from clcagefslib.io import make_userdir, switch_symlink
from clcagefslib.fs import get_user_prefix
from clcagefslib.selector.paths import get_alt_dirs
from clcagefslib.webisolation.jail_utils import full_website_path, get_website_id
from clcommon import clcaptain, clconfpars, clcagefs
from clcommon.utils import ExternalProgramFailed
IsError: TypeAlias = bool
@functools.cache
def is_ea4_enabled() -> bool:
"""
Return True if cPanel EasyApache4 (MultiPHP feature) is enabled
"""
return os.path.lexists('/etc/cpanel/ea4/is_ea4')
@functools.cache
def read_cpanel_ea4_php_conf() -> dict[str, str] | None:
"""
Read /etc/cpanel/ea4/php.conf
return something like {'default': 'ea-php54', 'ea-php56': 'suphp', 'ea-php54': 'cgi', 'ea-php55': 'suphp'}
return None if error has occured
"""
try:
with open('/etc/cpanel/ea4/php.conf', 'r') as f:
# conf = {'default': 'ea-php54', 'ea-php56': 'suphp', 'ea-php54': 'cgi', 'ea-php55': 'suphp'}
return yaml.load(f, yaml.SafeLoader)
except (yaml.YAMLError, IOError):
return None
def multiphp_system_default_is_ea_php() -> bool:
"""
Return True when default system php version selected via MultiPHP Manager in cPanel WHM is ea-php (not alt-php)
For details see CAG-774
"""
if is_ea4_enabled():
conf = read_cpanel_ea4_php_conf()
if conf:
try:
return conf['default'].startswith('ea-php')
except KeyError:
pass
return True
@functools.cache
def selector_modules_must_be_used():
"""
Return True if modules selected via PHP Selector (alt_php.ini) must be always used.
Never use modules selected in cPanel MultiPHP Manager.
See CAG-511 for details
"""
symlinks_rules_path = f'{ETC_CL_ALT_PATH}/symlinks.rules'
if clcagefs.in_cagefs():
symlinks_rules_path = f'{ETC_CL_ALT_CAGEFS_PATH}/symlinks.rules'
syml_rules = clconfpars.load_once(symlinks_rules_path, ignore_errors=True)
try:
return syml_rules['php.d.location'].lower() == 'selector'
except KeyError:
return False
# configure alt php - create .cagefs dir and create symlink
def configure_alt_php(pw: pwd.struct_passwd, php_version: str,
document_root: str | None = None,
write_log: bool = True,
drop_perm: bool = True,
force: bool = True,
configure_multiphp: bool = True) -> IsError:
"""
Create .cagefs directory in home directory of an user (if that dir does not exist),
and create symlinks to modules for alt-php
For details see CAG-447
Also switch symlinks that are used for integration with cPanel MultiPHP
For details please see CAG-445
drop_perm should be True when called as root, otherwise drop_perm should be False
Returns True if error has occured
:param pw: password file entry for an user
:param php_version: alt-php version selected for an user (for example 'native' or '5.6')
:param document_root: document root of the website (used for website isolation support)
:param write_log: write error messages to log or not
:param force: recreate symlinks even when they exist
"""
website_id = ""
if document_root:
website_id = get_website_id(document_root)
# create /home/user/.cagefs directory if it does not exist, set permissions/owner otherwise
real_homepath = os.path.realpath(pw.pw_dir)
path = os.path.join(pw.pw_dir, '.cagefs')
if website_id:
path = os.path.join(pw.pw_dir, '.cagefs', "websites", website_id)
if drop_perm:
if make_userdir(path, 0o771, pw.pw_uid, pw.pw_gid, real_homepath):
return True
elif not os.path.lexists(path):
try:
clcaptain.mkdir(path, 0o771)
except (OSError, ExternalProgramFailed) as e:
msg = f'Error: failed to create directory {path} : {str(e).replace("Errno", "Err code")}'
logging.error(msg, exc_info=e)
print(msg, file=sys.stderr)
return True
if drop_perm:
# drop privileges (switch to user)
secureio.set_user_perm(pw.pw_uid, pw.pw_gid)
error = _switch_symlink_for_alt_php_ini(php_version, pw, website_id, write_log, force)
if configure_multiphp:
error = _switch_symlink_for_cpanel_multi_php(pw, php_version, website_id, write_log, force) or error
if drop_perm:
# restore root privileges
secureio.set_root_perm()
return error
def _switch_symlink_for_alt_php_ini(
php_version: str,
pw: pwd.struct_passwd,
website_id: str = "",
write_log: bool = True,
force: bool = True) -> IsError:
"""
Switch symlink so it will point to directory with modules for alt-php
For details see CAG-447
Returns True if error has occured
Should be called as user (not root)!
:param php_version: alt-php version selected for an user (for example 'native' or '5.6')
:param pw: password file entry for an user
:param force: recreate symlinks even when they exist
"""
def _switch_symlink_for_dir(php_dir, base_path, website_id=""):
# create path to link, like /home/$USER/.cagefs/websites/