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
186 changes: 141 additions & 45 deletions .docker/wordpress/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -199,56 +199,156 @@ reset_local_user_passwords() {
echo "No local password reset target configured; set WORDPRESS_LOCAL_RESET_ALL_USERS_PASSWORDS=1 or WORDPRESS_LOCAL_RESET_PASSWORD_FOR_USER."
}

allow_unpinned_entry() {
local resource_label="$1"

if is_truthy "${WORDPRESS_SETUP_REQUIRE_PINNED:-0}"; then
echo " ✗ $resource_label has no pin and WORDPRESS_SETUP_REQUIRE_PINNED is enabled; skipping"
return 1
fi

echo " ⚠ $resource_label has no pin; using the latest version"
return 0
}

install_plugin() {
local plugin_slug="$1"
local plugin_version="$2"
local installed_version
local -a version_args=()

if [ -z "$plugin_version" ]; then
allow_unpinned_entry "Plugin $plugin_slug" || return 0
else
version_args=(--version="$plugin_version" --force)
fi

if runuser -u www-data -- wp plugin is-installed "$plugin_slug" 2>/dev/null; then
echo " ✓ Plugin $plugin_slug is already installed"
installed_version="$(runuser -u www-data -- wp plugin get "$plugin_slug" --field=version 2>/dev/null)"
if [ -z "$plugin_version" ] || [ "$installed_version" = "$plugin_version" ]; then
echo " ✓ Plugin $plugin_slug is already installed"
return 0
fi
echo " ↻ Updating $plugin_slug to $plugin_version..."
else
echo " ↓ Installing $plugin_slug..."
if runuser -u www-data -- wp plugin install "$plugin_slug" --activate 2>/dev/null; then
echo " ✓ Plugin $plugin_slug installed"
else
echo " ✗ Failed to install $plugin_slug"
fi
fi

if runuser -u www-data -- wp plugin install "$plugin_slug" "${version_args[@]}" --activate 2>/dev/null; then
echo " ✓ Plugin $plugin_slug installed"
else
echo " ✗ Failed to install $plugin_slug"
fi
}

install_plugin_archive() {
local plugin_slug="$1"
local archive_source="$2"
local archive_sha256="$3"
local download_dir
local archive_file

if [ -z "$archive_sha256" ]; then
allow_unpinned_entry "Plugin $plugin_slug" || return 0
fi

if runuser -u www-data -- wp plugin is-installed "$plugin_slug" 2>/dev/null; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

install_plugin_archive() returns immediately if the plugin is already installed, before checking the configured archive or checksum. If the pinned archive URL/SHA-256 is updated, subsequent starts silently retain the old installed plugin, so even a future Renovate update would not be applied.

Could we reconcile the installed artifact when the configured pin changes, or explicitly document that archive pins only control first-time installation? Please cover two successive starts with different archive pins and verify the intended installed version or artifact identity.

echo " ✓ Plugin $plugin_slug is already installed"
else
echo " ↓ Installing $plugin_slug from archive..."
if runuser -u www-data -- wp plugin install "$archive_source" --activate 2>/dev/null; then
echo " ✓ Plugin $plugin_slug installed"
else
echo " ✗ Failed to install $plugin_slug from archive"
return 0
fi

echo " ↓ Installing $plugin_slug from archive..."
if [ -n "$archive_sha256" ]; then
download_dir="$(mktemp -d)"
archive_file="$download_dir/$plugin_slug.zip"
if ! curl -fsSL "$archive_source" -o "$archive_file"; then
echo " ✗ Failed to download $plugin_slug"
rm -rf "$download_dir"
return 0
fi
if ! printf '%s %s\n' "$archive_sha256" "$archive_file" | sha256sum -c --status; then
echo " ✗ The archive of $plugin_slug does not match its sha256 checksum; skipping"
rm -rf "$download_dir"
return 0
fi
chown -R www-data:www-data "$download_dir"
archive_source="$archive_file"
fi

if runuser -u www-data -- wp plugin install "$archive_source" --activate 2>/dev/null; then
echo " ✓ Plugin $plugin_slug installed"
else
echo " ✗ Failed to install $plugin_slug from archive"
fi

if [ -n "$download_dir" ]; then
rm -rf "$download_dir"
fi
}

clone_custom_plugin() {
local plugin_name="$1"
checkout_git_ref() {
local target_dir="$1"
local git_url="$2"
local plugin_dir="/var/www/html/wp-content/plugins/$plugin_name"
local git_ref="$3"
local head_commit

if [ -d "$plugin_dir" ]; then
echo " ✓ Plugin $plugin_name already exists"
return 0
else
echo " ↓ Cloning $plugin_name..."
cd /var/www/html/wp-content/plugins/
if git clone "$git_url" "$plugin_name" 2>/dev/null; then
echo " ✓ Plugin $plugin_name cloned"
runuser -u www-data -- git -C "$target_dir" fetch --quiet "$git_url" "$git_ref" || return 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Git tags are mutable, but ref is currently treated as a pin even when it names a tag. Re-fetching that tag can resolve to a different commit without any change to the configured ref.

Could we distinguish immutable full-commit-SHA pins from movable tag references in the strict pinning policy? Either require commit SHAs for strict reproducibility or explicitly document the weaker guarantee of tags. A test that moves a tag between starts would make the expected behavior clear.

runuser -u www-data -- git -C "$target_dir" checkout --quiet --detach FETCH_HEAD || return 1

if [[ "$git_ref" =~ ^[0-9a-f]{40}$ ]]; then
head_commit="$(runuser -u www-data -- git -C "$target_dir" rev-parse HEAD)"
if [ "$head_commit" != "$git_ref" ]; then
echo " ✗ $target_dir is at $head_commit instead of $git_ref"
return 1
fi
fi
}

sync_git_checkout() {
local resource_label="$1"
local target_dir="$2"
local git_url="$3"
local git_ref="$4"

if [ -z "$git_ref" ]; then
allow_unpinned_entry "$resource_label" || return 1
fi

if [ -d "$target_dir" ]; then
if [ -z "$git_ref" ]; then
echo " ✓ $resource_label already exists"
return 0
else
echo " ✗ Failed to clone $plugin_name"
fi
if [ "$(runuser -u www-data -- git -C "$target_dir" rev-parse HEAD 2>/dev/null)" = "$git_ref" ]; then
echo " ✓ $resource_label is already at $git_ref"
return 0
fi
echo " ↻ Updating $resource_label to $git_ref..."
else
echo " ↓ Cloning $resource_label..."
if ! runuser -u www-data -- git clone --quiet "$git_url" "$target_dir"; then
echo " ✗ Failed to clone $resource_label"
return 1
fi
if [ -z "$git_ref" ]; then
echo " ✓ $resource_label cloned"
return 0
fi
fi

if ! checkout_git_ref "$target_dir" "$git_url" "$git_ref"; then
echo " ✗ Failed to check out $git_ref for $resource_label"
return 1
fi
echo " ✓ $resource_label is at $git_ref"
}

clone_custom_plugin() {
local plugin_name="$1"
local git_url="$2"
local git_ref="$3"

sync_git_checkout "Plugin $plugin_name" "/var/www/html/wp-content/plugins/$plugin_name" "$git_url" "$git_ref"
}

run_custom_plugin_post_install_commands() {
Expand Down Expand Up @@ -284,24 +384,15 @@ finalize_custom_plugin() {
clone_custom_theme() {
local theme_name="$1"
local git_url="$2"
local theme_dir="/var/www/html/wp-content/themes/$theme_name"
local git_ref="$3"

if [ -d "$theme_dir" ]; then
echo " ✓ Theme $theme_name already exists"
else
echo " ↓ Cloning theme $theme_name..."
cd /var/www/html/wp-content/themes/
if git clone "$git_url" "$theme_name" 2>/dev/null; then
chown -R www-data:www-data "$theme_dir"
echo " ✓ Theme $theme_name cloned"
else
echo " ✗ Failed to clone $theme_name"
fi
fi
sync_git_checkout "Theme $theme_name" "/var/www/html/wp-content/themes/$theme_name" "$git_url" "$git_ref" || true
}

install_org_plugins_from_config() {
local entry
local plugin_slug
local plugin_version

echo "Plugins from wordpress.org:"

Expand All @@ -310,19 +401,21 @@ install_org_plugins_from_config() {
return
fi

while IFS= read -r plugin_slug; do
plugin_slug="$(trim_value "$plugin_slug")"
while IFS= read -r entry; do
plugin_slug="$(trim_value "$(printf '%s' "$entry" | base64 -d | yq -r 'if type == "object" then .slug else . end')")"
plugin_version="$(printf '%s' "$entry" | base64 -d | yq -r 'if type == "object" then (.version // "" | tostring) else "" end')"
if [ -z "$plugin_slug" ] || [ "$plugin_slug" = "null" ]; then
continue
fi
install_plugin "$plugin_slug"
done < <(read_yaml_values '.wordpress_org_plugins[]?')
install_plugin "$plugin_slug" "$plugin_version"
done < <(read_yaml_values '.wordpress_org_plugins[]? | @base64')
}

install_archive_plugins_from_config() {
local entry
local plugin_slug
local archive_source
local archive_sha256

echo "Plugins from archive:"

Expand All @@ -334,13 +427,14 @@ install_archive_plugins_from_config() {
while IFS= read -r entry; do
plugin_slug="$(printf '%s' "$entry" | base64 -d | yq -r '.slug')"
archive_source="$(printf '%s' "$entry" | base64 -d | yq -r '.source')"
archive_sha256="$(printf '%s' "$entry" | base64 -d | yq -r '.sha256 // ""')"

if [ -z "$plugin_slug" ] || [ "$plugin_slug" = "null" ] || [ -z "$archive_source" ] || [ "$archive_source" = "null" ]; then
echo " ✗ Invalid configuration in wordpress_archive_plugins"
continue
fi

install_plugin_archive "$plugin_slug" "$archive_source"
install_plugin_archive "$plugin_slug" "$archive_source" "$archive_sha256"
done < <(read_yaml_values '.wordpress_archive_plugins[]? | @base64')
}

Expand All @@ -351,6 +445,7 @@ install_git_entries() {
local entry
local resource_name
local source_url
local git_ref

echo "$label:"

Expand All @@ -362,18 +457,19 @@ install_git_entries() {
while IFS= read -r entry; do
resource_name="$(printf '%s' "$entry" | base64 -d | yq -r '.slug')"
source_url="$(printf '%s' "$entry" | base64 -d | yq -r '.source')"
git_ref="$(printf '%s' "$entry" | base64 -d | yq -r '.ref // "" | tostring')"

if [ -z "$resource_name" ] || [ "$resource_name" = "null" ] || [ -z "$source_url" ] || [ "$source_url" = "null" ]; then
echo " ✗ Invalid configuration in $label"
continue
fi

if [ "$entry_type" = "plugin" ]; then
if clone_custom_plugin "$resource_name" "$source_url"; then
if clone_custom_plugin "$resource_name" "$source_url" "$git_ref"; then
finalize_custom_plugin "$resource_name" "$entry"
fi
else
clone_custom_theme "$resource_name" "$source_url"
clone_custom_theme "$resource_name" "$source_url" "$git_ref"
fi
done < <(read_yaml_values "${query}[]? | @base64")
}
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ services:
# Use wordpress_org_plugins, wordpress_archive_plugins, wordpress_custom_plugins, and wordpress_custom_themes.
WORDPRESS_SETUP_CONFIG_YAML: |
# Plugins installed directly from the WordPress.org repository.
# Use a mapping with version to pin a release.
wordpress_org_plugins:
- advanced-custom-fields
- woocommerce
- slug: woocommerce
version: 10.3.0

# Plugins installed from a .zip URL (e.g. GitHub Releases or S3).
# Optional: post_install_commands — list of shell commands run after installing
Expand All @@ -62,15 +64,19 @@ services:
wordpress_archive_plugins:
- slug: my-premium-plugin
source: https://example.com/releases/my-premium-plugin-1.0.0.zip
# Optional: the archive is only installed when it matches this checksum.
sha256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

# Plugins cloned from a Git repository.
# Optional: post_install_commands — list of shell commands run after cloning
# (and on every container start if the plugin directory already exists).
# Commands run as www-data from the WordPress root (/var/www/html).
# The env var PLUGIN_DIR is set to the absolute plugin directory path.
# Optional: ref pins a tag or a full commit SHA. A plugin whose commit does not match is not activated.
wordpress_custom_plugins:
- slug: my-plugin
source: https://github.com/org/my-plugin.git
ref: v1.2.0
- slug: my-plugin-with-fixup
source: https://github.com/org/my-plugin-with-fixup.git
post_install_commands:
Expand All @@ -81,6 +87,7 @@ services:
wordpress_custom_themes:
- slug: my-theme
source: https://github.com/org/my-theme.git
ref: 0123456789abcdef0123456789abcdef01234567

nginx:
environment:
Expand All @@ -100,6 +107,8 @@ services:
- 127.0.0.1:${MAILPIT_PORT:-8025}:8025
```

Entries without `version`, `sha256` or `ref` install whatever the source serves at that moment and are reported in the log. Set `WORDPRESS_SETUP_REQUIRE_PINNED=1` to skip them instead.

After that, start the stack with the standard Compose command:

```bash
Expand Down
1 change: 1 addition & 0 deletions common-services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ services:
- WORDPRESS_DB_PASSWORD=${WORDPRESS_DB_PASSWORD:-root}
- WORDPRESS_TABLE_PREFIX=${WORDPRESS_TABLE_PREFIX:-wp_}
- WORDPRESS_SETUP_CONFIG_YAML=${WORDPRESS_SETUP_CONFIG_YAML:-}
- WORDPRESS_SETUP_REQUIRE_PINNED=${WORDPRESS_SETUP_REQUIRE_PINNED:-0}
- WP_ENVIRONMENT_TYPE
- WORDPRESS_LOCAL_USERS_PASSWORD=${WORDPRESS_LOCAL_USERS_PASSWORD:-}
- WORDPRESS_LOCAL_RESET_ALL_USERS_PASSWORDS=${WORDPRESS_LOCAL_RESET_ALL_USERS_PASSWORDS:-0}
Expand Down
8 changes: 6 additions & 2 deletions tests/security/helpers/entrypoint.bash
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ entrypoint_root="${BATS_TEST_DIRNAME}/../.."
entrypoint_tmp="${BATS_TEST_TMPDIR}/entrypoint"
entrypoint_network=""
entrypoint_database_container=""
entrypoint_image="php:8.3-fpm"

entrypoint_setup() {
entrypoint_network="entrypoint-bats-${BATS_TEST_NUMBER:-0}-$$"
mkdir -p "${entrypoint_tmp}/document-root" "${entrypoint_tmp}/bin" "${entrypoint_tmp}/log"
mkdir -p "${entrypoint_tmp}/document-root/wp-content/plugins" "${entrypoint_tmp}/document-root/wp-content/themes" \
"${entrypoint_tmp}/bin" "${entrypoint_tmp}/log" "${entrypoint_tmp}/sources"
touch "${entrypoint_tmp}/document-root/wp-load.php" "${entrypoint_tmp}/document-root/wp-config.php"
chmod 777 "${entrypoint_tmp}/log"

cat > "${entrypoint_tmp}/bin/wp" <<'WP_STUB'
#!/bin/sh
echo "wp $*" >> /var/log/entrypoint/wp-calls
case "$1 $2" in
"plugin is-installed") exit 1 ;;
"user list") echo 1 ;;
"user get") echo 1 ;;
esac
Expand Down Expand Up @@ -59,7 +62,8 @@ entrypoint_run() {
-v "${entrypoint_tmp}/bin/php-fpm:/usr/local/sbin/php-fpm:ro" \
-v "${entrypoint_tmp}/log:/var/log/entrypoint" \
-v "${entrypoint_tmp}/document-root:/var/www/html" \
php:8.3-fpm bash /var/www/scripts/entrypoint.sh
-v "${entrypoint_tmp}/sources:/sources:ro" \
"${entrypoint_image}" bash /var/www/scripts/entrypoint.sh
}

entrypoint_wp_calls() {
Expand Down
Loading
Loading