Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion functions
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,78 @@ function setup_php_extensions() {
fi
}

# Functions ee calls unguarded that hardened php.ini files (e.g. HestiaCP's CLI one) often list in disable_functions.
EE_PHP_REQUIRED_FUNCTIONS="curl_exec disk_free_space escapeshellarg exec getmypid ini_set pcntl_signal pcntl_signal_dispatch php_uname posix_getegid posix_geteuid posix_getpwuid proc_close proc_open"

function check_php_functions() {
ee_log_info1 "Checking required PHP functions"
if ! command -v php >/dev/null 2>&1; then
return 0
fi

local report kind name disabled="" missing="" degraded="" ini_files="" scan_dir="" where
# PHP 7 only warns when a disabled function is called, so disabled pcntl_* just loses rollback on signals there.
# shellcheck disable=SC2016,SC2086 # PHP code, not shell; one argument per function
if ! report="$(php -r '
$off = function_exists("ini_get") ? preg_split("/[\s,]+/", (string) ini_get("disable_functions")) : array();
foreach (array_slice($argv, 1) as $f) {
if (function_exists($f)) { continue; }
if (!in_array($f, $off, true)) { echo "missing ", $f, "\n"; }
elseif (PHP_MAJOR_VERSION < 8 && strpos($f, "pcntl_") === 0) { echo "degraded ", $f, "\n"; }
else { echo "disabled ", $f, "\n"; }
}
if (function_exists("php_ini_loaded_file") && function_exists("php_ini_scanned_files") && function_exists("file_get_contents")) {
$files = array_merge(array((string) php_ini_loaded_file()), explode(",", (string) php_ini_scanned_files()));
foreach (array_filter(array_map("trim", $files)) as $file) {
if (preg_match("/^[ \t]*disable_functions[ \t]*=[ \t]*[\"\x27]?[ \t]*[A-Za-z_]/m", (string) @file_get_contents($file))) { echo "ini ", $file, "\n"; }
}
}
echo "scandir ", PHP_CONFIG_FILE_SCAN_DIR, "\n";' -- $EE_PHP_REQUIRED_FUNCTIONS)"; then
ee_log_fail "ERROR: Could not run the PHP CLI ($(command -v php)) to check the functions EasyEngine needs."
fi

while read -r kind name; do
case "$kind" in
disabled) disabled+="${disabled:+, }$name" ;;
missing) missing+="${missing:+, }$name" ;;
degraded) degraded+="${degraded:+, }$name" ;;
ini) ini_files+="${ini_files:+, }$name" ;;
scandir) scan_dir="$name" ;;
esac
done <<< "$report"

if [ -n "$degraded" ]; then
ee_log_warn "The PHP CLI disables ${degraded}; EasyEngine can't roll back a site operation interrupted by a signal (on PHP 8 this is a fatal error)."
fi
if [ -z "$disabled" ] && [ -z "$missing" ]; then
return 0
fi

if [ -n "$disabled" ]; then
echo "ERROR: The PHP CLI ($(command -v php)) disables functions EasyEngine needs: ${disabled}." >&2
{
if [ -n "$ini_files" ]; then
ee_log_verbose "disable_functions is set in: ${ini_files}"
else
ee_log_verbose "disable_functions is set in one of the files that php --ini lists."
fi
# A conf.d override only wins over files that load before it, so not when conf.d itself disables them.
if [ -n "$scan_dir" ] && [[ ", $ini_files" != *", $scan_dir/"* ]]; then
ee_log_verbose "To fix it, create ${scan_dir}/99-easyengine.ini that sets disable_functions to the same list without these functions, or edit php.ini directly."
else
if [ -n "$ini_files" ]; then where="the file(s) above"; else where="that file"; fi
ee_log_verbose "To fix it, remove them from disable_functions in ${where}."
fi
ee_log_verbose "Only the PHP CLI configuration needs the change, not PHP-FPM. Then re-run the installer."
} >&2
fi
if [ -n "$missing" ]; then
echo "ERROR: The PHP CLI ($(command -v php)) lacks functions EasyEngine needs: ${missing}." >&2
ee_log_verbose "To fix it, install or enable the PHP extensions that provide them (e.g. pcntl, posix, curl) for the PHP CLI. Then re-run the installer." >&2
fi
exit 1
}

function create_swap() {
ee_log_info2 "Enabling 1GiB swap"
EE_SWAP_FILE="/ee-swapfile"
Expand Down Expand Up @@ -467,9 +539,11 @@ function check_dependencies() {
fi

setup_host_dependencies
setup_docker
setup_php
setup_php_extensions
# Before Docker, so a refused host is left without it.
check_php_functions
setup_docker
check_swap
}

Expand Down
6 changes: 3 additions & 3 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ bounded **before** the phar download/pull and stubs `docker`, exercising only th
OS-version–dependent surface that has actually regressed in the past:

```
check_dependencies → setup_host_dependencies → setup_php → setup_php_extensions
check_dependencies → setup_host_dependencies → setup_php → setup_php_extensions → check_php_functions
```

PHP selection in particular can only be validated against real distro repos +
Expand Down Expand Up @@ -74,8 +74,8 @@ functions to drive the algorithm through every branch deterministically.
- `ee_select_and_install_php` — native vs PPA, version-first ordering, no-candidate.
- `download_and_install_easyengine` — checksum match / mismatch / empty phar /
empty checksum / download failure.
- Host-prep integration — php + the three required extensions install across
Ubuntu 22.04/24.04/26.04/rolling/devel and Debian 12/13.
- `check_php_functions` — functions disabled via `disable_functions` (comma- or space-separated) or missing from the build, PHP 7's warn-only pcntl case, the messages, and its place in `check_dependencies` (after PHP setup, before Docker).
- Host-prep integration — php + the three required extensions install across Ubuntu 22.04/24.04/26.04/rolling/devel and Debian 12/13, the functions ee calls are usable, and `check_php_functions` refuses a HestiaCP-style CLI `disable_functions`.

## Not yet covered (candidate follow-ups)

Expand Down
21 changes: 21 additions & 0 deletions tests/integration/host-prep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ for ext in curl sqlite3 zip; do
check "php extension loaded: $ext" bash -c "php -m | grep -qiE '^${ext}\$'"
done

# The functions ee calls are usable on a stock distro PHP; check_php_functions exits on failure, hence the subshell.
in_subshell() { ( "$@" ); }
check "required PHP functions are available" in_subshell check_php_functions

# A HestiaCP-style CLI disable_functions must be refused with the file named; a scan-dir file overrides php.ini.
scan_dir="$(php -r 'echo PHP_CONFIG_FILE_SCAN_DIR;')"
if [ -n "$scan_dir" ] && [ -d "$scan_dir" ]; then
hardened_ini="$scan_dir/99-ee-test-hardening.ini"
echo "disable_functions = pcntl_alarm,pcntl_fork,pcntl_signal,pcntl_signal_dispatch" > "$hardened_ini"
hardened_out="$( (check_php_functions) 2>&1 )"
hardened_rc=$?
rm -f "$hardened_ini"
check "disabled PHP functions are refused" test "$hardened_rc" -ne 0
check "the error names the disabled functions" grep -qF "disables functions EasyEngine needs: pcntl_signal, pcntl_signal_dispatch." <<< "$hardened_out"
check "the error names the ini file" grep -qF "$hardened_ini" <<< "$hardened_out"
check "the error says to edit that file" grep -qF "remove them from disable_functions in the file(s) above." <<< "$hardened_out"
check "the error is at most 4 lines" test "$(sed -n '/^ERROR: /,$p' <<< "$hardened_out" | wc -l)" -le 4
else
fail "PHP CLI has no config scan dir to test disable_functions with"
fi

# Base host dependency installed earlier in check_dependencies().
if ! command -v sqlite3 >/dev/null 2>&1; then
apt-get install -y sqlite3 >/dev/null 2>&1 || true
Expand Down
144 changes: 144 additions & 0 deletions tests/unit/php_functions.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env bats
#
# check_php_functions() — refuses a PHP CLI that disables or lacks functions ee calls (e.g. HestiaCP's pcntl_* list), which otherwise only fails on the first `ee site` command.

setup() {
source "${BATS_TEST_DIRNAME}/../helpers/common.bash"
# Resolve a real php before the stub dir shadows it (empty when none).
REAL_PHP="$(command -v php || true)"
export REAL_PHP
stub_init
export EE_QUIET_OUTPUT=""
load_functions
}

teardown() {
stub_cleanup
}

# php stub that prints the report lines from $PHP_REPORT and exits $PHP_EXIT.
stub_php_report() {
make_stub php <<'EOF'
#!/usr/bin/env bash
printf '%b' "${PHP_REPORT:-}"
exit "${PHP_EXIT:-0}"
EOF
}

# The error, from its ERROR: line on, spans exactly $1 lines.
assert_error_lines() {
local n
n="$(sed -n '/^ERROR: /,$p' <<< "$output" | wc -l)"
[ "$n" -eq "$1" ]
}

@test "passes when every required function is available" {
stub_php_report
PHP_REPORT="" run check_php_functions
[ "$status" -eq 0 ]
[[ "$output" != *ERROR* ]]
}

@test "passes when only an unrelated ini line is reported" {
stub_php_report
PHP_REPORT='ini /etc/php/8.3/cli/php.ini\n' run check_php_functions
[ "$status" -eq 0 ]
}

@test "fails and names disabled functions, the ini file and a conf.d override" {
stub_php_report
PHP_REPORT='disabled pcntl_signal\ndisabled pcntl_signal_dispatch\nini /etc/php/8.3/cli/php.ini\nscandir /etc/php/8.3/cli/conf.d\n' run check_php_functions
[ "$status" -eq 1 ]
[[ "$output" == *"ERROR: The PHP CLI ("*") disables functions EasyEngine needs: pcntl_signal, pcntl_signal_dispatch."* ]]
[[ "$output" == *" disable_functions is set in: /etc/php/8.3/cli/php.ini"* ]]
[[ "$output" == *"create /etc/php/8.3/cli/conf.d/99-easyengine.ini that sets disable_functions to the same list without these functions, or edit php.ini directly."* ]]
[[ "$output" == *"Only the PHP CLI configuration needs the change, not PHP-FPM. Then re-run the installer."* ]]
assert_error_lines 4
}

@test "suggests editing the files when a conf.d file disables the functions" {
stub_php_report
PHP_REPORT='disabled exec\nini /etc/php/8.3/cli/php.ini\nini /etc/php/8.3/cli/conf.d/99-zz-hardening.ini\nscandir /etc/php/8.3/cli/conf.d\n' run check_php_functions
[ "$status" -eq 1 ]
[[ "$output" == *"disable_functions is set in: /etc/php/8.3/cli/php.ini, /etc/php/8.3/cli/conf.d/99-zz-hardening.ini"* ]]
[[ "$output" == *"remove them from disable_functions in the file(s) above."* ]]
[[ "$output" != *99-easyengine.ini* ]]
assert_error_lines 4
}

@test "only warns about pcntl functions PHP 7 disables" {
stub_php_report
PHP_REPORT='degraded pcntl_signal\n' run check_php_functions
[ "$status" -eq 0 ]
[[ "$output" == *"disables pcntl_signal; EasyEngine can't roll back"* ]]
[[ "$output" != *ERROR* ]]
}

@test "points at php --ini when no file could be identified" {
stub_php_report
PHP_REPORT='disabled exec\n' run check_php_functions
[ "$status" -eq 1 ]
[[ "$output" == *"disable_functions is set in one of the files that php --ini lists."* ]]
[[ "$output" == *"remove them from disable_functions in that file."* ]]
assert_error_lines 4
}

@test "fails and names functions missing from the PHP build" {
stub_php_report
PHP_REPORT='missing pcntl_signal\n' run check_php_functions
[ "$status" -eq 1 ]
[[ "$output" == *"lacks functions EasyEngine needs: pcntl_signal."* ]]
[[ "$output" == *"To fix it, install or enable the PHP extensions that provide them"* ]]
[[ "$output" == *"Then re-run the installer."* ]]
[[ "$output" != *"disables functions"* ]]
assert_error_lines 2
}

@test "fails when the PHP CLI cannot run" {
stub_php_report
PHP_EXIT=255 run check_php_functions
[ "$status" -eq 1 ]
[[ "$output" == *"Could not run the PHP CLI"* ]]
}

@test "skips the check when no php is installed" {
PATH="$STUB_DIR:/usr/bin/missing" run check_php_functions
[ "$status" -eq 0 ]
}

# The rest run the real PHP code against a real php, when one is available.
stub_real_php() { # stub_real_php <disable_functions value>
if [ -z "$REAL_PHP" ]; then
skip "no php on this host"
fi
printf '#!/bin/sh\nexec %s -d "disable_functions=%s" "$@"\n' "$REAL_PHP" "$1" | make_stub php
}

@test "real php: detects functions disabled via disable_functions" {
# PHP splits the list on commas and whitespace.
stub_real_php "pcntl_alarm, curl_exec exec"
run check_php_functions
[ "$status" -eq 1 ]
[[ "$output" == *"disables functions EasyEngine needs: curl_exec, exec."* ]]
}

@test "real php: passes when nothing required is disabled" {
stub_real_php ""
# shellcheck disable=SC2016,SC2086
if ! php -r 'foreach (array_slice($argv, 1) as $f) { if (!function_exists($f)) { exit(1); } }' -- $EE_PHP_REQUIRED_FUNCTIONS; then
skip "this php lacks an extension ee needs (e.g. curl, pcntl, posix)"
fi
run check_php_functions
[ "$status" -eq 0 ]
}

@test "check_dependencies checks PHP functions after PHP setup and before Docker" {
CALLS=""
for fn in ee_apt_update setup_host_dependencies setup_php setup_php_extensions check_php_functions setup_docker check_swap; do
eval "$fn() { CALLS+=\" $fn\"; }"
done
export EE_LINUX_DISTRO=Debian
printf '#!/bin/sh\nexit 0\n' | make_stub sqlite3
check_dependencies >/dev/null
[[ "$CALLS" == *"setup_php_extensions check_php_functions setup_docker"* ]]
}
Loading