captaincore/lib/remote-scripts/fetch-site-data
2026-06-19 18:43:52 -04:00

644 lines
No EOL
26 KiB
Bash

#!/usr/bin/env bash
#
# Fetches site data
#
# Returns key:value pairs, one per line.
# Values may contain JSON. Split on the first colon only.
#
if [ ! -f "wp-config.php" ]; then
echo "WordPress not found"
exit
fi
# Install CaptainCore helper if missing
helper_check=$( wp plugin list --status=must-use --skip-themes --skip-plugins --skip-packages | grep "captaincore-helper" )
if [[ "$helper_check" == "" ]]; then
curl https://run.captaincore.io/deploy-helper | bash -s
fi
plugins=$( wp plugin list --format=json --skip-themes --skip-plugins --skip-packages --fields=name,title,status,version )
themes=$(wp theme list --format=json --skip-themes --skip-plugins --skip-packages --fields=name,title,status,version )
mu_plugins=$( wp plugin list --status=must-use --format=json --skip-themes --skip-plugins --skip-packages --fields=name,title,status,version 2>/dev/null )
[[ -z "$mu_plugins" || "$mu_plugins" == "[]" ]] && mu_plugins="[]"
core=$( wp core version --skip-themes --skip-plugins --skip-packages )
home_url=$( wp option get home --skip-themes --skip-plugins --skip-packages )
users=$( wp user list --format=json --skip-themes --skip-plugins --skip-packages )
core_verify_checksums_response=$( wp core verify-checksums --skip-themes --skip-plugins --skip-packages 2>&1 )
# Parse checksum output into categorized arrays
modified_files=""
extra_files=""
missing_files=""
modified_paths=()
extra_paths=()
while IFS= read -r line; do
case "$line" in
"Warning: File doesn't verify against checksum: "*)
file=$( echo "$line" | sed "s/Warning: File doesn't verify against checksum: //" )
modified_files="${modified_files:+$modified_files,}\"$file\""
modified_paths+=("$file")
;;
"Warning: File should not exist: "*)
file=$( echo "$line" | sed "s/Warning: File should not exist: //" )
extra_files="${extra_files:+$extra_files,}\"$file\""
extra_paths+=("$file")
;;
"Warning: File doesn't exist: "*)
file=$( echo "$line" | sed "s/Warning: File doesn't exist: //" )
missing_files="${missing_files:+$missing_files,}\"$file\""
;;
esac
done <<< "$core_verify_checksums_response"
# Determine status: "pass", "warning" (missing files only), or "fail" (modified or extra files)
if [[ -n "$modified_files" ]] || [[ -n "$extra_files" ]]; then
core_verify_checksums=0
checksum_status="fail"
elif [[ -n "$missing_files" ]]; then
core_verify_checksums=1
checksum_status="warning"
else
core_verify_checksums=1
checksum_status="pass"
fi
# Build JSON with categorized details
core_verify_checksums_json="{\"status\":\"$checksum_status\",\"modified\":[$modified_files],\"extra\":[$extra_files],\"missing\":[$missing_files]}"
# Plugin checksum verification against wordpress.org published hashes.
# Only plugins hosted on wordpress.org have published checksums; premium
# plugins without a checksums.json produce "skipping" warnings which are
# NOT failures. Genuine tampering is reported as tab-delimited rows in the
# format: slug<TAB>file<TAB>message.
plugin_verify_checksums_response=$( wp plugin verify-checksums --all --skip-themes --skip-plugins --skip-packages 2>&1 )
plugin_modified_entries=""
plugin_skipped_count=0
json_escape() {
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
printf '%s' "$s"
}
while IFS= read -r line; do
[[ -z "$line" ]] && continue
# Skip the header row that WP-CLI emits when there are failures
[[ "$line" == "plugin_name"$'\t'* ]] && continue
case "$line" in
"Warning: Couldn't fetch response from"*)
# HTTP fetch for the checksums JSON failed; paired with the
# "Could not retrieve" line below. Ignored here to avoid double-counting.
;;
"Warning: Could not retrieve the checksums"*)
plugin_skipped_count=$((plugin_skipped_count + 1))
;;
"Warning: Plugin "*"doesn't provide a readme.txt"*)
plugin_skipped_count=$((plugin_skipped_count + 1))
;;
"Success:"*|"Error:"*)
# Terminal status lines are informational only
;;
*$'\t'*$'\t'*)
# Tab-delimited failure row: slug<TAB>file<TAB>message
slug="${line%%$'\t'*}"
rest="${line#*$'\t'}"
file="${rest%%$'\t'*}"
message="${rest#*$'\t'}"
slug_esc=$( json_escape "$slug" )
file_esc=$( json_escape "$file" )
message_esc=$( json_escape "$message" )
entry="{\"slug\":\"${slug_esc}\",\"file\":\"${file_esc}\",\"message\":\"${message_esc}\"}"
plugin_modified_entries="${plugin_modified_entries:+$plugin_modified_entries,}${entry}"
;;
esac
done <<< "$plugin_verify_checksums_response"
if [[ -n "$plugin_modified_entries" ]]; then
plugin_checksum_status="fail"
else
plugin_checksum_status="pass"
fi
plugin_checksum_details_json="{\"status\":\"${plugin_checksum_status}\",\"modified\":[${plugin_modified_entries}],\"skipped_count\":${plugin_skipped_count}}"
db_name=$( wp eval "echo DB_NAME;" --skip-plugins --skip-themes --skip-packages )
db_username=$( wp eval "echo DB_USER;" --skip-plugins --skip-themes --skip-packages )
db_password=$( wp eval "echo DB_PASSWORD;" --skip-plugins --skip-themes --skip-packages )
if $( wp core is-installed --network --skip-themes --skip-plugins --skip-packages ); then
subsite_count=$( wp site list --format=count --skip-themes --skip-plugins --skip-packages )
fi
php_version=$( php -v | head -n1 | cut -d ' ' -f 2 | cut -d '.' -f 1,2 )
if [ -f "/etc/php/$php_version/fpm/php.ini" ]; then
php_memory=$( cat "/etc/php/$php_version/fpm/php.ini" | grep "memory_limit" )
fi
if [[ "$php_memory" == "" ]]; then
php_ini=$( wp eval 'echo php_ini_loaded_file();' )
php_memory=$( cat "$php_ini" | grep "memory_limit" )
fi
read -r -d '' php_code << heredoc
\$line = '$php_memory';
preg_match( '/memory_limit = (\d\w+)/', '$php_memory', \$matches );
echo \$matches[1];
heredoc
php_memory=$( php -r "$php_code" )
default_role=$( wp option get default_role --skip-themes --skip-plugins --skip-packages )
if $( wp core is-installed --network --skip-themes --skip-plugins --skip-packages 2>/dev/null ); then
registration=$( wp site option get registration --skip-themes --skip-plugins --skip-packages 2>/dev/null )
else
registration=$( wp option get users_can_register --skip-themes --skip-plugins --skip-packages 2>/dev/null )
fi
token=$( wp eval "echo md5( AUTH_KEY );" --skip-plugins --skip-themes )
# Output key:value pairs (trim whitespace from WP-CLI output)
trim() { printf '%s' "$1" | tr -d '\n\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'; }
echo "plugins:$(trim "$plugins")"
echo "themes:$(trim "$themes")"
echo "mu_plugins:$(trim "$mu_plugins")"
echo "core:$(trim "$core")"
echo "home_url:$(trim "$home_url")"
echo "users:$(trim "$users")"
echo "database_name:${db_name}"
echo "database_username:${db_username}"
echo "database_password:${db_password}"
echo "core_verify_checksums:${core_verify_checksums}"
echo "subsite_count:$(trim "$subsite_count")"
echo "php_memory:${php_memory}"
echo "default_role:${default_role}"
echo "registration:${registration}"
echo "token:${token}"
echo "core_checksum_details:${core_verify_checksums_json}"
echo "plugin_checksum_details:${plugin_checksum_details_json}"
echo "php_version:${php_version}"
echo "db_size:$(wp db size --size_format=b --skip-themes --skip-plugins --skip-packages 2>/dev/null | tail -1 | cut -f2)"
# Security log table size (bytes) and row count from information_schema
security_log_result=$(wp db query "SELECT data_length + index_length, table_rows FROM information_schema.TABLES WHERE table_schema = DATABASE() AND table_name LIKE '%captaincore_security_log'" --skip-column-names --batch --skip-plugins --skip-themes 2>/dev/null | grep -v '^$')
if [[ -n "$security_log_result" ]]; then
security_log_bytes=$(echo "$security_log_result" | awk '{print $1}')
security_log_rows=$(echo "$security_log_result" | awk '{print $2}')
echo "security_log:{\"size\":${security_log_bytes},\"rows\":${security_log_rows}}"
else
echo "security_log:{\"size\":0,\"rows\":0}"
fi
# Error log manifest: name, size (bytes), and line count for each error log file
read -r -d '' error_log_php << 'ERRLOGEOF'
<?php
$files = [];
$home_path = get_home_path();
$parent = dirname( $home_path );
$wpc = empty( WP_CONTENT_DIR ) ? "{$home_path}wp-content" : WP_CONTENT_DIR;
$wpc_base = basename( $wpc );
if (! function_exists('str_ends_with')) {
function str_ends_with(string $h, string $n): bool {
$l = strlen($n);
return ($l === 0 || 0 === substr_compare($h, $n, -$l));
}
}
function log_entry( $name, $path ) {
$size = filesize( $path );
$lines = 0;
$fh = fopen( $path, 'r' );
if ( $fh ) {
while ( fgets( $fh ) !== false ) { $lines++; }
fclose( $fh );
}
return [ "name" => $name, "size" => $size, "lines" => $lines ];
}
if ( is_file( "{$home_path}error_log" ) && filesize( "{$home_path}error_log" ) > 0 ) {
$files[] = log_entry( "error_log", "{$home_path}error_log" );
}
foreach ( glob( "{$parent}/logs/*error.log*" ) as $f ) {
if ( filesize( $f ) == 0 || str_ends_with( $f, '.gz' ) ) { continue; }
$files[] = log_entry( "~/logs/" . basename( $f ), $f );
}
if ( is_file( "{$wpc}/debug.log" ) && filesize( "{$wpc}/debug.log" ) > 0 ) {
$files[] = log_entry( "{$wpc_base}/debug.log", "{$wpc}/debug.log" );
}
echo json_encode( $files );
ERRLOGEOF
error_logs_json=$(echo "$error_log_php" | wp eval-file - --skip-themes --skip-plugins 2>/dev/null)
if [[ -z "$error_logs_json" ]]; then
error_logs_json="[]"
fi
echo "error_logs:${error_logs_json}"
# Session / privilege signal — compromise telemetry for WP Registry session-anomaly detection.
# Efficient by design: NEVER loads all users. Scans capability ROLE definitions + a targeted
# capabilities-meta query, then pulls session_tokens only for the (tiny) privileged set, so
# cost is ~#privileged-users not #total-users (proven ~3s / <2KB on a 135k-user site).
# Hunts by effective CAPABILITY not role label: catches subscribers/customers with injected
# admin caps + low roles silently redefined with takeover caps. injected_users = the backdoor.
read -r -d '' session_signal_php <<'SESSIONEOF'
<?php
global $wpdb;
// Capability tiers. CODE_TAKEOVER = site/code takeover (arbitrary PHP, config, plugin/theme).
// USER_TAKEOVER = user-management (lower confidence — legit "member managers" hold these).
$CODE_TAKEOVER = [ 'manage_options','install_plugins','install_themes','update_plugins','update_core',
'activate_plugins','edit_plugins','edit_themes','edit_files' ];
$USER_TAKEOVER = [ 'edit_users','promote_users','delete_users','create_users','remove_users' ];
$TAKEOVER = array_merge( $CODE_TAKEOVER, $USER_TAKEOVER );
$CONTENT = [ 'unfiltered_html' ];
// Core admin caps used to recognize an "effective administrator" however their privileges are
// stored (real role slug, legacy level_10, or a User-Role-Editor cap-bag with no admin slug).
$CORE_ADMIN = [ 'manage_options','install_plugins','edit_plugins','update_core','edit_users','edit_files','activate_plugins' ];
// Pass 1: classify ROLE definitions (catches a redefined low role carrying takeover caps).
$roles = get_option( $wpdb->prefix . 'user_roles', [] );
$all_role_slugs = array_keys( (array) $roles );
$admin_roles = []; $content_only_roles = []; $role_audit = [];
foreach ( (array) $roles as $rk => $rd ) {
$caps = isset( $rd['capabilities'] ) ? array_keys( array_filter( (array) $rd['capabilities'] ) ) : [];
$danger = array_values( array_intersect( $caps, array_merge( $TAKEOVER, $CONTENT ) ) );
if ( array_intersect( $caps, $TAKEOVER ) ) { $admin_roles[] = $rk; $role_audit[$rk] = $danger; }
elseif ( array_intersect( $caps, $CONTENT ) ) { $content_only_roles[] = $rk; $role_audit[$rk] = $danger; }
}
$admin_role_set = array_unique( array_merge( $admin_roles, [ 'administrator', 'super_admin' ] ) );
// Pass 2: targeted capabilities-meta scan for admin-capable roles OR direct takeover caps.
$tokens = array_unique( array_merge( $admin_role_set, $TAKEOVER ) );
$likes = [];
foreach ( $tokens as $t ) { $likes[] = $wpdb->prepare( "meta_value LIKE %s", '%' . $wpdb->esc_like( $t ) . '%' ); }
$rows = $wpdb->get_results( "SELECT user_id, meta_value FROM {$wpdb->usermeta}
WHERE meta_key = '{$wpdb->prefix}capabilities' AND (" . implode( ' OR ', $likes ) . ")" );
$cap_user_ids = []; $admin_ids = []; $injected = []; $user_role_map = [];
foreach ( $rows as $r ) {
$caps = maybe_unserialize( $r->meta_value );
if ( ! is_array( $caps ) ) { continue; }
$ur = array_keys( array_filter( $caps ) );
// Separate the user's actual ROLE slugs from individually-granted capabilities. wp_capabilities
// stores role slugs + directly-added caps; role-INHERITED caps are resolved at runtime, not stored.
$base_roles = array_values( array_intersect( $ur, $all_role_slugs ) );
$extra_caps = array_values( array_diff( $ur, $all_role_slugs ) );
$has_takeover = (bool) array_intersect( $ur, $TAKEOVER );
$via_admin_role = (bool) array_intersect( $base_roles, $admin_role_set );
if ( ! $has_takeover && ! $via_admin_role ) { continue; }
$uid = (int) $r->user_id;
$cap_user_ids[] = $uid;
$user_role_map[$uid] = $base_roles;
// Effective administrator: an admin role, legacy level_10, or holds most core-admin caps —
// regardless of how privileges are stored. These ARE admins (count in the admin tier), not backdoors.
$is_effective_admin = $via_admin_role || in_array( 'level_10', $ur, true )
|| count( array_intersect( $ur, $CORE_ADMIN ) ) >= 4;
if ( $is_effective_admin ) { $admin_ids[] = $uid; }
// Injected-capability backdoor: a NON-admin account carrying an individually-granted CODE/CONFIG
// takeover cap (manage_options, install_plugins, edit_themes/files, ...). Role-inherited caps,
// user-management-only grants, and effective-admins are excluded — those were the false positives
// on the first fleet run (User-Role-Editor cap-bags, "member manager" roles).
if ( ! $is_effective_admin ) {
$injected_code = array_values( array_intersect( $extra_caps, $CODE_TAKEOVER ) );
if ( $injected_code ) {
$injected[] = [ 'user_id' => $uid, 'base_roles' => $base_roles,
'extra_caps' => array_values( array_intersect( $extra_caps, $TAKEOVER ) ),
'injected_caps' => $injected_code ];
}
}
}
// Pull identities + sessions only for the (small) privileged set.
$accounts = [];
$tier = [ 'administrator' => [ 'users' => 0, 's' => 0, 'ips' => [] ],
'admin_capable' => [ 'users' => 0, 's' => 0, 'ips' => [] ] ];
if ( $cap_user_ids ) {
$in = implode( ',', array_map( 'intval', $cap_user_ids ) );
$info = $wpdb->get_results( "SELECT ID, user_login, user_registered FROM {$wpdb->users} WHERE ID IN ($in)" );
foreach ( $info as $u ) {
$uid = (int) $u->ID;
$is_admin = in_array( $uid, $admin_ids, true );
$sessions = get_user_meta( $uid, 'session_tokens', true );
$s_out = [];
if ( is_array( $sessions ) ) {
foreach ( $sessions as $s ) {
$ip = $s['ip'] ?? '';
$s_out[] = [
'ip' => $ip,
'login' => isset( $s['login'] ) ? date( 'Y-m-d H:i:s', $s['login'] ) : '',
'expiration' => isset( $s['expiration'] ) ? date( 'Y-m-d H:i:s', $s['expiration'] ) : '',
];
$tier['admin_capable']['s']++;
if ( $ip ) { $tier['admin_capable']['ips'][ $ip ] = 1; }
if ( $is_admin ) {
$tier['administrator']['s']++;
if ( $ip ) { $tier['administrator']['ips'][ $ip ] = 1; }
}
}
}
$tier['admin_capable']['users']++;
if ( $is_admin ) { $tier['administrator']['users']++; }
$accounts[] = [
'login' => $u->user_login,
'roles' => array_values( $user_role_map[ $uid ] ?? [] ),
'registered' => $u->user_registered,
'is_admin' => $is_admin,
'sessions' => $s_out,
];
}
}
$out = [
'collected_at' => gmdate( 'Y-m-d\TH:i:s\Z' ),
'total_users' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->users}" ),
'session_token_rows' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key = 'session_tokens'" ),
'administrator' => [ 'users' => $tier['administrator']['users'], 'active_sessions' => $tier['administrator']['s'], 'unique_ips' => count( $tier['administrator']['ips'] ) ],
'admin_capable' => [ 'users' => $tier['admin_capable']['users'], 'active_sessions' => $tier['admin_capable']['s'], 'unique_ips' => count( $tier['admin_capable']['ips'] ) ],
'admin_capable_roles' => array_values( $admin_roles ),
'content_only_roles' => array_values( $content_only_roles ),
'redefined_role_audit' => (object) $role_audit,
'super_admins' => function_exists( 'get_super_admins' ) ? array_values( get_super_admins() ) : [],
'injected_users' => $injected,
'accounts' => $accounts,
];
echo wp_json_encode( $out );
SESSIONEOF
session_signal_json=$(echo "$session_signal_php" | wp eval-file - --skip-themes --skip-plugins 2>/dev/null)
if [[ -z "$session_signal_json" ]]; then
session_signal_json="{}"
fi
echo "session_signal:${session_signal_json}"
# Per-component SHA256 content hashes for plugins, themes, and mu-plugins
# Uses -type f to hash regular files, and -type l to include symlink targets/paths.
# Symlinks are hashed as "SYMLINK:<target>" so a symlink change alters the directory hash.
hash_directory() {
local dir="$1"
{
find "$dir" -type f \
! -path '*/node_modules/*' \
! -path '*/.git/*' \
! -name .DS_Store \
! -name error_log \
-exec sha256sum {} + 2>/dev/null
find "$dir" -type l \
! -path '*/node_modules/*' \
! -path '*/.git/*' \
2>/dev/null | while IFS= read -r link; do
target=$(readlink "$link")
echo "$(echo -n "SYMLINK:${target}" | sha256sum | awk '{print $1}') ${link}"
done
} | LC_ALL=C sort -k2 | sha256sum | awk '{print $1}'
}
# Compute hashes from within wp-content so paths are relative (plugins/slug/file.php)
pushd wp-content > /dev/null 2>&1
component_hashes="{"
first=true
# Hash each plugin directory
if [[ -d "plugins" ]]; then
for dir in plugins/*/; do
[[ -d "$dir" ]] || continue
slug=$(basename "$dir")
hash=$(hash_directory "$dir")
if [[ "$first" == true ]]; then first=false; else component_hashes+=","; fi
component_hashes+="\"${slug}\":\"${hash}\""
done
fi
# Hash each theme directory
if [[ -d "themes" ]]; then
for dir in themes/*/; do
[[ -d "$dir" ]] || continue
slug=$(basename "$dir")
hash=$(hash_directory "$dir")
if [[ "$first" == true ]]; then first=false; else component_hashes+=","; fi
component_hashes+="\"${slug}\":\"${hash}\""
done
fi
# Hash individual mu-plugin components
# Auto-detect: each .php file is a component; include matching directory + companion files
if [[ -d "mu-plugins" ]]; then
for phpfile in mu-plugins/*.php; do
[[ -f "$phpfile" ]] || continue
slug=$(basename "$phpfile" .php)
# Collect hashes for all files belonging to this component
component_file_hashes=$(
# The .php entry file
sha256sum "$phpfile" 2>/dev/null
# Matching directory (slug/)
if [[ -d "mu-plugins/$slug" ]]; then
find "mu-plugins/$slug" -type f \
! -path '*/node_modules/*' ! -path '*/.git/*' \
! -name .DS_Store ! -name error_log \
-exec sha256sum {} + 2>/dev/null
find "mu-plugins/$slug" -type l \
! -path '*/node_modules/*' ! -path '*/.git/*' \
2>/dev/null | while IFS= read -r link; do
target=$(readlink "$link")
echo "$(echo -n "SYMLINK:${target}" | sha256sum | awk '{print $1}') ${link}"
done
fi
# Companion files (same basename, different extension: .js, .css, etc.)
for companion in mu-plugins/${slug}.*; do
[[ -f "$companion" && "$companion" != "$phpfile" ]] || continue
sha256sum "$companion" 2>/dev/null
done
)
hash=$(echo "$component_file_hashes" | grep -v '^$' | LC_ALL=C sort -k2 | sha256sum | awk '{print $1}')
if [[ "$first" == true ]]; then first=false; else component_hashes+=","; fi
component_hashes+="\"mu:${slug}\":\"${hash}\""
done
fi
component_hashes+="}"
popd > /dev/null 2>&1
echo "component_hashes:${component_hashes}"
# MU-plugin file listing (root-level files and directories only)
pushd wp-content > /dev/null 2>&1
mu_plugin_files="{"
mu_first=true
if [[ -d "mu-plugins" ]]; then
for entry in mu-plugins/*; do
[[ -e "$entry" ]] || continue
base=$(basename "$entry")
[[ "$base" == ".DS_Store" ]] && continue
if [[ "$mu_first" == true ]]; then mu_first=false; else mu_plugin_files+=","; fi
if [[ -d "$entry" ]]; then
mu_plugin_files+="\"${base}/\":true"
else
mu_plugin_files+="\"${base}\":true"
fi
done
fi
mu_plugin_files+="}"
popd > /dev/null 2>&1
echo "mu_plugin_files:${mu_plugin_files}"
# Per-file SHA256 hashes for core extra/modified files
core_file_hashes="{"
core_hash_first=true
for file in "${extra_paths[@]}" "${modified_paths[@]}"; do
[[ -z "$file" || ! -f "$file" ]] && continue
hash=$(sha256sum "$file" 2>/dev/null | awk '{print $1}')
[[ -z "$hash" ]] && continue
if [[ "$core_hash_first" == true ]]; then core_hash_first=false; else core_file_hashes+=","; fi
core_file_hashes+="\"${file}\":\"${hash}\""
done
core_file_hashes+="}"
echo "core_file_hashes:${core_file_hashes}"
# Loose PHP file hashes (everything in wp-content/ outside plugins/themes/mu-plugins)
pushd wp-content > /dev/null 2>&1
loose_hashes="{"
loose_first=true
while IFS= read -r file; do
[[ -z "$file" ]] && continue
hash=$(sha256sum "$file" 2>/dev/null | awk '{print $1}')
[[ -z "$hash" ]] && continue
if [[ "$loose_first" == true ]]; then loose_first=false; else loose_hashes+=","; fi
loose_hashes+="\"${file}\":\"${hash}\""
done < <(find . -name "*.php" -type f \
! -path './plugins/*' \
! -path './themes/*' \
! -path './mu-plugins/*' \
! -path './cache/*' \
! -path './wps-cache/*' \
! -path './wphb-cache/*' \
! -path './updraft/*' \
! -path './upgrade-temp-backup/*' \
! -path './umbrella-upgrade-temp-backup/*' \
2>/dev/null | sed 's|^\./||' | LC_ALL=C sort)
loose_hashes+="}"
popd > /dev/null 2>&1
echo "loose_file_hashes:${loose_hashes}"
# Restic cache size (bytes), 0 if none
restic_cache_dir="$HOME/.cache/restic"
if [[ -d "$restic_cache_dir" ]]; then
echo "restic_cache:$(du -sb "$restic_cache_dir" 2>/dev/null | cut -f1)"
else
echo "restic_cache:0"
fi
# Discover sensitive pages (checkout/cart/account) for active ecommerce plugins.
# Rules kept in sync with lib/capture-plugin-pages.json on the CaptainCore server.
# Result is a JSON array of home_url-relative paths consumed by `capture generate`.
read -r -d '' capture_pages_php << 'CAPPAGESEOF'
<?php
$rules = [
[
'slug' => 'woocommerce',
'discover' => [
['type' => 'option_page_id', 'option' => 'woocommerce_checkout_page_id'],
['type' => 'option_page_id', 'option' => 'woocommerce_cart_page_id'],
['type' => 'option_page_id', 'option' => 'woocommerce_myaccount_page_id'],
],
],
[
'slug' => 'easy-digital-downloads',
'discover' => [
['type' => 'option_array_page_id', 'option' => 'edd_settings', 'key' => 'purchase_page'],
['type' => 'option_array_page_id', 'option' => 'edd_settings', 'key' => 'success_page'],
],
],
[
'slug' => 'give',
'discover' => [
['type' => 'option_array_page_id', 'option' => 'give_settings', 'key' => 'success_page'],
['type' => 'option_array_page_id', 'option' => 'give_settings', 'key' => 'failure_page'],
],
],
];
$is_slug_active = function ($slug) {
$active = (array) get_option('active_plugins', []);
foreach ($active as $path) {
if (dirname($path) === $slug) {
return true;
}
}
if (is_multisite()) {
$network = (array) get_site_option('active_sitewide_plugins', []);
foreach (array_keys($network) as $path) {
if (dirname($path) === $slug) {
return true;
}
}
}
return false;
};
$resolve_page_id = function ($id) {
$id = (int) $id;
if ($id <= 0) {
return null;
}
$url = get_permalink($id);
if (! $url) {
return null;
}
$home = untrailingslashit(home_url());
if (strpos($url, $home) === 0) {
$path = substr($url, strlen($home));
} else {
$path = parse_url($url, PHP_URL_PATH);
}
if ($path === '' || $path === null) {
$path = '/';
}
if ($path[0] !== '/') {
$path = '/' . $path;
}
return $path;
};
$paths = [];
foreach ($rules as $rule) {
if (! $is_slug_active($rule['slug'])) {
continue;
}
foreach ($rule['discover'] as $d) {
$page_id = 0;
if ($d['type'] === 'option_page_id') {
$page_id = (int) get_option($d['option'], 0);
} elseif ($d['type'] === 'option_array_page_id') {
$opt = get_option($d['option'], []);
if (is_array($opt) && isset($opt[$d['key']])) {
$page_id = (int) $opt[$d['key']];
}
}
$path = $resolve_page_id($page_id);
if ($path !== null && ! in_array($path, $paths, true)) {
$paths[] = $path;
}
}
}
echo json_encode($paths);
CAPPAGESEOF
capture_plugin_pages_json=$(echo "$capture_pages_php" | wp eval-file - --skip-themes --skip-plugins 2>/dev/null)
if [[ -z "$capture_plugin_pages_json" ]]; then
capture_plugin_pages_json="[]"
fi
echo "capture_plugin_pages:${capture_plugin_pages_json}"