true, 'kdf' => array( 'algo' => 'pbkdf2-sha256', 'iter' => Crypto::ITER ), 'salt' => base64_encode( $enc['salt'] ), 'verify_iv' => base64_encode( $iv ), // Lets restore reject a wrong password before touching anything. 'verify' => base64_encode( Crypto::enc( 'AISV-OK', $enc['key'], $iv ) ), ); } $inv = array(); if ( ! empty( $inventory ) ) { $inv['inventory'] = array( 'tables' => isset( $inventory['tables'] ) ? array_values( array_map( 'strval', (array) $inventory['tables'] ) ) : array(), 'file_count' => isset( $inventory['file_count'] ) ? (int) $inventory['file_count'] : 0, ); } // G1 incremental fields (cycle/sequence/base_file/deletions) when provided. $extra = is_array( $extra ) ? $extra : array(); return $crypto + $inv + $extra + array( 'format' => self::FORMAT, 'plugin' => 'ai-siteark', 'version' => defined( 'AISITEARK_VERSION' ) ? AISITEARK_VERSION : '', 'wp_version' => get_bloginfo( 'version' ), 'php_version' => PHP_VERSION, // DB server so restore can flag a downgrade (e.g. MySQL 8 → MariaDB) // whose collations/syntax would break the import. 'db_server_info' => is_object( $wpdb ) && method_exists( $wpdb, 'db_server_info' ) ? (string) $wpdb->db_server_info() : '', 'db_version' => is_object( $wpdb ) ? (string) $wpdb->db_version() : '', 'site_url' => site_url(), 'home_url' => home_url(), 'abspath' => ABSPATH, 'content_dir' => WP_CONTENT_DIR, 'table_prefix' => $wpdb->prefix, 'multisite' => is_multisite(), 'charset' => $wpdb->charset, 'scope' => $scope, 'created_at' => gmdate( 'c' ), ); } /** * The backup's declared inventory ({ tables, file_count }), or null when the * manifest predates inventory support (older backups skip missing-item checks). * * @param array $m Manifest. * @return array|null */ public static function inventory( array $m ) { return ( isset( $m['inventory'] ) && is_array( $m['inventory'] ) ) ? $m['inventory'] : null; } /** * Validate a decoded manifest before a restore. * * @param mixed $m Decoded manifest. * @return true|\WP_Error */ public static function validate( $m ) { // Accept backups from BOTH product ids: the current slug (ai-siteark) and the legacy // bridge slug (ai-sitevault). They are the same product — a site updated in place from // AI-SiteVault runs AI-SiteArk code under the old slug and tags its manifests // 'ai-sitevault' — so a backup (uploaded, URL-fetched, or pulled via site-to-site // migration) made on either build must restore on either build. Without this, a token // pull from an AI-SiteVault source failed with "missing a valid AI-SiteArk manifest". if ( ! is_array( $m ) || ! in_array( $m['plugin'] ?? '', array( 'ai-siteark', 'ai-sitevault' ), true ) ) { return new \WP_Error( 'aisiteark_manifest', __( 'This archive is missing a valid AI-SiteArk manifest.', 'ai-siteark' ) ); } if ( (int) ( $m['format'] ?? 0 ) > self::FORMAT ) { return new \WP_Error( 'aisiteark_manifest_format', __( 'This backup was made by a newer version of AI-SiteArk. Please update first.', 'ai-siteark' ) ); } return true; } /** * Whether a manifest describes an encrypted backup. * * @param array $m Manifest. * @return bool */ public static function is_encrypted( array $m ) { return ! empty( $m['encrypted'] ); } /** * Verify a password against the manifest's check token. * * @param array $m Manifest. * @param string $password Password. * @return string|false Derived key on success, false on wrong password. */ public static function verify_password( array $m, $password ) { if ( empty( $m['encrypted'] ) ) { return ''; } if ( ! Crypto::available() || '' === (string) $password ) { return false; } $key = Crypto::key( $password, base64_decode( $m['salt'] ?? '' ) ); $dec = Crypto::dec( base64_decode( $m['verify'] ?? '' ), $key, base64_decode( $m['verify_iv'] ?? '' ) ); return ( 'AISV-OK' === $dec ) ? $key : false; } /** * Whether restoring this manifest onto the current site is a cross-site * migration (different URL or path), i.e. search-replace is needed. * * @param array $m Manifest. * @return bool */ public static function is_migration( array $m ) { $src_url = isset( $m['site_url'] ) ? untrailingslashit( (string) $m['site_url'] ) : ''; $src_path = isset( $m['abspath'] ) ? (string) $m['abspath'] : ''; return ( $src_url && untrailingslashit( site_url() ) !== $src_url ) || ( $src_path && ABSPATH !== $src_path ); } }