Changeset 3699356
- Timestamp:
- 09/16/2026 11:50:17 PM (5 weeks ago)
- Location:
- my-agent-crew-connector/trunk
- Files:
-
- 5 edited
-
includes/capability-manifests/updraftplus.php (modified) (3 diffs)
-
includes/class-cbregen-capability-runtime.php (modified) (1 diff)
-
includes/class-cbregen-updraftplus-adapter.php (modified) (3 diffs)
-
my-agent-crew-connector.php (modified) (2 diffs)
-
readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
my-agent-crew-connector/trunk/includes/capability-manifests/updraftplus.php
r3668824 r3699356 3 3 'schema' => 'cbregen-capability-manifest@1', 4 4 'manifest_id' => 'updraftplus_core', 5 'version' => '1. 1.0',5 'version' => '1.2.0', 6 6 'slug' => 'updraftplus', 7 7 'source' => array( … … 11 11 'key' => 'adapter:updraftplus', 12 12 '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.', 14 14 'legacy_enable_keys'=> array( 'cli:updraftplus' ), 15 15 ), … … 75 75 ), 76 76 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( 77 103 'id' => 'restore_backup', 78 104 'name' => 'adapter_updraftplus_restore_backup', -
my-agent-crew-connector/trunk/includes/class-cbregen-capability-runtime.php
r3668824 r3699356 219 219 'updraftplus_backup_status' => array( 'CBRegen_UpdraftPlus_Adapter', 'backup_status' ), 220 220 'updraftplus_list_backups' => array( 'CBRegen_UpdraftPlus_Adapter', 'list_backups' ), 221 'updraftplus_list_archives' => array( 'CBRegen_UpdraftPlus_Adapter', 'list_archives' ), 221 222 'updraftplus_restore_backup' => array( 'CBRegen_UpdraftPlus_Adapter', 'restore_backup' ), 222 223 'updraftplus_restore_status' => array( 'CBRegen_UpdraftPlus_Adapter', 'restore_status' ), -
my-agent-crew-connector/trunk/includes/class-cbregen-updraftplus-adapter.php
r3668824 r3699356 14 14 const RESTORE_JOB_OPTION = 'cbregen_updraft_adapter_restore_job'; 15 15 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; 16 18 17 19 /** … … 20 22 public static function boot() { 21 23 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 } 22 48 } 23 49 … … 931 957 932 958 /** 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 /** 933 1266 * @param string $status Status. 934 1267 * @param array<string,mixed>|null $latest Latest backup. -
my-agent-crew-connector/trunk/my-agent-crew-connector.php
r3668824 r3699356 4 4 * Plugin URI: https://myagentcrew.com/wordpress-connector/ 5 5 * 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. 36 * Version: 2.1.4 7 7 * Requires at least: 5.6 8 8 * Requires PHP: 7.4 … … 19 19 } 20 20 21 define( 'CBREGEN_VERSION', '2.1. 3' );21 define( 'CBREGEN_VERSION', '2.1.4' ); 22 22 define( 'CBREGEN_DIR', plugin_dir_path( __FILE__ ) ); 23 23 -
my-agent-crew-connector/trunk/readme.txt
r3668824 r3699356 5 5 Tested up to: 7.0 6 6 Requires PHP: 7.4 7 Stable tag: 2.1. 37 Stable tag: 2.1.4 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 70 70 71 71 == 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). 72 76 73 77 = 2.1.3 =
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)