Skip to content
Open
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
39 changes: 39 additions & 0 deletions community-fixes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Community Fixes

CloudPanel CE tracks issues in this repository, but the application source is distributed via the `cloudpanel` Debian package and is not published here. These scripts patch installed CloudPanel servers for well-defined bugs reported in GitHub issues.

## Available Fixes

| Issue | Script | Description |
|-------|--------|-------------|
| [#761](https://github.com/cloudpanel-io/cloudpanel-ce/issues/761) | [fix-761-site-php-settings-type.sh](./fix-761-site-php-settings-type.sh) | Fixes PHP 8+ TypeError in `SitePhpSettingsType::getPhpVersionChoices()` |
| [#758](https://github.com/cloudpanel-io/cloudpanel-ce/issues/758) | [fix-758-site-delete-crontab.sh](./fix-758-site-delete-crontab.sh) | Removes orphan user crontabs during `site:delete` |
| [#758](https://github.com/cloudpanel-io/cloudpanel-ce/issues/758) | [cleanup-orphan-crontabs.sh](./cleanup-orphan-crontabs.sh) | One-time cleanup for existing orphan crontab files |

## Usage

Run as root on a CloudPanel server:

```bash
curl -fsSL https://raw.githubusercontent.com/cloudpanel-io/cloudpanel-ce/master/community-fixes/fix-761-site-php-settings-type.sh | sudo bash
```

Or clone this repository and run the script directly:

```bash
sudo bash community-fixes/fix-761-site-php-settings-type.sh
```

Each script creates a timestamped backup before modifying files.

## Reverting

Backups are written next to the patched file with a `.bak.YYYYMMDDHHMMSS` suffix. To revert:

```bash
cp /path/to/file.bak.TIMESTAMP /path/to/file
```

## Contributing

If you have a fix for another open issue, add a script here and reference the issue number in the filename and script header.
31 changes: 31 additions & 0 deletions community-fixes/cleanup-orphan-crontabs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Utility for https://github.com/cloudpanel-io/cloudpanel-ce/issues/758
# Removes crontab spool files for users that no longer exist on the system.
set -euo pipefail

CRONTAB_SPOOL="/var/spool/cron/crontabs"

if [[ ! -d "$CRONTAB_SPOOL" ]]; then
echo "No crontab spool directory at $CRONTAB_SPOOL"
exit 0
fi

removed=0
kept=0

for crontab_file in "$CRONTAB_SPOOL"/*; do
[[ -e "$crontab_file" ]] || continue

username="$(basename "$crontab_file")"

if id "$username" &>/dev/null; then
kept=$((kept + 1))
continue
fi

echo "Removing orphan crontab for deleted user: $username"
rm -f "$crontab_file"
removed=$((removed + 1))
done

echo "Done. Removed: $removed, kept: $kept"
127 changes: 127 additions & 0 deletions community-fixes/fix-758-site-delete-crontab.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# Fix for https://github.com/cloudpanel-io/cloudpanel-ce/issues/758
# site:delete removes /etc/cron.d/<user> but leaves /var/spool/cron/crontabs/<user>,
# causing cron "Authentication failure" spam after user deletion.
set -euo pipefail

APP_SRC="/home/clp/htdocs/app/files/src"
CRONTAB_SPOOL="/var/spool/cron/crontabs"

if [[ ! -d "$APP_SRC" ]]; then
echo "ERROR: $APP_SRC not found. Is CloudPanel installed?" >&2
exit 1
fi

mapfile -t CANDIDATES < <(grep -rl 'userdel' "$APP_SRC" 2>/dev/null || true)

if [[ ${#CANDIDATES[@]} -eq 0 ]]; then
echo "ERROR: No files containing 'userdel' found under $APP_SRC" >&2
exit 1
fi

TARGET_FILE=""
for candidate in "${CANDIDATES[@]}"; do
if grep -qE 'site:delete|SiteDelete|DeleteSite|domainName' "$candidate" 2>/dev/null; then
TARGET_FILE="$candidate"
break
fi
done

if [[ -z "$TARGET_FILE" ]]; then
for candidate in "${CANDIDATES[@]}"; do
if grep -qE "userdel.*-rf|'userdel',\s*'-rf'" "$candidate" 2>/dev/null; then
TARGET_FILE="$candidate"
break
fi
done
fi

if [[ -z "$TARGET_FILE" ]]; then
echo "ERROR: Could not locate site delete command file." >&2
exit 1
fi

if grep -qE 'crontab.*-r|/var/spool/cron/crontabs' "$TARGET_FILE"; then
echo "Already patched: $TARGET_FILE handles crontab cleanup"
exit 0
fi

BACKUP="${TARGET_FILE}.bak.$(date +%Y%m%d%H%M%S)"
cp "$TARGET_FILE" "$BACKUP"
echo "Backup created: $BACKUP"
echo "Patching: $TARGET_FILE"

php <<PHP
<?php
\$file = '$TARGET_FILE';
\$content = file_get_contents(\$file);
\$original = \$content;

\$userVar = null;
\$patterns = [
"/['\"]userdel['\"]\s*,\s*['\"]-rf['\"]\s*,\s*(\\\$[a-zA-Z_][a-zA-Z0-9_]*)/",
"/userdel\\s+-rf\\s+['\"]?\\.?\\.?\\.?\\s*(\\\$[a-zA-Z_][a-zA-Z0-9_]*)/",
"/userdel\\s+-rf\\s+\\.\\s*(\\\$[a-zA-Z_][a-zA-Z0-9_]*)/",
];

foreach (\$patterns as \$pattern) {
if (preg_match(\$pattern, \$content, \$matches)) {
\$userVar = \$matches[1];
break;
}
}

if (\$userVar === null) {
fwrite(STDERR, "ERROR: Could not detect site user variable near userdel in \$file\n");
exit(1);
}

\$injection = <<<PATCH

// Remove user crontab before deleting the system user (issue #758)
if (class_exists(Process::class)) {
(new Process(['crontab', '-r', '-u', {$userVar}]))->run();
} else {
exec(sprintf('crontab -r -u %s 2>/dev/null', escapeshellarg({$userVar})));
}
@unlink(sprintf('/var/spool/cron/crontabs/%s', {$userVar}));

PATCH;

\$patched = false;
\$linePatterns = [
"/^(\\s*)(\\\$process\\s*=\\s*new\\s+Process\\s*\\(\\s*\\[\\s*['\"]userdel['\"])/m",
"/^(\\s*)(.*userdel.*-rf.*)/m",
];

foreach (\$linePatterns as \$pattern) {
\$result = preg_replace(\$pattern, \$injection . "\$1\$2", \$content, 1, \$count);
if (\$count > 0 && \$result !== null) {
\$content = \$result;
\$patched = true;
break;
}
}

if (!\$patched) {
fwrite(STDERR, "ERROR: Could not inject crontab cleanup before userdel in \$file\n");
exit(1);
}

if (\$content === \$original) {
fwrite(STDERR, "ERROR: No changes applied to \$file\n");
exit(1);
}

if (file_put_contents(\$file, \$content) === false) {
fwrite(STDERR, "ERROR: Failed to write \$file\n");
exit(1);
}

echo "SUCCESS: Patched site delete flow for issue #758\n";
echo "Detected site user variable: {\$userVar}\n";
PHP

echo ""
echo "Cleaning up existing orphan crontabs..."
bash "$(dirname "$0")/cleanup-orphan-crontabs.sh"
57 changes: 57 additions & 0 deletions community-fixes/fix-761-site-php-settings-type.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Fix for https://github.com/cloudpanel-io/cloudpanel-ce/issues/761
# SitePhpSettingsType::getPhpVersionChoices() uses `$phpVersion + 0` which
# throws TypeError on PHP 8.0+ when $phpVersion is a non-numeric string.
set -euo pipefail

TARGET_FILE="/home/clp/htdocs/app/files/src/Form/SitePhpSettingsType.php"

if [[ ! -f "$TARGET_FILE" ]]; then
echo "ERROR: $TARGET_FILE not found. Is CloudPanel installed?" >&2
exit 1
fi

BACKUP="${TARGET_FILE}.bak.$(date +%Y%m%d%H%M%S)"
cp "$TARGET_FILE" "$BACKUP"
echo "Backup created: $BACKUP"

php <<'PHP'
<?php
$file = '/home/clp/htdocs/app/files/src/Form/SitePhpSettingsType.php';
$content = file_get_contents($file);
$original = $content;

$replacements = [
// Documented pattern from issue #761
'if (true === is_float($phpVersion + 0))' => 'if (preg_match(\'/^\d+\.\d+$/\', $phpVersion))',
'if (is_float($phpVersion + 0))' => 'if (preg_match(\'/^\d+\.\d+$/\', $phpVersion))',
// Obfuscated variants may use different spacing
'is_float($phpVersion + 0)' => 'preg_match(\'/^\d+\.\d+$/\', $phpVersion)',
'is_float((float) $phpVersion)' => 'preg_match(\'/^\d+\.\d+$/\', $phpVersion)',
];

foreach ($replacements as $search => $replace) {
$content = str_replace($search, $replace, $content);
}

// Fallback: regex for any variable name used in getPhpVersionChoices-style checks
$content = preg_replace(
'/is_float\s*\(\s*(\$\w+)\s*\+\s*0\s*\)/',
'preg_match(\'/^\\d+\\.\\d+$/\', $1)',
$content
) ?? $content;

if ($content === $original) {
fwrite(STDERR, "ERROR: No matching pattern found in $file.\n");
fwrite(STDERR, "The file may already be patched or use a different code structure.\n");
exit(1);
}

if (file_put_contents($file, $content) === false) {
fwrite(STDERR, "ERROR: Failed to write $file\n");
exit(1);
}

echo "SUCCESS: Patched SitePhpSettingsType.php for issue #761\n";
echo "PHP version directory detection now uses preg_match('/^\\d+\\.\\d+$/', \$phpVersion)\n";
PHP
38 changes: 38 additions & 0 deletions community-fixes/test-761-fix-logic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
/**
* Validates the PHP version directory detection logic from issue #761.
* Run: php community-fixes/test-761-fix-logic.php
*/

function isValidPhpVersionDirectory(string $phpVersion): bool
{
return (bool) preg_match('/^\d+\.\d+$/', $phpVersion);
}

$tests = [
['8.4', true],
['8.1', true],
['7.4', true],
['conf.d', false],
['.', false],
['..', false],
['php-fpm', false],
['8', false],
['8.4.1', false],
];

$failed = 0;
foreach ($tests as [$input, $expected]) {
$actual = isValidPhpVersionDirectory($input);
if ($actual !== $expected) {
echo "FAIL: '$input' expected " . ($expected ? 'true' : 'false') . ", got " . ($actual ? 'true' : 'false') . PHP_EOL;
$failed++;
}
}

if ($failed > 0) {
echo "$failed test(s) failed." . PHP_EOL;
exit(1);
}

echo 'All tests passed.' . PHP_EOL;