HEX
Server: Apache
System: Linux 212a9e2a4109 6.14.0-1010-aws #10~24.04.1-Ubuntu SMP Fri Jul 18 20:44:30 UTC 2025 x86_64
User: (1001)
PHP: 8.2.20
Disabled: NONE
Upload Files
File: /opt/bitnami/scripts/libwordpress.sh
#!/bin/bash
# Copyright Broadcom, Inc. All Rights Reserved.
# SPDX-License-Identifier: APACHE-2.0
#
# Bitnami WordPress library

# shellcheck disable=SC1091

# Load generic libraries
. /opt/bitnami/scripts/libphp.sh
. /opt/bitnami/scripts/libfs.sh
. /opt/bitnami/scripts/libos.sh
. /opt/bitnami/scripts/libnet.sh
. /opt/bitnami/scripts/libvalidations.sh
. /opt/bitnami/scripts/libpersistence.sh
. /opt/bitnami/scripts/libwebserver.sh
. /opt/bitnami/scripts/libservice.sh

# Load database library
if [[ -f /opt/bitnami/scripts/libmysqlclient.sh ]]; then
    . /opt/bitnami/scripts/libmysqlclient.sh
elif [[ -f /opt/bitnami/scripts/libmysql.sh ]]; then
    . /opt/bitnami/scripts/libmysql.sh
elif [[ -f /opt/bitnami/scripts/libmariadb.sh ]]; then
    . /opt/bitnami/scripts/libmariadb.sh
fi

########################
# Validate settings in WORDPRESS_* env vars
# Globals:
#   WORDPRESS_*
# Arguments:
#   None
# Returns:
#   0 if the validation succeeded, 1 otherwise
#########################
wordpress_validate() {
    debug "Validating settings in WORDPRESS_* environment variables..."
    local error_code=0

    # Auxiliary functions
    print_validation_error() {
        error "$1"
        error_code=1
    }
    check_empty_value() {
        if is_empty_value "${!1}"; then
            print_validation_error "${1} must be set"
        fi
    }
    check_yes_no_value() {
        if ! is_yes_no_value "${!1}" && ! is_true_false_value "${!1}"; then
            print_validation_error "The allowed values for ${1} are: yes no"
        fi
    }
    check_int_value() {
        if ! is_int "${!1}"; then
            print_validation_error "The value for $1 should be an integer"
        fi
    }
    check_multi_value() {
        if [[ " ${2} " != *" ${!1} "* ]]; then
            print_validation_error "The allowed values for ${1} are: ${2}"
        fi
    }
    check_resolved_hostname() {
        if ! is_hostname_resolved "$1"; then
            warn "Hostname ${1} could not be resolved, this could lead to connection issues"
        fi
    }
    check_mounted_file() {
        if [[ -n "${!1:-}" ]] && ! [[ -f "${!1:-}" ]]; then
            print_validation_error "The variable ${1} is defined but the file ${!1} is not accessible or does not exist"
        fi
    }
    check_valid_port() {
        local port_var="${1:?missing port variable}"
        local err
        if ! err="$(validate_port "${!port_var}")"; then
            print_validation_error "An invalid port was specified in the environment variable ${port_var}: ${err}."
        fi
    }

    # Warn users in case the configuration file is not writable
    is_file_writable "$WORDPRESS_CONF_FILE" || warn "The WordPress configuration file '${WORDPRESS_CONF_FILE}' is not writable. Configurations based on environment variables will not be applied for this file."

    # Validate user inputs
    check_yes_no_value "WORDPRESS_ENABLE_HTTPS"
    check_yes_no_value "WORDPRESS_HTACCESS_OVERRIDE_NONE"
    check_yes_no_value "WORDPRESS_ENABLE_HTACCESS_PERSISTENCE"
    check_yes_no_value "WORDPRESS_RESET_DATA_PERMISSIONS"
    check_yes_no_value "WORDPRESS_SKIP_BOOTSTRAP"
    check_multi_value "WORDPRESS_AUTO_UPDATE_LEVEL" "major minor none"
    check_yes_no_value "WORDPRESS_ENABLE_REVERSE_PROXY"
    check_yes_no_value "WORDPRESS_ENABLE_XML_RPC"

    # Multisite validations
    check_yes_no_value "WORDPRESS_ENABLE_MULTISITE"
    if is_boolean_yes "$WORDPRESS_ENABLE_MULTISITE"; then
        # subdirectory is an alias for subfolder
        check_multi_value "WORDPRESS_MULTISITE_NETWORK_TYPE" "subfolder subdirectory subdomain"
        check_yes_no_value "WORDPRESS_MULTISITE_ENABLE_NIP_IO_REDIRECTION"
        check_int_value "WORDPRESS_MULTISITE_FILEUPLOAD_MAXK"
        if ! is_empty_value "$WORDPRESS_MULTISITE_HOST"; then
            check_resolved_hostname "$WORDPRESS_MULTISITE_HOST"
            [[ "$WORDPRESS_MULTISITE_HOST" =~ localhost ]] && print_validation_error "WORDPRESS_MULTISITE_HOST must be set to an actual hostname, localhost values are not allowed."
            validate_ipv4 "$WORDPRESS_MULTISITE_HOST" && print_validation_error "WORDPRESS_MULTISITE_HOST must be set to an actual hostname, IP addresses are not allowed."
            check_valid_port "WORDPRESS_MULTISITE_EXTERNAL_HTTP_PORT_NUMBER"
            check_valid_port "WORDPRESS_MULTISITE_EXTERNAL_HTTPS_PORT_NUMBER"
        else
            print_validation_error "WORDPRESS_MULTISITE_HOST must be set when enabling WordPress Multisite mode."
        fi
    elif ! is_empty_value "$WORDPRESS_MULTISITE_HOST"; then
        warn "Multisite mode is not enabled, and WORDPRESS_MULTISITE_HOST is only used for Multisite installations. Its value will be ignored."
    fi

    # Database configuration validations
    check_resolved_hostname "$WORDPRESS_DATABASE_HOST"
    check_valid_port "WORDPRESS_DATABASE_PORT_NUMBER"
    check_yes_no_value "WORDPRESS_ENABLE_DATABASE_SSL"
    if is_boolean_yes "$WORDPRESS_ENABLE_DATABASE_SSL"; then
        check_yes_no_value "WORDPRESS_VERIFY_DATABASE_SSL"
        check_mounted_file "WORDPRESS_DATABASE_SSL_CERT_FILE"
        check_mounted_file "WORDPRESS_DATABASE_SSL_KEY_FILE"
        check_mounted_file "WORDPRESS_DATABASE_SSL_CA_FILE"
    fi

    # Validate credentials
    if is_boolean_yes "${ALLOW_EMPTY_PASSWORD:-}"; then
        warn "You set the environment variable ALLOW_EMPTY_PASSWORD=${ALLOW_EMPTY_PASSWORD:-}. For safety reasons, do not use this flag in a production environment."
    else
        for empty_env_var in "WORDPRESS_DATABASE_PASSWORD" "WORDPRESS_PASSWORD"; do
            is_empty_value "${!empty_env_var}" && print_validation_error "The ${empty_env_var} environment variable is empty or not set. Set the environment variable ALLOW_EMPTY_PASSWORD=yes to allow a blank password. This is only recommended for development environments."
        done
    fi

    # Validate SMTP credentials
    if ! is_empty_value "$WORDPRESS_SMTP_HOST"; then
        check_resolved_hostname "$WORDPRESS_SMTP_HOST"
        for empty_env_var in "WORDPRESS_SMTP_USER" "WORDPRESS_SMTP_PASSWORD"; do
            is_empty_value "${!empty_env_var}" && warn "The ${empty_env_var} environment variable is empty or not set."
        done
        is_empty_value "$WORDPRESS_SMTP_PORT_NUMBER" && print_validation_error "The WORDPRESS_SMTP_PORT_NUMBER environment variable is empty or not set."
        ! is_empty_value "$WORDPRESS_SMTP_PORT_NUMBER" && check_valid_port "WORDPRESS_SMTP_PORT_NUMBER"
        ! is_empty_value "$WORDPRESS_SMTP_PROTOCOL" && check_multi_value "WORDPRESS_SMTP_PROTOCOL" "ssl tls"
    fi

    # Validate htaccess persistence
    if is_boolean_yes "$WORDPRESS_ENABLE_HTACCESS_PERSISTENCE" && [[ "$(web_server_type)" = "apache" ]]; then
        if is_boolean_yes "$WORDPRESS_HTACCESS_OVERRIDE_NONE"; then
            local htaccess_file="${WORDPRESS_BASE_DIR}/wordpress-htaccess.conf"
            local htaccess_dest="${APACHE_HTACCESS_DIR}/wordpress-htaccess.conf"
            if is_file_writable "$htaccess_dest"; then
                ! is_file_writable "$htaccess_file" && print_validation_error "The WORDPRESS_ENABLE_HTACCESS_PERSISTENCE configuration is enabled, but the htaccess file to persist ${htaccess_file} is not writable."
            else
                warn "The WORDPRESS_ENABLE_HTACCESS_PERSISTENCE configuration is enabled but the ${htaccess_dest} file is not writable. The file will not be persisted."
            fi
        else
            local htaccess_file="${WORDPRESS_BASE_DIR}/.htaccess"
            ! is_file_writable "$htaccess_file" && print_validation_error "The WORDPRESS_ENABLE_HTACCESS_PERSISTENCE configuration is enabled but the htaccess file to persist ${htaccess_file} is not writable."
        fi
    fi

    # Check that the web server is properly set up
    web_server_validate || print_validation_error "Web server validation failed"

    return "$error_code"
}

########################
# Configure database settings in wp-config.php
# Globals:
#   WORDPRESS_*
# Arguments:
#   None
# Returns:
#   None
#########################
wordpress_set_db_settings() {
    # Configure database credentials
    wordpress_conf_set "DB_NAME" "$WORDPRESS_DATABASE_NAME"
    wordpress_conf_set "DB_USER" "$WORDPRESS_DATABASE_USER"
    wordpress_conf_set "DB_PASSWORD" "$WORDPRESS_DATABASE_PASSWORD"
    wordpress_conf_set "DB_HOST" "${WORDPRESS_DATABASE_HOST}:${WORDPRESS_DATABASE_PORT_NUMBER}"
    # Configure database SSL/TLS connections
    if is_boolean_yes "$WORDPRESS_ENABLE_DATABASE_SSL"; then
        ! is_empty_value "$WORDPRESS_DATABASE_SSL_KEY_FILE" && wordpress_conf_set "MYSQL_SSL_KEY" "$WORDPRESS_DATABASE_SSL_KEY_FILE"
        ! is_empty_value "$WORDPRESS_DATABASE_SSL_CERT_FILE" && wordpress_conf_set "MYSQL_SSL_CERT" "$WORDPRESS_DATABASE_SSL_CERT_FILE"
        ! is_empty_value "$WORDPRESS_DATABASE_SSL_CA_FILE" && wordpress_conf_set "MYSQL_SSL_CA" "$WORDPRESS_DATABASE_SSL_CA_FILE"
        local wp_mysqli_client_flags="MYSQLI_CLIENT_SSL"
        if ! is_boolean_yes "$WORDPRESS_VERIFY_DATABASE_SSL"; then
            wp_mysqli_client_flags+=" | MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT"
        fi
        wordpress_conf_set "MYSQL_CLIENT_FLAGS" "$wp_mysqli_client_flags" yes
    fi
}

########################
# Ensure WordPress is initialized
# Globals:
#   WORDPRESS_*
# Arguments:
#   None
# Returns:
#   None
#########################
wordpress_initialize() {
    # For backwards compatibility, check if the .htaccess file should be persisted
    # Now it is possible specify the list of files by overriding WORDPRESS_DATA_TO_PERSIST
    if is_boolean_yes "$WORDPRESS_ENABLE_HTACCESS_PERSISTENCE" && [[ "$(web_server_type)" = "apache" ]]; then
        if is_boolean_yes "$WORDPRESS_HTACCESS_OVERRIDE_NONE"; then
            local htaccess_file="${WORDPRESS_BASE_DIR}/wordpress-htaccess.conf"
            local htaccess_dest="${APACHE_HTACCESS_DIR}/wordpress-htaccess.conf"
            if is_file_writable "$htaccess_dest" && is_file_writable "$htaccess_file"; then
                # If the file was not created at build time, copy the default configuration
                [[ ! -f "$htaccess_file" ]] && cp "$htaccess_dest" "$htaccess_file"
                # With the use of symlinks, we can configure Apache to use it before the persisted file is even created
                rm "$htaccess_dest"
                ln -s "$htaccess_file" "$htaccess_dest"
                WORDPRESS_DATA_TO_PERSIST+=" ${htaccess_file}"
            fi
        else
            local htaccess_file="${WORDPRESS_BASE_DIR}/.htaccess"
            if is_file_writable "$htaccess_file"; then
                # If the file was not mounted, use default configuration (currently empty htaccess for app root)
                [[ ! -f "$htaccess_file" ]] && touch "$htaccess_file"
                WORDPRESS_DATA_TO_PERSIST+=" ${htaccess_file}"
            fi
        fi
    fi

    # Check if WordPress has already been initialized and persisted in a previous run
    local -r app_name="wordpress"
    if ! is_app_initialized "$app_name" || [[ ! -f "$WORDPRESS_CONF_FILE" ]]; then
        # Ensure WordPress persisted directories exist (i.e. when a volume has been mounted to /bitnami)
        info "Ensuring WordPress directories exist"
        ensure_dir_exists "$WORDPRESS_VOLUME_DIR"
        # Use daemon:root ownership for compatibility when running as a non-root user
        am_i_root && configure_permissions_ownership "$WORDPRESS_VOLUME_DIR" -d "775" -f "664" -u "$WEB_SERVER_DAEMON_USER" -g "root"
        info "Trying to connect to the database server"
        wordpress_wait_for_mysql_connection "$WORDPRESS_DATABASE_HOST" "$WORDPRESS_DATABASE_PORT_NUMBER" "$WORDPRESS_DATABASE_NAME" "$WORDPRESS_DATABASE_USER" "$WORDPRESS_DATABASE_PASSWORD"

        # Apply changes to WordPress configuration file based on user inputs
        # See: https://wordpress.org/support/article/editing-wp-config-php/
        # Note that wp-config.php is officially indented via tabs, not spaces
        info "Configuring WordPress with settings provided via environment variables"
        if is_file_writable "$WORDPRESS_CONF_FILE"; then
            # Set miscellaneous configurations
            wordpress_conf_set "FS_METHOD" "direct"
            is_boolean_yes "$WORDPRESS_ENABLE_REVERSE_PROXY" && wordpress_configure_reverse_proxy
            ! is_boolean_yes "$WORDPRESS_ENABLE_MULTISITE" && wordpress_configure_urls
            # The only variable/non-constant in the entire configuration file is '$table_prefix'
            replace_in_file "$WORDPRESS_CONF_FILE" "^(\s*\\\$table_prefix\s*=\s*).*" "\1'$WORDPRESS_TABLE_PREFIX';"
            wordpress_set_db_settings
            # Configure random keys and salt values
            wp_execute config shuffle-salts

            # Configure keys and salt values
            ! is_empty_value "$WORDPRESS_AUTH_KEY" && wordpress_conf_set "AUTH_KEY" "$WORDPRESS_AUTH_KEY"
            ! is_empty_value "$WORDPRESS_SECURE_AUTH_KEY" && wordpress_conf_set "SECURE_AUTH_KEY" "$WORDPRESS_SECURE_AUTH_KEY"
            ! is_empty_value "$WORDPRESS_LOGGED_IN_KEY" && wordpress_conf_set "LOGGED_IN_KEY" "$WORDPRESS_LOGGED_IN_KEY"
            ! is_empty_value "$WORDPRESS_NONCE_KEY" && wordpress_conf_set "NONCE_KEY" "$WORDPRESS_NONCE_KEY"
            ! is_empty_value "$WORDPRESS_AUTH_SALT" && wordpress_conf_set "AUTH_SALT" "$WORDPRESS_AUTH_SALT"
            ! is_empty_value "$WORDPRESS_SECURE_AUTH_SALT" && wordpress_conf_set "SECURE_AUTH_SALT" "$WORDPRESS_SECURE_AUTH_SALT"
            ! is_empty_value "$WORDPRESS_LOGGED_IN_SALT" && wordpress_conf_set "LOGGED_IN_SALT" "$WORDPRESS_LOGGED_IN_SALT"
            ! is_empty_value "$WORDPRESS_NONCE_SALT" && wordpress_conf_set "NONCE_SALT" "$WORDPRESS_NONCE_SALT"

            # Enable or disable auto-updates
            # https://wordpress.org/support/article/configuring-automatic-background-updates/#constant-to-configure-core-updates
            if [[ "$WORDPRESS_AUTO_UPDATE_LEVEL" = "minor" ]]; then
                wordpress_conf_set "WP_AUTO_UPDATE_CORE" "minor"
            else
                wordpress_conf_set "WP_AUTO_UPDATE_CORE" "$([[ "$WORDPRESS_AUTO_UPDATE_LEVEL" = "major" ]] && echo "true" || echo "false")" yes
            fi
            # Disable pingback to prevent WordPress from participating in DDoS attacks
            wordpress_disable_pingback
            # Lastly, allow to append any custom configuration to the wp-config.php file via an environment variable
            ! is_empty_value "$WORDPRESS_EXTRA_WP_CONFIG_CONTENT" && wordpress_conf_append "$WORDPRESS_EXTRA_WP_CONFIG_CONTENT"
        else
            warn "Skipping modifications to ${WORDPRESS_CONF_FILE} because it is not writable"
        fi

        # Initialize the WordPress application
        if ! is_boolean_yes "$WORDPRESS_SKIP_BOOTSTRAP"; then
            # Build install arguments and run installation command
            local wp_install_flags=(
                "--title=${WORDPRESS_BLOG_NAME}"
                "--admin_user=${WORDPRESS_USERNAME}"
                "--admin_password=${WORDPRESS_PASSWORD}"
                "--admin_email=${WORDPRESS_EMAIL}"
                "--skip-email"
            )
            # The --url argument is required in all cases
            # However, in Multisite it is used to set the domains, meaning the value having a direct impact
            # In non-Multisite installations, however, it will only set install metadata and not have any usage impact
            if is_boolean_yes "$WORDPRESS_ENABLE_MULTISITE"; then
                local wordpress_url="$WORDPRESS_MULTISITE_HOST"
                if is_boolean_yes "$WORDPRESS_ENABLE_HTTPS" || [[ "$WORDPRESS_SCHEME" = "https" ]]; then
                    wordpress_url="https://${wordpress_url}"
                    [[ "$WORDPRESS_MULTISITE_EXTERNAL_HTTPS_PORT_NUMBER" != "443" ]] && wordpress_url+=":${WORDPRESS_MULTISITE_EXTERNAL_HTTPS_PORT_NUMBER}"
                else
                    wordpress_url="http://${wordpress_url}"
                    [[ "$WORDPRESS_MULTISITE_EXTERNAL_HTTP_PORT_NUMBER" != "80" ]] && wordpress_url+=":${WORDPRESS_MULTISITE_EXTERNAL_HTTP_PORT_NUMBER}"
                fi
                wp_install_flags+=("--url=${wordpress_url}")
            else
                wp_install_flags+=("--url=localhost")
            fi
            # Allow to specify extra CLI flags, but ensure they are added last
            local -a wp_extra_install_flags
            read -r -a wp_extra_install_flags <<<"$WORDPRESS_EXTRA_INSTALL_ARGS"
            [[ "${#wp_extra_install_flags[@]}" -gt 0 ]] && wp_install_flags+=("${wp_extra_install_flags[@]}")
            # Run installation command, which differs between normal and Multisite installations
            if is_boolean_yes "$WORDPRESS_ENABLE_MULTISITE"; then
                info "Installing WordPress Multisite"
                [[ "$WORDPRESS_MULTISITE_NETWORK_TYPE" = "subdomain" ]] && wp_install_flags=("--subdomains" "${wp_install_flags[@]}")
                wp_execute core multisite-install "${wp_install_flags[@]}"
            else
                info "Installing WordPress"
                wp_execute core install "${wp_install_flags[@]}"
            fi
            # Install plugins defined via environment variables
            local -a install_plugins_args=()
            if [[ "$WORDPRESS_PLUGINS" = "all" ]]; then
                info "Activating all installed plugins"
                install_plugins_args+=("--all")
                if is_boolean_yes "$WORDPRESS_ENABLE_MULTISITE"; then
                    install_plugins_args+=("--network")
                fi
                wp_execute plugin activate "${install_plugins_args[@]}"
            elif [[ "$WORDPRESS_PLUGINS" != "none" ]]; then
                local -a plugins_to_install
                read -r -a plugins_to_install <<<"$(echo "$WORDPRESS_PLUGINS" | tr ',;' ' ')"
                if [[ "${#plugins_to_install[@]}" -gt 0 ]]; then
                    info "Installing and activating plugins: ${plugins_to_install[*]}"
                    install_plugins_args+=("${plugins_to_install[@]}")
                    if is_boolean_yes "$WORDPRESS_ENABLE_MULTISITE"; then
                        install_plugins_args+=("--activate-network")
                    else
                        install_plugins_args+=("--activate")
                    fi
                    wp_execute plugin install "${install_plugins_args[@]}"
                fi
            fi
            # Post installation steps
            local -r default_user_id="1"
            wp_execute user meta set "$default_user_id" first_name "$WORDPRESS_FIRST_NAME"
            wp_execute user meta set "$default_user_id" last_name "$WORDPRESS_LAST_NAME"
            # Increase upload limit for multisite installations (default is 1MB)
            local -r default_site_id="1"
            is_boolean_yes "$WORDPRESS_ENABLE_MULTISITE" && wp_execute site meta update "$default_site_id" fileupload_maxk "$WORDPRESS_MULTISITE_FILEUPLOAD_MAXK"
            # Enable friendly URLs / permalinks (using historic Bitnami defaults)
            wp_execute rewrite structure '/%year%/%monthnum%/%day%/%postname%/'
            ! is_empty_value "$WORDPRESS_SMTP_HOST" && wordpress_configure_smtp
        else
            info "An already initialized WordPress database was provided, configuration will be skipped"
            wp_execute core update-db
        fi

        info "Persisting WordPress installation"
        persist_app "$app_name" "$WORDPRESS_DATA_TO_PERSIST"

        # Secure wp-config.php file after persisting data because then we can ensure the commands to work
        # when running the scripts as non-root users
        local wp_config_path
        wp_config_path="$(readlink -f "$WORDPRESS_CONF_FILE")"
        if am_i_root; then
            is_file_writable "$wp_config_path" && configure_permissions_ownership "$wp_config_path" -f "440" -u "$WEB_SERVER_DAEMON_USER" -g "root"
        else
            is_file_writable "$wp_config_path" && configure_permissions_ownership "$wp_config_path" -f "440"
        fi
    else
        info "Restoring persisted WordPress installation"
        restore_persisted_app "$app_name" "$WORDPRESS_DATA_TO_PERSIST"
        info "Trying to connect to the database server"
        local db_name db_user db_pass db_host db_port
        if is_boolean_yes "$WORDPRESS_OVERRIDE_DATABASE_SETTINGS"; then
            info "Overriding the database configuration in wp-config.php with the provided environment variables"
            # Make the wp-config.php file writable to change the db settings
            local wp_config_path wp_config_perms
            wp_config_path="$(readlink -f "$WORDPRESS_CONF_FILE")"
            wp_config_perms="$(stat -c "%a" "$WORDPRESS_CONF_FILE")"
            if am_i_root; then
                ! is_file_writable "$wp_config_path" && configure_permissions_ownership "$wp_config_path" -f "775" -u "$WEB_SERVER_DAEMON_USER" -g "root"
            else
                ! is_file_writable "$wp_config_path" && configure_permissions_ownership "$wp_config_path" -f "775"
            fi
            wordpress_set_db_settings
            # Make it non-writable again
            if am_i_root; then
                is_file_writable "$wp_config_path" && configure_permissions_ownership "$wp_config_path" -f "$wp_config_perms" -u "$WEB_SERVER_DAEMON_USER" -g "root"
            else
                is_file_writable "$wp_config_path" && configure_permissions_ownership "$wp_config_path" -f "$wp_config_perms"
            fi
        fi
        db_name="$(wordpress_conf_get "DB_NAME")"
        db_user="$(wordpress_conf_get "DB_USER")"
        db_pass="$(wordpress_conf_get "DB_PASSWORD")"
        db_host_port="$(wordpress_conf_get "DB_HOST")"
        db_host="${db_host_port%:*}"
        if [[ "$db_host_port" =~ :[0-9]+$ ]]; then
            # Use '##' to extract only the part after the last colon, to avoid any possible issues with IPv6 addresses
            db_port="${db_host_port##*:}"
        else
            db_port="$WORDPRESS_DATABASE_PORT_NUMBER"
        fi
        wordpress_wait_for_mysql_connection "$db_host" "$db_port" "$db_name" "$db_user" "$db_pass"
        wp_execute core update-db

        if is_boolean_yes "$WORDPRESS_RESET_DATA_PERMISSIONS"; then
            warn "Resetting file permissions in persisted volume"
            local wp_config_path
            wp_config_path="$(readlink -f "$WORDPRESS_CONF_FILE")"
            if am_i_root; then
                is_file_writable "$wp_config_path" && configure_permissions_ownership "$wp_config_path" -f "440" -u "$WEB_SERVER_DAEMON_USER" -g "root"
                configure_permissions_ownership "${WORDPRESS_VOLUME_DIR}/wp-content" -d "775" -f "664" -u "$WEB_SERVER_DAEMON_USER" -g "root"
            else
                is_file_writable "$wp_config_path" && configure_permissions_ownership "$wp_config_path" -f "440"
                configure_permissions_ownership "${WORDPRESS_VOLUME_DIR}/wp-content" -d "775" -f "664"
            fi
        else
            debug "Not resetting file permissions in persisted volume"
        fi
    fi

    # Avoid exit code of previous commands to affect the result of this function
    true
}

########################
# Executes the 'wp' CLI with the specified arguments and print result to stdout/stderr
# Globals:
#   WORDPRESS_*
# Arguments:
#   $1..$n - Arguments to pass to the CLI call
# Returns:
#   None
#########################
wp_execute_print_output() {
    # Avoid creating unnecessary cache files at initialization time
    local -a env=("env" "WP_CLI_CONFIG_PATH=${WP_CLI_CONF_FILE}" "WP_CLI_CACHE_DIR=/dev/null")
    local -a cmd=("${PHP_BIN_DIR}/php" "${WP_CLI_BIN_DIR}/wp-cli.phar" "$@")
    # Allow to specify extra CLI flags, but ensure they are added last
    local -a wp_extra_cli_flags
    read -r -a wp_extra_cli_flags <<<"$WORDPRESS_EXTRA_CLI_ARGS"
    [[ "${#wp_extra_cli_flags[@]}" -gt 0 ]] && cmd+=("${wp_extra_cli_flags[@]}")
    # Run as web server user to avoid having to change permissions/ownership afterwards
    if am_i_root; then
        run_as_user "$WEB_SERVER_DAEMON_USER" "${env[@]}" "${cmd[@]}"
    else
        "${env[@]}" "${cmd[@]}"
    fi
}

########################
# Executes the 'wp' CLI with the specified arguments
# Globals:
#   WORDPRESS_*
# Arguments:
#   $1..$n - Arguments to pass to the CLI call
# Returns:
#   None
#########################
wp_execute() {
    debug_execute wp_execute_print_output "$@"
}

########################
# Append configuration to the WordPress configuration file
# Globals:
#   WORDPRESS_*
# Arguments:
#   $1 - Configuration to append
# Returns:
#   None
#########################
wordpress_conf_append() {
    local -r conf="${1:?conf missing}"
    # This is basically escaping the newline character, for sed
    local conf_without_newlines
    conf_without_newlines="$(awk '{ printf "%s\\n", $0 }' <<<"$conf")"
    replace_in_file "$WORDPRESS_CONF_FILE" "(/\* That's all, stop editing\! .*)" "${conf_without_newlines}\n\1"
}

########################
# Add or modify an entry in the WordPress configuration file
# Globals:
#   WORDPRESS_*
# Arguments:
#   $1 - Variable name
#   $2 - Value to assign to the variable
#   $3 - Whether the value is a literal, or if instead it should be quoted (default: no)
# Returns:
#   None
#########################
wordpress_conf_set() {
    local -r key="${1:?key missing}"
    local -r value="${2:-}"
    local -r is_literal="${3:-no}"
    debug "Setting ${key} to '${value}' in WordPress configuration (literal: ${is_literal})"
    # Note: Using an empty --url to avoid any failure if the current URL is not properly configured
    local -a cmd=("wp_execute" "--url=http:" "config" "set" "$key" "$value")
    if is_boolean_yes "$is_literal"; then
        cmd+=("--raw")
    fi
    "${cmd[@]}"
}

########################
# Get an entry from the WordPress configuration file
# Globals:
#   WORDPRESS_*
# Arguments:
#   $1 - Variable name
# Returns:
#   None
#########################
wordpress_conf_get() {
    local -r key="${1:?key missing}"
    debug "Getting ${key} from WordPress configuration"
    # Use an empty URL to avoid any failure if the URL is not properly set
    wp_execute_print_output --url=http: config get "$key"
}

########################
# Wait until the database is accessible with the currently-known credentials
# Globals:
#   *
# Arguments:
#   $1 - database host
#   $2 - database port
#   $3 - database name
#   $4 - database username
#   $5 - database user password (optional)
# Returns:
#   true if the database connection succeeded, false otherwise
#########################
wordpress_wait_for_mysql_connection() {
    local -r db_host="${1:?missing database host}"
    local -r db_port="${2:?missing database port}"
    local -r db_name="${3:?missing database name}"
    local -r db_user="${4:?missing database user}"
    local -r db_pass="${5:-}"
    check_mysql_connection() {
        echo "SELECT 1" | mysql_remote_execute "$db_host" "$db_port" "$db_name" "$db_user" "$db_pass"
    }
    if ! retry_while "check_mysql_connection"; then
        error "Could not connect to the database"
        return 1
    fi
}

########################
# Disable the pingback functionality for WordPress
# Globals:
#   *
# Arguments:
#   None
# Returns:
#   None
#########################
wordpress_disable_pingback() {
    # Append logic to disable pingbacks at the end of the file
    # Must be added after wp-settings.php is require'd since it defines them
    # Also note that wp-config.php is officially indented via tabs, not spaces
    cat >>"$WORDPRESS_CONF_FILE" <<"EOF"

/**
 * Disable pingback.ping xmlrpc method to prevent WordPress from participating in DDoS attacks
 * More info at: https://docs.bitnami.com/general/apps/wordpress/troubleshooting/xmlrpc-and-pingback/
 */
if ( !defined( 'WP_CLI' ) ) {
	// remove x-pingback HTTP header
	add_filter("wp_headers", function($headers) {
		unset($headers["X-Pingback"]);
		return $headers;
	});
	// disable pingbacks
	add_filter( "xmlrpc_methods", function( $methods ) {
		unset( $methods["pingback.ping"] );
		return $methods;
	});
}
EOF
}

########################
# Configure reverse proxy headers
# Globals:
#   *
# Arguments:
#   None
# Returns:
#   None
#########################
wordpress_configure_reverse_proxy() {
    wordpress_conf_append "$(
        cat <<"EOF"
/**
 * Handle potential reverse proxy headers. Ref:
 *  - https://wordpress.org/support/article/faq-installation/#how-can-i-get-wordpress-working-when-im-behind-a-reverse-proxy
 *  - https://wordpress.org/support/article/administration-over-ssl/#using-a-reverse-proxy
 */
if ( ! empty( $_SERVER['HTTP_X_FORWARDED_HOST'] ) ) {
	$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST'];
}
if ( ! empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) \&\& 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
	$_SERVER['HTTPS'] = 'on';
}
EOF
    )"
}

########################
# Configure application URLs for WordPress (non-Multisite)
# Globals:
#   *
# Arguments:
#   None
# Returns:
#   None
#########################
wordpress_configure_urls() {
    # Set URL to dynamic value, depending on which host WordPress is accessed from (to be overridden later)
    # Note that wp-config.php is officially indented via tabs, not spaces
    wordpress_conf_append "$(
        cat <<"EOF"
/**
 * The WP_SITEURL and WP_HOME options are configured to access from any hostname or IP address.
 * If you want to access only from an specific domain, you can modify them. For example:
 *  define('WP_HOME','http://example.com');
 *  define('WP_SITEURL','http://example.com');
 *
 */
if ( defined( 'WP_CLI' ) ) {
	$_SERVER['HTTP_HOST'] = '127.0.0.1';
}
EOF
    )"
    local wp_url_protocol="http"
    (is_boolean_yes "$WORDPRESS_ENABLE_HTTPS" || [[ "$WORDPRESS_SCHEME" = "https" ]]) && wp_url_protocol="https"
    local wp_url_string="'${wp_url_protocol}://' . \$_SERVER['HTTP_HOST'] . '/'"
    wordpress_conf_set "WP_HOME" "$wp_url_string" yes
    wordpress_conf_set "WP_SITEURL" "$wp_url_string" yes
}

########################
# Configure SMTP
# Globals:
#   *
# Arguments:
#   None
# Returns:
#   None
#########################
wordpress_configure_smtp() {
    info "Enabling wp-mail-smtp plugin"
    local -a install_smtp_plugin_args
    if is_boolean_yes "$WORDPRESS_ENABLE_MULTISITE"; then
        install_smtp_plugin_args=("--activate-network")
    else
        install_smtp_plugin_args=("--activate")
    fi
    wp_execute plugin install wp-mail-smtp "${install_smtp_plugin_args[@]}"
    info "Configuring SMTP settings"
    wp_execute option patch update wp_mail_smtp mail from_email "$WORDPRESS_SMTP_FROM_EMAIL"
    wp_execute option patch update wp_mail_smtp mail from_name "$WORDPRESS_SMTP_FROM_NAME"
    wp_execute option patch update wp_mail_smtp mail mailer "smtp"
    wp_execute option patch insert wp_mail_smtp smtp host "$WORDPRESS_SMTP_HOST"
    wp_execute option patch insert wp_mail_smtp smtp port "$WORDPRESS_SMTP_PORT_NUMBER"
    wp_execute option patch insert wp_mail_smtp smtp encryption "$WORDPRESS_SMTP_PROTOCOL"
    wp_execute option patch insert wp_mail_smtp smtp user "$WORDPRESS_SMTP_USER"
    wp_execute option patch insert wp_mail_smtp smtp pass "$WORDPRESS_SMTP_PASSWORD"
    # Prevent WP Mail SMTP wizard to be launched after logging in the admin panel
    wp_execute option set wp_mail_smtp_activation_prevent_redirect 1
}

########################
# Apply web server configuration to host WordPress
# Globals:
#   *
# Arguments:
#   None
# Returns:
#   None
#########################
wordpress_generate_web_server_configuration() {
    # Web server config files will be generated twice, in order to properly support WORDPRESS_HTACCESS_OVERRIDE_NONE
    # At build time, htaccess files will not be moved - This will happen at runtime only if WORDPRESS_HTACCESS_OVERRIDE_NONE is enabled
    local -a web_server_config_create_flags
    if ! is_boolean_yes "$WORDPRESS_HTACCESS_OVERRIDE_NONE"; then
        # Enable .htaccess files
        web_server_config_create_flags+=("--apache-move-htaccess" "no" "--apache-allow-override" "All")
    else
        # Use htaccess.conf file loaded at web server startup
        web_server_config_create_flags+=("--apache-move-htaccess" "yes" "--apache-allow-override" "None")
    fi
    local apache_config nginx_config
    local template_dir="${BITNAMI_ROOT_DIR}/scripts/wordpress/bitnami-templates"
    # Fix themes/plugins usage
    apache_config="$(render-template "${template_dir}/apache-wordpress-volume-rewrite.conf.tpl")"
    nginx_config="$(render-template "${template_dir}/nginx-wordpress-volume-rewrite.conf.tpl")"
    nginx_external_config=""
    # Enable friendly URLs
    if ! is_boolean_yes "$WORDPRESS_ENABLE_MULTISITE"; then
        # Basic configuration (non-Multisite)
        apache_config+=$'\n'"$(render-template "${template_dir}/apache-wordpress-basic.conf.tpl")"
        nginx_config+=$'\n'"$(render-template "${template_dir}/nginx-wordpress-basic.conf.tpl")"
    elif [[ "$WORDPRESS_MULTISITE_NETWORK_TYPE" = "subfolder" || "$WORDPRESS_MULTISITE_NETWORK_TYPE" = "subdirectory" ]]; then
        # Multisite configuration for subfolder/subdirectory network type
        apache_config+=$'\n'"$(render-template "${template_dir}/apache-wordpress-multisite-subfolder.conf.tpl")"
        nginx_config+=$'\n'"$(render-template "${template_dir}/nginx-wordpress-multisite-subfolder.conf.tpl")"
        nginx_external_config+=$'\n'"$(render-template "${template_dir}/nginx-wordpress-multisite-subfolder-external.conf.tpl")"
    elif [[ "$WORDPRESS_MULTISITE_NETWORK_TYPE" = "subdomain" ]]; then
        # nip.io allows to create subdomains when WordPress Multisite is configured with an IP address
        # It only makes sense for WordPress Multisite when using subdomain network type
        # The redirection simply improves user experience so the site can be accessed via IP addresses without getting errors
        if is_boolean_yes "$WORDPRESS_MULTISITE_ENABLE_NIP_IO_REDIRECTION"; then
            apache_config+=$'\n'"$(render-template "${template_dir}/apache-nip-io-redirect.conf.tpl")"
            nginx_config+=$'\n'"$(render-template "${template_dir}/nginx-nip-io-redirect.conf.tpl")"
        fi
        # Multisite configuration for subdomain network type
        apache_config+=$'\n'"$(render-template "${template_dir}/apache-wordpress-multisite-subdomain.conf.tpl")"
        nginx_config+=$'\n'"$(render-template "${template_dir}/nginx-wordpress-multisite-subdomain.conf.tpl")"
        nginx_external_config+=$'\n'"$(render-template "${template_dir}/nginx-wordpress-multisite-subdomain-external.conf.tpl")"
    else
        error "Unknown WordPress Multisite network mode"
        return 1
    fi

    if ! is_boolean_yes "$WORDPRESS_ENABLE_XML_RPC"; then
        apache_config+=$'\n'"$(render-template "${template_dir}/apache-wordpress-disable-xml-rpc.tpl")"
        nginx_config+=$'\n'"$(render-template "${template_dir}/nginx-wordpress-disable-xml-rpc.tpl")"
    fi

    web_server_config_create_flags+=("--apache-extra-directory-configuration" "$apache_config" "--nginx-additional-configuration" "$nginx_config")
    [[ -n "$nginx_external_config" ]] && web_server_config_create_flags+=("--nginx-external-configuration" "$nginx_external_config")
    ensure_web_server_app_configuration_exists "wordpress" --type "php" "${web_server_config_create_flags[@]}"
}