PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
mainwp / includes / abilities / class-mainwp-abilities-batch.php

class-mainwp-abilities-batch.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2.2, at includes/abilities/class-mainwp-abilities-batch.php

340 lines 13.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Batch Abilities
4 *
5 * @package MainWP\Dashboard
6 */
7
8 namespace MainWP\Dashboard;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 /**
15 * Class MainWP_Abilities_Batch
16 *
17 * Registers and implements batch operation monitoring abilities for the MainWP Dashboard.
18 *
19 * This class provides 1 ability:
20 * - mainwp/get-batch-job-status-v1: Retrieve status of a queued batch operation
21 *
22 * Batch operations (sync, update, reconnect, disconnect, check, suspend) that affect
23 * >200 sites are automatically queued for background processing. This ability allows
24 * clients to poll for job status, progress, and results.
25 */
26 class MainWP_Abilities_Batch { //phpcs:ignore -- NOSONAR - multi methods.
27
28 /**
29 * Register all batch abilities.
30 *
31 * @return void
32 */
33 public static function register(): void {
34 if ( ! function_exists( 'wp_register_ability' ) ) {
35 return;
36 }
37
38 self::register_get_batch_job_status();
39 }
40
41 /**
42 * Register mainwp/get-batch-job-status-v1 ability.
43 *
44 * @return void
45 */
46 private static function register_get_batch_job_status(): void {
47 wp_register_ability(
48 'mainwp/get-batch-job-status-v1',
49 array(
50 'label' => __( 'Get Batch Job Status', 'mainwp' ),
51 'description' => __( 'Retrieve status of a queued batch operation (sync, update, reconnect, disconnect, check, suspend). Returns progress, results, and errors. Possible errors: mainwp_job_not_found.', 'mainwp' ),
52 'category' => 'mainwp-batch',
53 'input_schema' => self::get_batch_job_status_input_schema(),
54 'output_schema' => self::get_batch_job_status_output_schema(),
55 'execute_callback' => array( self::class, 'execute_get_batch_job_status' ),
56 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_rest_api_permission' ),
57 'meta' => array(
58 'show_in_rest' => true,
59 'annotations' => array(
60 'instructions' => '',
61 'readonly' => true,
62 'destructive' => false,
63 'idempotent' => true,
64 ),
65 ),
66 )
67 );
68 }
69
70 /**
71 * Get input schema for get-batch-job-status-v1.
72 *
73 * @return array
74 */
75 public static function get_batch_job_status_input_schema(): array {
76 return array(
77 'type' => 'object',
78 'properties' => array(
79 'job_id' => array(
80 'type' => 'string',
81 'pattern' => '^(sync|update|reconnect|disconnect|check|suspend)_',
82 'description' => __( 'Batch job ID returned from queued operation (e.g., sync_abc123, update_xyz789)', 'mainwp' ),
83 ),
84 ),
85 'required' => array( 'job_id' ),
86 'additionalProperties' => false,
87 );
88 }
89
90 /**
91 * Get output schema for get-batch-job-status-v1.
92 *
93 * @return array
94 */
95 public static function get_batch_job_status_output_schema(): array { // phpcs:ignore -- NOSONAR - complex array.
96 return array(
97 'type' => 'object',
98 'properties' => array(
99 'job_id' => array(
100 'type' => 'string',
101 ),
102 'type' => array(
103 'type' => 'string',
104 'enum' => array( 'sync', 'update', 'reconnect', 'disconnect', 'check', 'suspend' ),
105 ),
106 'status' => array(
107 'type' => 'string',
108 'enum' => array( 'queued', 'processing', 'completed', 'failed' ),
109 ),
110 'progress' => array(
111 'type' => 'integer',
112 'minimum' => 0,
113 'maximum' => 100,
114 ),
115 'processed' => array(
116 'type' => 'integer',
117 'description' => __( 'Number of sites processed so far', 'mainwp' ),
118 ),
119 'total' => array(
120 'type' => 'integer',
121 'description' => __( 'Total number of sites to process', 'mainwp' ),
122 ),
123 'succeeded' => array(
124 'type' => 'integer',
125 'description' => __( 'Number of successful operations', 'mainwp' ),
126 ),
127 'failed' => array(
128 'type' => 'integer',
129 'description' => __( 'Number of failed operations', 'mainwp' ),
130 ),
131 'started_at' => array(
132 'oneOf' => array(
133 array(
134 'type' => 'string',
135 'format' => 'date-time',
136 ),
137 array(
138 'type' => 'null',
139 ),
140 ),
141 ),
142 'completed_at' => array(
143 'oneOf' => array(
144 array(
145 'type' => 'string',
146 'format' => 'date-time',
147 ),
148 array(
149 'type' => 'null',
150 ),
151 ),
152 ),
153 'errors' => array(
154 'type' => 'array',
155 'items' => array(
156 'type' => 'object',
157 'properties' => array(
158 'site_id' => array(
159 'type' => 'integer',
160 ),
161 'code' => array(
162 'type' => 'string',
163 ),
164 'message' => array(
165 'type' => 'string',
166 ),
167 ),
168 'required' => array( 'site_id', 'code', 'message' ),
169 ),
170 ),
171 'job_timed_out' => array(
172 'type' => 'boolean',
173 'description' => __( 'Whether the job exceeded the maximum processing time window (4 hours).', 'mainwp' ),
174 ),
175 'job_error_code' => array(
176 'type' => 'string',
177 'description' => __( 'Top-level error code when job failed due to timeout or other job-level issue. Present when job_timed_out is true.', 'mainwp' ),
178 ),
179 'job_error_message' => array(
180 'type' => 'string',
181 'description' => __( 'Human-readable description of the job-level error. Present when job_timed_out is true.', 'mainwp' ),
182 ),
183 ),
184 'required' => array( 'job_id', 'type', 'status', 'progress', 'processed', 'total', 'succeeded', 'failed' ),
185 );
186 }
187
188 /**
189 * Extract job type from job_id prefix.
190 *
191 * @param string $job_id The batch job ID.
192 * @return string|null Job type string or null if prefix is not recognized.
193 */
194 private static function get_job_type_from_job_id( string $job_id ): ?string {
195 if ( strpos( $job_id, 'sync_' ) === 0 ) {
196 return 'sync';
197 } elseif ( strpos( $job_id, 'update_' ) === 0 ) {
198 return 'update';
199 } elseif ( strpos( $job_id, 'reconnect_' ) === 0 ) {
200 return 'reconnect';
201 } elseif ( strpos( $job_id, 'disconnect_' ) === 0 ) {
202 return 'disconnect';
203 } elseif ( strpos( $job_id, 'check_' ) === 0 ) {
204 return 'check';
205 } elseif ( strpos( $job_id, 'suspend_' ) === 0 ) {
206 return 'suspend';
207 }
208
209 return null;
210 }
211
212 /**
213 * Execute get-batch-job-status-v1 ability.
214 *
215 * @param array $input Input parameters with job_id.
216 * @return array|\WP_Error Job status array or WP_Error on failure.
217 */
218 public static function execute_get_batch_job_status( array $input ) { // phpcs:ignore -- NOSONAR - complex function.
219 $job_id = isset( $input['job_id'] ) ? $input['job_id'] : '';
220
221 if ( empty( $job_id ) || ! is_string( $job_id ) ) {
222 return new \WP_Error(
223 'ability_invalid_input',
224 __( 'Missing or invalid job_id parameter.', 'mainwp' ),
225 array( 'status' => 400 )
226 );
227 }
228
229 $job_type_from_prefix = self::get_job_type_from_job_id( $job_id );
230
231 if ( null === $job_type_from_prefix ) {
232 return new \WP_Error(
233 'mainwp_invalid_job_id',
234 __( 'Invalid job ID format. Job ID must start with a recognized operation type prefix (sync_, update_, reconnect_, disconnect_, check_, suspend_).', 'mainwp' ),
235 array( 'status' => 400 )
236 );
237 }
238
239 $job_data = null;
240
241 if ( 'sync' === $job_type_from_prefix ) {
242 $job_data = MainWP_Abilities_Util::get_batch_sync_status( $job_id );
243 } elseif ( 'update' === $job_type_from_prefix ) {
244 $job_data = MainWP_Abilities_Util::get_batch_update_status( $job_id );
245 } else {
246 $job_data = MainWP_Abilities_Util::get_batch_operation_status( $job_id );
247 }
248
249 if ( null === $job_data ) {
250 return new \WP_Error(
251 'mainwp_job_not_found',
252 __( 'Batch job not found. Job may have expired (jobs are kept for 24 hours).', 'mainwp' ),
253 array( 'status' => 404 )
254 );
255 }
256
257 // Normalize job data fields with defensive fallbacks.
258 // This section tolerates partially populated transients to avoid misleading status
259 // for malformed or incomplete job data (e.g., from interrupted background processes).
260 $job_type = isset( $job_data['job_type'] ) && ! empty( $job_data['job_type'] )
261 ? $job_data['job_type']
262 : $job_type_from_prefix;
263
264 // Validate status against known values; default to 'queued' if invalid.
265 $valid_statuses = array( 'queued', 'processing', 'completed', 'failed' );
266 $status = isset( $job_data['status'] ) && in_array( $job_data['status'], $valid_statuses, true )
267 ? $job_data['status']
268 : 'queued';
269
270 // Clamp progress to valid 0-100 range.
271 $progress = isset( $job_data['progress'] ) ? max( 0, min( 100, (int) $job_data['progress'] ) ) : 0;
272 $processed = isset( $job_data['processed'] ) ? max( 0, (int) $job_data['processed'] ) : 0;
273 $total = isset( $job_data['total'] ) ? max( 0, (int) $job_data['total'] ) : 0;
274
275 // Count successes from whichever key is populated (sync/update/operation).
276 // Only count if the key exists AND contains an array; fall back to zero otherwise.
277 $succeeded = 0;
278 if ( isset( $job_data['synced'] ) && is_array( $job_data['synced'] ) ) {
279 $succeeded = count( $job_data['synced'] );
280 } elseif ( isset( $job_data['updated'] ) && is_array( $job_data['updated'] ) ) {
281 $succeeded = count( $job_data['updated'] );
282 } elseif ( isset( $job_data['successful'] ) && is_array( $job_data['successful'] ) ) {
283 $succeeded = count( $job_data['successful'] );
284 }
285
286 // Treat non-array 'errors' as empty list to avoid type errors.
287 $errors_array = isset( $job_data['errors'] ) && is_array( $job_data['errors'] ) ? $job_data['errors'] : array();
288
289 $started_at = null;
290 $completed_at = null;
291
292 if ( isset( $job_data['started'] ) && is_numeric( $job_data['started'] ) && $job_data['started'] > 0 ) {
293 $started_at = gmdate( 'Y-m-d\TH:i:s\Z', (int) $job_data['started'] );
294 }
295
296 if ( isset( $job_data['completed'] ) && is_numeric( $job_data['completed'] ) && $job_data['completed'] > 0 ) {
297 $completed_at = gmdate( 'Y-m-d\TH:i:s\Z', (int) $job_data['completed'] );
298 }
299
300 $normalized_errors = array();
301 foreach ( $errors_array as $error ) {
302 if ( is_array( $error ) ) {
303 $normalized_errors[] = array(
304 'site_id' => isset( $error['site_id'] ) ? (int) $error['site_id'] : 0,
305 'code' => isset( $error['code'] ) ? (string) $error['code'] : '',
306 'message' => isset( $error['message'] ) ? (string) $error['message'] : '',
307 );
308 }
309 }
310
311 $failed = count( $normalized_errors );
312
313 $response = array(
314 'job_id' => $job_id,
315 'type' => $job_type,
316 'status' => $status,
317 'progress' => $progress,
318 'processed' => $processed,
319 'total' => $total,
320 'succeeded' => $succeeded,
321 'failed' => $failed,
322 'started_at' => $started_at,
323 'completed_at' => $completed_at,
324 'errors' => $normalized_errors,
325 );
326
327 // Expose timeout state and add top-level error fields when job timed out.
328 // This provides a clear indicator without requiring clients to scan the errors array.
329 $job_timed_out = ! empty( $job_data['job_timed_out'] );
330 $response['job_timed_out'] = $job_timed_out;
331
332 if ( $job_timed_out ) {
333 $response['job_error_code'] = 'timeout';
334 $response['job_error_message'] = __( 'Job timed out after 4 hours. Partial results may be available.', 'mainwp' );
335 }
336
337 return $response;
338 }
339 }
340