Plugin Directory

Changeset 3699356


Ignore:
Timestamp:
09/16/2026 11:50:17 PM (5 weeks ago)
Author:
bilalhameed001
Message:

Release 2.1.4: Updraft list_archives with signed download URLs for Hosting migrate pull.

Location:
my-agent-crew-connector/trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • my-agent-crew-connector/trunk/includes/capability-manifests/updraftplus.php

    r3668824 r3699356  
    33        'schema'      => 'cbregen-capability-manifest@1',
    44        'manifest_id' => 'updraftplus_core',
    5         'version'     => '1.1.0',
     5        'version'     => '1.2.0',
    66        'slug'        => 'updraftplus',
    77        'source'      => array(
    … …  
    1111                'key'               => 'adapter:updraftplus',
    1212                'label'             => 'UpdraftPlus (PHP adapter)',
    13                 'description'       => 'Backup Now, list backups, restore, and status via UpdraftPlus PHP (shared hosting; no WP-CLI required).',
     13                'description'       => 'Backup Now, list backups, list archives (signed download URLs for migrate), restore, and status via UpdraftPlus PHP.',
    1414                'legacy_enable_keys'=> array( 'cli:updraftplus' ),
    1515        ),
    … …  
    7575                ),
    7676                array(
     77                        'id'          => 'list_archives',
     78                        'name'        => 'adapter_updraftplus_list_archives',
     79                        'description' => 'List local Updraft archive filenames for a backup set and return short-lived signed download URLs (for MyAgentCrew Hosting migrate pull). Prefers newest local_ready backup if timestamp omitted. Skips encrypted .crypt files. Poll after Backup Now finishes.',
     80                        'inputSchema' => array(
     81                                'type'       => 'object',
     82                                'properties' => array(
     83                                        'timestamp' => array(
     84                                                'type'        => 'integer',
     85                                                'description' => 'Backup timestamp/key from list_backups. Optional — defaults to newest local_ready.',
     86                                        ),
     87                                        'backupKey' => array(
     88                                                'type'        => 'string',
     89                                                'description' => 'Alias for timestamp.',
     90                                        ),
     91                                        'ttl'       => array(
     92                                                'type'        => 'integer',
     93                                                'description' => 'Signed URL lifetime in seconds (default 7200, max 86400).',
     94                                        ),
     95                                ),
     96                        ),
     97                        'execute'     => array(
     98                                'primitive' => 'callback',
     99                                'handler'   => 'updraftplus_list_archives',
     100                        ),
     101                ),
     102                array(
    77103                        'id'          => 'restore_backup',
    78104                        'name'        => 'adapter_updraftplus_restore_backup',
  • my-agent-crew-connector/trunk/includes/class-cbregen-capability-runtime.php

    r3668824 r3699356  
    219219                        'updraftplus_backup_status'   => array( 'CBRegen_UpdraftPlus_Adapter', 'backup_status' ),
    220220                        'updraftplus_list_backups'    => array( 'CBRegen_UpdraftPlus_Adapter', 'list_backups' ),
     221                        'updraftplus_list_archives'   => array( 'CBRegen_UpdraftPlus_Adapter', 'list_archives' ),
    221222                        'updraftplus_restore_backup'  => array( 'CBRegen_UpdraftPlus_Adapter', 'restore_backup' ),
    222223                        'updraftplus_restore_status'  => array( 'CBRegen_UpdraftPlus_Adapter', 'restore_status' ),
  • my-agent-crew-connector/trunk/includes/class-cbregen-updraftplus-adapter.php

    r3668824 r3699356  
    1414        const RESTORE_JOB_OPTION = 'cbregen_updraft_adapter_restore_job';
    1515        const RESTORE_CRON_HOOK  = 'cbregen_updraftplus_do_restore';
     16        /** Signed archive download TTL (seconds). Large backups need a long window. */
     17        const ARCHIVE_URL_TTL    = 7200;
    1618
    1719        /**
    … …  
    2022        public static function boot() {
    2123                add_action( self::RESTORE_CRON_HOOK, array( __CLASS__, 'execute_restore_job' ), 10, 1 );
     24                add_action( 'rest_api_init', array( __CLASS__, 'register_stream_routes' ) );
     25        }
     26
     27        /**
     28         * Public signed download routes for Provision pull (no Bearer — HMAC token).
     29         */
     30        public static function register_stream_routes() {
     31                foreach ( array( 'my-agent-crew/v1', 'chatbot-regen/v1' ) as $namespace ) {
     32                        register_rest_route(
     33                                $namespace,
     34                                '/updraft/archive',
     35                                array(
     36                                        'methods'             => 'GET',
     37                                        'callback'            => array( __CLASS__, 'stream_archive' ),
     38                                        'permission_callback' => '__return_true',
     39                                        'args'                => array(
     40                                                'token' => array(
     41                                                        'required' => true,
     42                                                        'type'     => 'string',
     43                                                ),
     44                                        ),
     45                                )
     46                        );
     47                }
    2248        }
    2349
    … …  
    931957
    932958        /**
     959         * List local Updraft archive files for a backup set + short-lived signed download URLs.
     960         * Used by MyAgentCrew Provision migrate pull (External site → Contabo).
     961         *
     962         * @param array<string,mixed> $manifest Manifest.
     963         * @param array<string,mixed> $tool     Tool definition.
     964         * @param array<string,mixed> $args     Request args.
     965         * @return array<string,mixed>|WP_Error
     966         */
     967        public static function list_archives( array $manifest, array $tool, array $args ) {
     968                if ( ! self::is_available() ) {
     969                        return new WP_Error(
     970                                'cbregen_updraft_missing',
     971                                'UpdraftPlus is not installed or active.',
     972                                array( 'status' => 404 )
     973                        );
     974                }
     975                if ( ! self::bootstrap_updraft() ) {
     976                        return new WP_Error( 'cbregen_updraft_bootstrap', 'Could not bootstrap UpdraftPlus.' );
     977                }
     978
     979                $history = self::get_backup_history();
     980                $timestamp = 0;
     981                if ( isset( $args['timestamp'] ) ) {
     982                        $timestamp = (int) $args['timestamp'];
     983                } elseif ( isset( $args['backupKey'] ) && is_numeric( $args['backupKey'] ) ) {
     984                        $timestamp = (int) $args['backupKey'];
     985                } elseif ( isset( $args['backup_key'] ) && is_numeric( $args['backup_key'] ) ) {
     986                        $timestamp = (int) $args['backup_key'];
     987                } elseif ( isset( $args['key'] ) && is_numeric( $args['key'] ) ) {
     988                        $timestamp = (int) $args['key'];
     989                }
     990
     991                $row = null;
     992                $key = null;
     993                if ( $timestamp > 0 && ! empty( $history[ $timestamp ] ) && is_array( $history[ $timestamp ] ) ) {
     994                        $key = $timestamp;
     995                        $row = $history[ $timestamp ];
     996                } else {
     997                        // Newest local_ready backup.
     998                        $summaries = array();
     999                        foreach ( $history as $hkey => $hrow ) {
     1000                                $sum = self::summarize_backup_row( $hkey, $hrow );
     1001                                if ( $sum && ! empty( $sum['local_ready'] ) ) {
     1002                                        $summaries[] = array( 'key' => $hkey, 'row' => $hrow, 'ts' => (int) $sum['timestamp'] );
     1003                                }
     1004                        }
     1005                        usort(
     1006                                $summaries,
     1007                                static function ( $a, $b ) {
     1008                                        return $b['ts'] - $a['ts'];
     1009                                }
     1010                        );
     1011                        if ( empty( $summaries ) ) {
     1012                                return new WP_Error(
     1013                                        'cbregen_updraft_no_local_backup',
     1014                                        'No local_ready Updraft backup found. Run Backup Now with nocloud first.',
     1015                                        array( 'status' => 404, 'admin_url' => self::updraft_admin_url() )
     1016                                );
     1017                        }
     1018                        $key = $summaries[0]['key'];
     1019                        $row = $summaries[0]['row'];
     1020                        $timestamp = $summaries[0]['ts'];
     1021                }
     1022
     1023                $summary = self::summarize_backup_row( $key, $row );
     1024                if ( ! $summary || empty( $summary['local_ready'] ) ) {
     1025                        return new WP_Error(
     1026                                'cbregen_updraft_not_local',
     1027                                'Backup ' . $timestamp . ' is not fully present on local disk (local_ready=false).',
     1028                                array( 'status' => 409, 'admin_url' => self::updraft_admin_url() )
     1029                        );
     1030                }
     1031
     1032                $dir = self::backups_dir();
     1033                $ttl = isset( $args['ttl'] ) ? max( 300, min( 86400, (int) $args['ttl'] ) ) : self::ARCHIVE_URL_TTL;
     1034                $files = array();
     1035                $skipped_crypt = array();
     1036
     1037                foreach ( $summary['components'] as $entity ) {
     1038                        $st = self::entity_local_status( $row, $entity );
     1039                        foreach ( $st['files'] as $filename ) {
     1040                                $filename = basename( (string) $filename );
     1041                                if ( $filename === '' ) {
     1042                                        continue;
     1043                                }
     1044                                if ( preg_match( '/\.crypt(\.|$)/i', $filename ) ) {
     1045                                        $skipped_crypt[] = $filename;
     1046                                        continue;
     1047                                }
     1048                                $path = trailingslashit( $dir ) . $filename;
     1049                                if ( ! is_file( $path ) || filesize( $path ) <= 0 ) {
     1050                                        continue;
     1051                                }
     1052                                $token = self::sign_archive_token( $timestamp, $filename, $ttl );
     1053                                $url   = rest_url( 'my-agent-crew/v1/updraft/archive?token=' . rawurlencode( $token ) );
     1054                                $files[] = array(
     1055                                        'name'      => $filename,
     1056                                        'size'      => (int) filesize( $path ),
     1057                                        'url'       => $url,
     1058                                        'headers'   => array(),
     1059                                        'entity'    => $entity,
     1060                                        'backupKey' => (string) $timestamp,
     1061                                );
     1062                        }
     1063                }
     1064
     1065                if ( empty( $files ) ) {
     1066                        $msg = 'No downloadable archive files found for this backup.';
     1067                        if ( ! empty( $skipped_crypt ) ) {
     1068                                $msg .= ' Encrypted (.crypt) archives are not supported for migrate pull.';
     1069                        }
     1070                        return new WP_Error(
     1071                                'cbregen_updraft_no_files',
     1072                                $msg,
     1073                                array(
     1074                                        'status'         => 404,
     1075                                        'skipped_crypt'  => $skipped_crypt,
     1076                                        'admin_url'      => self::updraft_admin_url(),
     1077                                )
     1078                        );
     1079                }
     1080
     1081                return array(
     1082                        'ok'            => true,
     1083                        'tool'          => isset( $tool['name'] ) ? $tool['name'] : 'adapter_updraftplus_list_archives',
     1084                        'backupKey'     => (string) $timestamp,
     1085                        'timestamp'     => (int) $timestamp,
     1086                        'key'           => $key,
     1087                        'local_ready'   => true,
     1088                        'components'    => $summary['components'],
     1089                        'files'         => $files,
     1090                        'fileCount'     => count( $files ),
     1091                        'bytesTotal'    => array_sum( array_map( static function ( $f ) { return (int) $f['size']; }, $files ) ),
     1092                        'expiresIn'     => $ttl,
     1093                        'skipped_crypt' => $skipped_crypt,
     1094                        'admin_url'     => self::updraft_admin_url(),
     1095                        'message'       => sprintf(
     1096                                'Listed %d Updraft archive file(s) for backup %s with signed download URLs (TTL %ds).',
     1097                                count( $files ),
     1098                                $timestamp,
     1099                                $ttl
     1100                        ),
     1101                );
     1102        }
     1103
     1104        /**
     1105         * Stream one Updraft archive file after verifying HMAC token.
     1106         * Supports HTTP Range for Provision resume downloads.
     1107         *
     1108         * @param WP_REST_Request $request Request.
     1109         * @return WP_REST_Response|WP_Error
     1110         */
     1111        public static function stream_archive( $request ) {
     1112                $token = (string) $request->get_param( 'token' );
     1113                $payload = self::verify_archive_token( $token );
     1114                if ( is_wp_error( $payload ) ) {
     1115                        return $payload;
     1116                }
     1117
     1118                $filename = basename( (string) $payload['file'] );
     1119                if ( $filename === '' || preg_match( '/\.crypt(\.|$)/i', $filename ) ) {
     1120                        return new WP_Error( 'cbregen_updraft_forbidden', 'File not allowed.', array( 'status' => 403 ) );
     1121                }
     1122
     1123                $path = trailingslashit( self::backups_dir() ) . $filename;
     1124                if ( ! is_file( $path ) || ! is_readable( $path ) ) {
     1125                        return new WP_Error( 'cbregen_updraft_missing_file', 'Archive file not found on disk.', array( 'status' => 404 ) );
     1126                }
     1127
     1128                $size = (int) filesize( $path );
     1129                $start = 0;
     1130                $end   = $size - 1;
     1131                $status = 200;
     1132
     1133                $range = isset( $_SERVER['HTTP_RANGE'] ) ? (string) $_SERVER['HTTP_RANGE'] : '';
     1134                if ( $range && preg_match( '/bytes=(\d*)-(\d*)/', $range, $m ) ) {
     1135                        if ( $m[1] !== '' ) {
     1136                                $start = (int) $m[1];
     1137                        }
     1138                        if ( $m[2] !== '' ) {
     1139                                $end = (int) $m[2];
     1140                        }
     1141                        if ( $start > $end || $start >= $size ) {
     1142                                status_header( 416 );
     1143                                header( 'Content-Range: bytes */' . $size );
     1144                                exit;
     1145                        }
     1146                        $status = 206;
     1147                }
     1148
     1149                $length = $end - $start + 1;
     1150
     1151                // Bypass WP JSON wrapper — stream raw bytes.
     1152                status_header( $status );
     1153                header( 'Content-Type: application/octet-stream' );
     1154                header( 'Content-Disposition: attachment; filename="' . rawurlencode( $filename ) . '"' );
     1155                header( 'Accept-Ranges: bytes' );
     1156                header( 'Content-Length: ' . $length );
     1157                if ( 206 === $status ) {
     1158                        header( 'Content-Range: bytes ' . $start . '-' . $end . '/' . $size );
     1159                }
     1160                header( 'Cache-Control: no-store' );
     1161                header( 'X-AgentCrew-Updraft-File: ' . $filename );
     1162
     1163                // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
     1164                $fh = fopen( $path, 'rb' );
     1165                if ( ! $fh ) {
     1166                        status_header( 500 );
     1167                        echo 'Could not open archive.';
     1168                        exit;
     1169                }
     1170                if ( $start > 0 ) {
     1171                        fseek( $fh, $start );
     1172                }
     1173                $remaining = $length;
     1174                while ( $remaining > 0 && ! feof( $fh ) ) {
     1175                        $chunk = fread( $fh, min( 1024 * 1024, $remaining ) );
     1176                        if ( false === $chunk || $chunk === '' ) {
     1177                                break;
     1178                        }
     1179                        echo $chunk;
     1180                        $remaining -= strlen( $chunk );
     1181                        if ( function_exists( 'fastcgi_finish_request' ) ) {
     1182                                // Keep streaming; do not finish early.
     1183                        }
     1184                        if ( connection_aborted() ) {
     1185                                break;
     1186                        }
     1187                }
     1188                fclose( $fh );
     1189                exit;
     1190        }
     1191
     1192        /**
     1193         * @return string
     1194         */
     1195        private static function backups_dir() {
     1196                global $updraftplus;
     1197                $dir = '';
     1198                if ( is_a( $updraftplus, 'UpdraftPlus' ) && method_exists( $updraftplus, 'backups_dir_location' ) ) {
     1199                        $dir = (string) $updraftplus->backups_dir_location();
     1200                }
     1201                if ( $dir === '' ) {
     1202                        $upload = wp_upload_dir();
     1203                        $dir    = trailingslashit( $upload['basedir'] ) . 'updraft';
     1204                }
     1205                return $dir;
     1206        }
     1207
     1208        /**
     1209         * @return string
     1210         */
     1211        private static function archive_signing_secret() {
     1212                $api = '';
     1213                if ( class_exists( 'CBRegen_Auth' ) ) {
     1214                        $api = (string) get_option( CBRegen_Auth::OPTION_KEY, '' );
     1215                }
     1216                return hash( 'sha256', 'cbregen-updraft-archive|' . wp_salt( 'auth' ) . '|' . $api . '|' . home_url( '/' ), true );
     1217        }
     1218
     1219        /**
     1220         * @param int    $timestamp Backup key.
     1221         * @param string $filename  Basename.
     1222         * @param int    $ttl       Seconds.
     1223         * @return string
     1224         */
     1225        private static function sign_archive_token( $timestamp, $filename, $ttl ) {
     1226                $payload = array(
     1227                        'ts'   => (int) $timestamp,
     1228                        'file' => basename( (string) $filename ),
     1229                        'exp'  => time() + (int) $ttl,
     1230                        'n'    => wp_generate_password( 8, false, false ),
     1231                );
     1232                $json = wp_json_encode( $payload );
     1233                $b64  = rtrim( strtr( base64_encode( $json ), '+/', '-_' ), '=' );
     1234                $sig  = hash_hmac( 'sha256', $b64, self::archive_signing_secret() );
     1235                return $b64 . '.' . $sig;
     1236        }
     1237
     1238        /**
     1239         * @param string $token Token.
     1240         * @return array<string,mixed>|WP_Error
     1241         */
     1242        private static function verify_archive_token( $token ) {
     1243                $token = (string) $token;
     1244                $parts = explode( '.', $token );
     1245                if ( count( $parts ) !== 2 ) {
     1246                        return new WP_Error( 'cbregen_updraft_bad_token', 'Invalid download token.', array( 'status' => 403 ) );
     1247                }
     1248                list( $b64, $sig ) = $parts;
     1249                $expect = hash_hmac( 'sha256', $b64, self::archive_signing_secret() );
     1250                if ( ! hash_equals( $expect, $sig ) ) {
     1251                        return new WP_Error( 'cbregen_updraft_bad_token', 'Invalid download token signature.', array( 'status' => 403 ) );
     1252                }
     1253                $pad  = strlen( $b64 ) % 4;
     1254                $raw  = base64_decode( strtr( $b64, '-_', '+/' ) . ( $pad ? str_repeat( '=', 4 - $pad ) : '' ) );
     1255                $data = json_decode( (string) $raw, true );
     1256                if ( ! is_array( $data ) || empty( $data['file'] ) || empty( $data['exp'] ) ) {
     1257                        return new WP_Error( 'cbregen_updraft_bad_token', 'Corrupt download token.', array( 'status' => 403 ) );
     1258                }
     1259                if ( (int) $data['exp'] < time() ) {
     1260                        return new WP_Error( 'cbregen_updraft_token_expired', 'Download token expired. Call list_archives again.', array( 'status' => 403 ) );
     1261                }
     1262                return $data;
     1263        }
     1264
     1265        /**
    9331266         * @param string                   $status Status.
    9341267         * @param array<string,mixed>|null $latest Latest backup.
  • my-agent-crew-connector/trunk/my-agent-crew-connector.php

    r3668824 r3699356  
    44 * Plugin URI:        https://myagentcrew.com/wordpress-connector/
    55 * Description:       Connect WordPress to a My Agent Crew agent. Phase 2 MCP gateway: curated capabilities plus dynamic discovery of enabled third-party /wp-json/ namespaces.
    6  * Version:           2.1.3
     6 * Version:           2.1.4
    77 * Requires at least: 5.6
    88 * Requires PHP:      7.4
    … …  
    1919}
    2020
    21 define( 'CBREGEN_VERSION', '2.1.3' );
     21define( 'CBREGEN_VERSION', '2.1.4' );
    2222define( 'CBREGEN_DIR', plugin_dir_path( __FILE__ ) );
    2323
  • my-agent-crew-connector/trunk/readme.txt

    r3668824 r3699356  
    55Tested up to: 7.0
    66Requires PHP: 7.4
    7 Stable tag: 2.1.3
     7Stable tag: 2.1.4
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    7070
    7171== Changelog ==
     72
     73= 2.1.4 =
     74* Updraft migrate stream: `adapter_updraftplus_list_archives` returns local archive files with short-lived signed download URLs (`GET /my-agent-crew/v1/updraft/archive?token=…`, HTTP Range supported) for MyAgentCrew Hosting Provision pull.
     75* Skips encrypted `.crypt` archives (not supported by Provision migrate).
    7276
    7377= 2.1.3 =
Note: See TracChangeset for help on using the changeset viewer.