Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
65 changes: 64 additions & 1 deletion functions
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,67 @@ 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=""
# 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+=" $name" ;;
missing) missing+=" $name" ;;
degraded) degraded+=" $name" ;;
ini) 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
echo " They are listed in disable_functions in:${ini_files:- (see php --ini)}" >&2
echo " Hosting panels such as HestiaCP set this. Remove them from disable_functions in the PHP CLI configuration (the PHP-FPM one can stay as it is) and re-run." >&2
if [ -n "$scan_dir" ]; then
echo " Panel upgrades may rewrite php.ini; a file such as ${scan_dir}/99-easyengine.ini that sets disable_functions without them keeps the change." >&2
fi
fi
if [ -n "$missing" ]; then
echo "ERROR: The PHP CLI ($(command -v php)) lacks functions EasyEngine needs:${missing}. Enable the PHP extensions that provide them (e.g. pcntl, posix, curl) and re-run." >&2
fi
exit 1
}

function create_swap() {
ee_log_info2 "Enabling 1GiB swap"
EE_SWAP_FILE="/ee-swapfile"
Expand Down Expand Up @@ -467,9 +528,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
19 changes: 19 additions & 0 deletions tests/integration/host-prep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ 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
panel_ini="$scan_dir/99-ee-test-panel.ini"
echo "disable_functions = pcntl_alarm,pcntl_fork,pcntl_signal,pcntl_signal_dispatch" > "$panel_ini"
panel_out="$( (check_php_functions) 2>&1 )"
panel_rc=$?
rm -f "$panel_ini"
check "disabled PHP functions are refused" test "$panel_rc" -ne 0
check "the error names the disabled functions" grep -qF "disables functions EasyEngine needs: pcntl_signal pcntl_signal_dispatch." <<< "$panel_out"
check "the error names the ini file" grep -qF "$panel_ini" <<< "$panel_out"
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
120 changes: 120 additions & 0 deletions tests/unit/php_functions.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/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
}

@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 durable 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" == *"disables functions EasyEngine needs: pcntl_signal pcntl_signal_dispatch."* ]]
[[ "$output" == *"disable_functions in: /etc/php/8.3/cli/php.ini"* ]]
[[ "$output" == *"/etc/php/8.3/cli/conf.d/99-easyengine.ini"* ]]
}

@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" == *"(see php --ini)"* ]]
}

@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" != *"disables functions"* ]]
}

@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