PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.3
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.3
6.3 6.2.3 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 All 157 releases
← All changes | includes/abilities/class-mainwp-abilities-cron.php +913 -635 6.1.7 → 6.3 View file →
@@ -95,8 +95,162 @@
95 95 }
96 96 }
97 97
98 98 /**
99 + * Restore the job owner and confirm the account can still manage sites.
100 + *
101 + * Cron has no authenticated owner by default. Site scope and rights must
102 + * be checked as the user who queued the job. Deleted accounts and accounts
103 + * that lost manage_options must not continue driving remote calls.
104 + *
105 + * @param array $job Job data (reads user_id).
106 + *
107 + * @return bool True when the owner is valid (or the job predates owner
108 + * stamping), false when a stamped owner no longer exists or cannot manage sites.
109 + */
110 + private function restore_job_owner( array $job ): bool {
111 + $owner_id = isset( $job['user_id'] ) ? (int) $job['user_id'] : 0;
112 +
113 + if ( $owner_id <= 0 ) {
114 + // Legacy job queued before owner stamping: nothing to revalidate
115 + // against, preserve prior behaviour.
116 + return true;
117 + }
118 +
119 + if ( ! get_userdata( $owner_id ) ) {
120 + return false;
121 + }
122 +
123 + if ( ! user_can( $owner_id, 'manage_options' ) ) {
124 + return false;
125 + }
126 +
127 + wp_set_current_user( $owner_id );
128 + return true;
129 + }
130 +
131 + /**
132 + * Preserve enqueue-time scope for legacy jobs without an owner to recheck.
133 + *
134 + * @param array $job Job data (reads user_id).
135 + * @return string|int[] 'all' for legacy jobs or the owner's allowed site IDs.
136 + */
137 + private function get_job_allowed_site_ids( array $job ) {
138 + $owner_id = isset( $job['user_id'] ) ? (int) $job['user_id'] : 0;
139 +
140 + return $owner_id <= 0 ? 'all' : MainWP_Abilities_Util::get_allowed_site_ids();
141 + }
142 +
143 + /**
144 + * Whether the restored owner may still act on a site.
145 + *
146 + * @param int $site_id Site ID.
147 + * @param string|int[] $allowed_sites 'all' or the owner's allowed site IDs.
148 + *
149 + * @return bool
150 + */
151 + private function owner_can_access_site( $site_id, $allowed_sites ): bool {
152 + return 'all' === $allowed_sites || in_array( (int) $site_id, $allowed_sites, true );
153 + }
154 +
155 + /**
156 + * Fail a whole job because its owner no longer exists or cannot manage sites.
157 + *
158 + * Remaining sites must not run after the owner loses enqueue permission.
159 + *
160 + * @param array $job Job data.
161 + * @param string $transient_key Transient key prefix.
162 + * @param string $job_id Job ID.
163 + *
164 + * @return void
165 + */
166 + private function fail_job_owner_revoked( array $job, string $transient_key, string $job_id ): void {
167 + $job['errors'] = isset( $job['errors'] ) && is_array( $job['errors'] ) ? $job['errors'] : array();
168 + $job['status'] = 'failed';
169 + $job['completed'] = time();
170 + $job['errors'][] = array(
171 + 'site_id' => 0,
172 + 'code' => 'access_revoked',
173 + 'message' => __( 'The account that queued this job no longer exists or can no longer manage sites; remaining sites were not processed.', 'mainwp' ),
174 + );
175 + set_transient( $transient_key . $job_id, $job, DAY_IN_SECONDS );
176 + $this->log_debug( 'Job owner no longer exists or cannot manage sites, failing job: ' . $job_id );
177 + }
178 +
179 + /**
180 + * Whether the restored job owner still holds every operation right the
181 + * job was stamped with at enqueue.
182 + *
183 + * Evaluated per chunk run, after restore_job_owner() has set the owner as
184 + * the current user. The global wrapper bypasses under DOING_CRON and
185 + * would make this a no-op, so the direct filter is required. Jobs queued
186 + * before rights stamping carry no key and are treated as requiring
187 + * nothing, so in-flight jobs survive a deploy.
188 + *
189 + * @param array $job Job data.
190 + *
191 + * @return bool True when every stamped right is still held.
192 + */
193 + private function owner_holds_required_rights( array $job ): bool {
194 + // No stamped owner (legacy job, or a WP-CLI enqueue without --user that
195 + // passed the machine-context bypass): there is no user whose rights
196 + // could have changed, and evaluating the filter for user 0 under
197 + // wp-cron would fail every such job once Team Control is active.
198 + $owner_id = isset( $job['user_id'] ) ? (int) $job['user_id'] : 0;
199 + if ( $owner_id <= 0 ) {
200 + return true;
201 + }
202 +
203 + $rights = isset( $job['required_rights'] ) && is_array( $job['required_rights'] ) ? $job['required_rights'] : array();
204 +
205 + foreach ( $rights as $right ) {
206 + if ( ! MainWP_Abilities_Util::current_user_has_operation_right( (string) $right ) ) {
207 + return false;
208 + }
209 + }
210 +
211 + return true;
212 + }
213 +
214 + /**
215 + * Fail a job because the owner lost a required operation right mid-job.
216 + *
217 + * Every remaining (unprocessed) target is marked access_revoked without
218 + * any remote call, then the job terminates the same way the owner-deleted
219 + * terminal does: status failed, completed stamped, transient saved.
220 + *
221 + * @param array $job Job data.
222 + * @param string $transient_key Transient key prefix.
223 + * @param string $job_id Job ID.
224 + * @param string $success_key Job key holding successful site IDs ('synced'|'updated'|'successful').
225 + *
226 + * @return void
227 + */
228 + private function fail_job_rights_revoked( array $job, string $transient_key, string $job_id, string $success_key ): void {
229 + $successes = isset( $job[ $success_key ] ) && is_array( $job[ $success_key ] ) ? $job[ $success_key ] : array();
230 + $job['errors'] = isset( $job['errors'] ) && is_array( $job['errors'] ) ? $job['errors'] : array();
231 + $sites = isset( $job['sites'] ) && is_array( $job['sites'] ) ? $job['sites'] : array();
232 +
233 + $unchanged = isset( $job['unchanged'] ) && is_array( $job['unchanged'] ) ? $job['unchanged'] : array();
234 + $processed_ids = array_merge( $successes, $unchanged, array_column( $job['errors'], 'site_id' ) );
235 + $remaining = array_values( array_diff( $sites, $processed_ids ) );
236 +
237 + foreach ( $remaining as $site_id ) {
238 + $job['errors'][] = array(
239 + 'site_id' => (int) $site_id,
240 + 'code' => 'access_revoked',
241 + 'message' => __( 'A right this job requires was revoked before this site was processed.', 'mainwp' ),
242 + );
243 + }
244 +
245 + $job['processed'] = ( isset( $job['processed'] ) ? (int) $job['processed'] : 0 ) + count( $remaining );
246 + $job['status'] = 'failed';
247 + $job['completed'] = time();
248 + set_transient( $transient_key . $job_id, $job, DAY_IN_SECONDS );
249 + $this->log_debug( 'Job owner lost a required operation right, failing job: ' . $job_id );
250 + }
251 +
252 + /**
99 253 * Process a batch sync job.
100 254 *
101 255 * Called via WP-Cron when a sync job is scheduled.
102 256 * Processes sites in chunks, updates progress, and reschedules until complete.
@@ -137,118 +291,144 @@
137 291 $this->log_debug( 'Sync job timed out: ' . $job_id );
138 292 return;
139 293 }
140 294
141 - // Status transition: queued -> processing.
142 - if ( 'queued' === $job['status'] ) {
143 - $job['status'] = 'processing';
144 - $job['started'] = time();
145 - }
295 + // Later callbacks must retain their own user context.
296 + $previous_user_id = get_current_user_id();
297 + try {
298 + if ( ! $this->restore_job_owner( $job ) ) {
299 + $this->fail_job_owner_revoked( $job, 'mainwp_sync_job_', $job_id );
300 + return;
301 + }
302 + if ( ! $this->owner_holds_required_rights( $job ) ) {
303 + $this->fail_job_rights_revoked( $job, 'mainwp_sync_job_', $job_id, 'synced' );
304 + return;
305 + }
306 + $allowed_sites = $this->get_job_allowed_site_ids( $job );
146 307
147 - // Chunk processing (default 20 sites per run).
148 - // Cast to int and clamp to minimum of 1 to prevent infinite loop from misconfigured filter.
149 - $chunk_size = (int) apply_filters( 'mainwp_abilities_cron_chunk_size', 20 );
150 - if ( $chunk_size < 1 ) {
151 - $chunk_size = 1;
152 - }
308 + // Status transition: queued -> processing.
309 + if ( 'queued' === $job['status'] ) {
310 + $job['status'] = 'processing';
311 + $job['started'] = time();
312 + }
153 313
154 - // Normalize result arrays defensively (transient data could be corrupted).
155 - $job['synced'] = isset( $job['synced'] ) && is_array( $job['synced'] ) ? $job['synced'] : array();
156 - $job['errors'] = isset( $job['errors'] ) && is_array( $job['errors'] ) ? $job['errors'] : array();
314 + // Chunk processing (default 20 sites per run).
315 + // Cast to int and clamp to minimum of 1 to prevent infinite loop from misconfigured filter.
316 + $chunk_size = (int) apply_filters( 'mainwp_abilities_cron_chunk_size', 20 );
317 + if ( $chunk_size < 1 ) {
318 + $chunk_size = 1;
319 + }
157 320
158 - // Calculate already processed site IDs.
159 - $processed_ids = array_merge(
160 - $job['synced'],
161 - array_column( $job['errors'], 'site_id' )
162 - );
321 + // Normalize result arrays defensively (transient data could be corrupted).
322 + $job['synced'] = isset( $job['synced'] ) && is_array( $job['synced'] ) ? $job['synced'] : array();
323 + $job['errors'] = isset( $job['errors'] ) && is_array( $job['errors'] ) ? $job['errors'] : array();
163 324
164 - // Get remaining sites to process.
165 - $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
166 - $chunk = array_slice( $remaining, 0, $chunk_size );
325 + // Calculate already processed site IDs.
326 + $processed_ids = array_merge(
327 + $job['synced'],
328 + array_column( $job['errors'], 'site_id' )
329 + );
167 330
168 - $this->log_debug(
169 - sprintf(
170 - 'Processing sync job %s: %d sites in chunk, %d remaining',
171 - $job_id,
172 - count( $chunk ),
173 - count( $remaining )
174 - )
175 - );
331 + // Get remaining sites to process.
332 + $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
333 + $chunk = array_slice( $remaining, 0, $chunk_size );
176 334
177 - // Process each site in the chunk.
178 - foreach ( $chunk as $site_id ) {
179 - try {
180 - $website = MainWP_DB::instance()->get_website_by_id( $site_id );
335 + $this->log_debug(
336 + sprintf(
337 + 'Processing sync job %s: %d sites in chunk, %d remaining',
338 + $job_id,
339 + count( $chunk ),
340 + count( $remaining )
341 + )
342 + );
181 343
182 - if ( empty( $website ) ) {
183 - $job['errors'][] = array(
184 - 'site_id' => $site_id,
185 - 'code' => 'site_not_found',
186 - 'message' => __( 'Site not found.', 'mainwp' ),
187 - );
188 - ++$job['processed'];
189 - continue;
190 - }
344 + // Process each site in the chunk.
345 + foreach ( $chunk as $site_id ) {
346 + try {
347 + if ( ! $this->owner_can_access_site( $site_id, $allowed_sites ) ) {
348 + $job['errors'][] = array(
349 + 'site_id' => $site_id,
350 + 'code' => 'access_revoked',
351 + 'message' => __( 'Access to this site was revoked before it was processed.', 'mainwp' ),
352 + );
353 + ++$job['processed'];
354 + continue;
355 + }
191 356
192 - // Call sync_site: false for force fetch, true for allow disconnect, false for clear session.
193 - $result = MainWP_Sync::sync_site( $website, false, true, false );
357 + $website = MainWP_DB::instance()->get_website_by_id( $site_id );
194 358
195 - if ( true === $result ) {
196 - $job['synced'][] = $site_id;
197 - } else {
359 + if ( empty( $website ) ) {
360 + $job['errors'][] = array(
361 + 'site_id' => $site_id,
362 + 'code' => 'site_not_found',
363 + 'message' => __( 'Site not found.', 'mainwp' ),
364 + );
365 + ++$job['processed'];
366 + continue;
367 + }
368 +
369 + // Call sync_site: false for force fetch, true for allow disconnect, false for clear session.
370 + $result = MainWP_Sync::sync_site( $website, false, true, false );
371 +
372 + if ( true === $result ) {
373 + $job['synced'][] = $site_id;
374 + } else {
375 + $job['errors'][] = array(
376 + 'site_id' => $site_id,
377 + 'code' => 'sync_failed',
378 + 'message' => __( 'Sync returned false.', 'mainwp' ),
379 + );
380 + }
381 + } catch ( \Exception $e ) {
198 382 $job['errors'][] = array(
199 383 'site_id' => $site_id,
200 - 'code' => 'sync_failed',
201 - 'message' => __( 'Sync returned false.', 'mainwp' ),
384 + 'code' => 'exception',
385 + 'message' => $e->getMessage(),
202 386 );
203 387 }
204 - } catch ( \Exception $e ) {
205 - $job['errors'][] = array(
206 - 'site_id' => $site_id,
207 - 'code' => 'exception',
208 - 'message' => $e->getMessage(),
209 - );
388 +
389 + ++$job['processed'];
390 + $job['progress'] = $job['total'] > 0
391 + ? round( ( $job['processed'] / $job['total'] ) * 100 )
392 + : 0;
210 393 }
211 394
212 - ++$job['processed'];
213 - $job['progress'] = $job['total'] > 0
214 - ? round( ( $job['processed'] / $job['total'] ) * 100 )
215 - : 0;
216 - }
395 + // Save progress.
396 + set_transient( 'mainwp_sync_job_' . $job_id, $job, DAY_IN_SECONDS );
217 397
218 - // Save progress.
219 - set_transient( 'mainwp_sync_job_' . $job_id, $job, DAY_IN_SECONDS );
398 + // Recalculate remaining after processing.
399 + $processed_ids = array_merge(
400 + $job['synced'],
401 + array_column( $job['errors'], 'site_id' )
402 + );
403 + $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
220 404
221 - // Recalculate remaining after processing.
222 - $processed_ids = array_merge(
223 - $job['synced'],
224 - array_column( $job['errors'], 'site_id' )
225 - );
226 - $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
227 -
228 - // Reschedule or complete.
229 - if ( count( $remaining ) > 0 ) {
230 - // Schedule next chunk (30 second delay).
231 - if ( ! wp_next_scheduled( 'mainwp_process_sync_job', array( $job_id ) ) ) {
232 - wp_schedule_single_event( time() + 30, 'mainwp_process_sync_job', array( $job_id ) );
405 + // Reschedule or complete.
406 + if ( count( $remaining ) > 0 ) {
407 + // Schedule next chunk (30 second delay).
408 + if ( ! wp_next_scheduled( 'mainwp_process_sync_job', array( $job_id ) ) ) {
409 + wp_schedule_single_event( time() + 30, 'mainwp_process_sync_job', array( $job_id ) );
410 + }
411 + $this->log_debug(
412 + sprintf( 'Sync job %s rescheduled: %d sites remaining', $job_id, count( $remaining ) )
413 + );
414 + } else {
415 + // Job complete.
416 + $job['status'] = 'completed';
417 + $job['completed'] = time();
418 + $job['progress'] = 100;
419 + set_transient( 'mainwp_sync_job_' . $job_id, $job, DAY_IN_SECONDS );
420 + $this->log_debug(
421 + sprintf(
422 + 'Sync job %s completed: %d synced, %d errors',
423 + $job_id,
424 + count( $job['synced'] ),
425 + count( $job['errors'] )
426 + )
427 + );
233 428 }
234 - $this->log_debug(
235 - sprintf( 'Sync job %s rescheduled: %d sites remaining', $job_id, count( $remaining ) )
236 - );
237 - } else {
238 - // Job complete.
239 - $job['status'] = 'completed';
240 - $job['completed'] = time();
241 - $job['progress'] = 100;
242 - set_transient( 'mainwp_sync_job_' . $job_id, $job, DAY_IN_SECONDS );
243 - $this->log_debug(
244 - sprintf(
245 - 'Sync job %s completed: %d synced, %d errors',
246 - $job_id,
247 - count( $job['synced'] ),
248 - count( $job['errors'] )
249 - )
250 - );
429 + } finally {
430 + wp_set_current_user( $previous_user_id );
251 431 }
252 432 }
253 433
254 434 /**
@@ -293,301 +473,485 @@
293 473 $this->log_debug( 'Update job timed out: ' . $job_id );
294 474 return;
295 475 }
296 476
297 - // Status transition: queued -> processing.
298 - if ( 'queued' === $job['status'] ) {
299 - $job['status'] = 'processing';
300 - $job['started'] = time();
301 - }
477 + // Later callbacks must retain their own user context.
478 + $previous_user_id = get_current_user_id();
479 + try {
480 + if ( ! $this->restore_job_owner( $job ) ) {
481 + $this->fail_job_owner_revoked( $job, 'mainwp_update_job_', $job_id );
482 + return;
483 + }
484 + if ( ! $this->owner_holds_required_rights( $job ) ) {
485 + $this->fail_job_rights_revoked( $job, 'mainwp_update_job_', $job_id, 'updated' );
486 + return;
487 + }
488 + $allowed_sites = $this->get_job_allowed_site_ids( $job );
302 489
303 - // Chunk processing (default 20 sites per run).
304 - // Cast to int and clamp to minimum of 1 to prevent infinite loop from misconfigured filter.
305 - $chunk_size = (int) apply_filters( 'mainwp_abilities_cron_chunk_size', 20 );
306 - if ( $chunk_size < 1 ) {
307 - $chunk_size = 1;
308 - }
490 + // Status transition: queued -> processing.
491 + if ( 'queued' === $job['status'] ) {
492 + $job['status'] = 'processing';
493 + $job['started'] = time();
494 + }
309 495
310 - // Normalize result arrays defensively (transient data could be corrupted).
311 - $job['updated'] = isset( $job['updated'] ) && is_array( $job['updated'] ) ? $job['updated'] : array();
312 - $job['errors'] = isset( $job['errors'] ) && is_array( $job['errors'] ) ? $job['errors'] : array();
496 + // Chunk processing (default 20 sites per run).
497 + // Cast to int and clamp to minimum of 1 to prevent infinite loop from misconfigured filter.
498 + $chunk_size = (int) apply_filters( 'mainwp_abilities_cron_chunk_size', 20 );
499 + if ( $chunk_size < 1 ) {
500 + $chunk_size = 1;
501 + }
313 502
314 - // Calculate already processed site IDs.
315 - $processed_ids = array_merge(
316 - $job['updated'],
317 - array_column( $job['errors'], 'site_id' )
318 - );
503 + // Normalize result arrays defensively (transient data could be corrupted).
504 + $job['updated'] = isset( $job['updated'] ) && is_array( $job['updated'] ) ? $job['updated'] : array();
505 + $job['errors'] = isset( $job['errors'] ) && is_array( $job['errors'] ) ? $job['errors'] : array();
506 + $job['skipped'] = isset( $job['skipped'] ) && is_array( $job['skipped'] ) ? $job['skipped'] : array();
507 + $job['unchanged'] = isset( $job['unchanged'] ) && is_array( $job['unchanged'] ) ? $job['unchanged'] : array();
319 508
320 - // Get remaining sites to process.
321 - $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
322 - $chunk = array_slice( $remaining, 0, $chunk_size );
509 + // Calculate already processed site IDs.
510 + $processed_ids = array_merge(
511 + $job['updated'],
512 + $job['unchanged'],
513 + array_column( $job['errors'], 'site_id' )
514 + );
323 515
324 - $this->log_debug(
325 - sprintf(
326 - 'Processing update job %s: %d sites in chunk, %d remaining, types: %s',
327 - $job_id,
328 - count( $chunk ),
329 - count( $remaining ),
330 - implode( ',', $job['types'] )
331 - )
332 - );
516 + // Get remaining sites to process.
517 + $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
518 + $chunk = array_slice( $remaining, 0, $chunk_size );
333 519
334 - // Process each site in the chunk.
335 - foreach ( $chunk as $site_id ) {
336 - try {
337 - $website = MainWP_DB::instance()->get_website_by_id( $site_id );
520 + $this->log_debug(
521 + sprintf(
522 + 'Processing update job %s: %d sites in chunk, %d remaining, types: %s',
523 + $job_id,
524 + count( $chunk ),
525 + count( $remaining ),
526 + implode( ',', $job['types'] )
527 + )
528 + );
338 529
339 - if ( empty( $website ) ) {
340 - $job['errors'][] = array(
341 - 'site_id' => $site_id,
342 - 'code' => 'site_not_found',
343 - 'message' => __( 'Site not found.', 'mainwp' ),
344 - );
345 - ++$job['processed'];
346 - continue;
347 - }
530 + // Process each site in the chunk.
531 + foreach ( $chunk as $site_id ) {
532 + try {
533 + if ( ! $this->owner_can_access_site( $site_id, $allowed_sites ) ) {
534 + $job['errors'][] = array(
535 + 'site_id' => $site_id,
536 + 'code' => 'access_revoked',
537 + 'message' => __( 'Access to this site was revoked before it was processed.', 'mainwp' ),
538 + );
539 + ++$job['processed'];
540 + continue;
541 + }
348 542
349 - $site_success = true;
350 - $site_errors = array();
543 + $website = MainWP_DB::instance()->get_website_by_id( $site_id );
351 544
352 - // Normalize specific_items to avoid TypeError if malformed in transient data.
353 - $specific_items = isset( $job['specific_items'] ) && is_array( $job['specific_items'] )
354 - ? $job['specific_items']
355 - : array();
545 + if ( empty( $website ) ) {
546 + $job['errors'][] = array(
547 + 'site_id' => $site_id,
548 + 'code' => 'site_not_found',
549 + 'message' => __( 'Site not found.', 'mainwp' ),
550 + );
551 + ++$job['processed'];
552 + continue;
553 + }
356 554
357 - // Process each update type.
358 - foreach ( $job['types'] as $type ) {
359 - try {
360 - switch ( $type ) {
361 - case 'core':
362 - /**
363 - * Fires before WordPress core update action.
364 - *
365 - * @since 5.4
366 - *
367 - * @param object $website Site object.
368 - */
369 - do_action( 'mainwp_before_core_update', $website );
555 + $site_success = true;
556 + $site_errors = array();
557 + $site_dispatched = false;
370 558
371 - $information = MainWP_Connect::fetch_url_authed( $website, 'upgrade' );
559 + // Normalize specific_items to avoid TypeError if malformed in transient data.
560 + $specific_items = isset( $job['specific_items'] ) && is_array( $job['specific_items'] )
561 + ? $job['specific_items']
562 + : array();
372 563
373 - // Validate response - check for transport failure or API error.
374 - if ( false === $information ) {
375 - $site_success = false;
376 - $site_errors[] = 'core: ' . __( 'Connection failed - no response from child site.', 'mainwp' );
377 - $this->log_debug(
378 - sprintf(
379 - 'Core upgrade failed for site %d: transport failure (false response)',
380 - $website->id
381 - )
382 - );
564 + // The restored owner's global lists and current holds are read at dispatch time.
565 + $plan = MainWP_Abilities_Updates::build_update_plan( array( $website ), $job['types'], $specific_items );
566 + if ( ! empty( $plan['errors'] ) ) {
567 + foreach ( $plan['errors'] as $error ) {
568 + $job['errors'][] = array(
569 + 'site_id' => $site_id,
570 + 'code' => $error['code'],
571 + 'message' => $error['message'],
572 + );
573 + }
574 + ++$job['processed'];
575 + continue;
576 + }
577 + $planned_slugs = array(
578 + 'core' => array(),
579 + 'plugin' => array(),
580 + 'theme' => array(),
581 + 'translation' => array(),
582 + );
583 + foreach ( $plan['sites'] as $entry ) {
584 + foreach ( $entry['skipped'] as $item ) {
585 + $job['skipped'][] = array(
586 + 'site_id' => $site_id,
587 + 'type' => $item['type'],
588 + 'slug' => $item['slug'],
589 + 'reason' => $item['reason'],
590 + );
591 + }
592 + foreach ( $entry['items'] as $item ) {
593 + $planned_slugs[ $item['type'] ][] = $item['slug'];
594 + }
595 + }
383 596
384 - /**
385 - * Fires after WordPress core update action.
386 - *
387 - * @since 5.4
388 - *
389 - * @param array|false $information Response from child site (false on failure).
390 - * @param object $website Site object.
391 - */
392 - do_action( 'mainwp_after_core_update', $information, $website );
393 - } elseif ( is_array( $information ) && isset( $information['error'] ) ) {
394 - $site_success = false;
395 - $site_errors[] = 'core: ' . $information['error'];
396 - $this->log_debug(
397 - sprintf(
398 - 'Core upgrade failed for site %d: %s',
399 - $website->id,
400 - $information['error']
401 - )
402 - );
403 -
404 - /** This action is documented above in this switch case. */
405 - do_action( 'mainwp_after_core_update', $information, $website );
406 - } elseif ( ! is_array( $information ) ) {
407 - $site_success = false;
408 - $site_errors[] = 'core: ' . __( 'Invalid response from child site.', 'mainwp' );
409 - $this->log_debug(
410 - sprintf(
411 - 'Core upgrade failed for site %d: invalid response type (%s), raw: %s',
412 - $website->id,
413 - gettype( $information ),
414 - is_scalar( $information ) ? $information : wp_json_encode( $information )
415 - )
416 - );
417 -
418 - /** This action is documented above in this switch case. */
419 - do_action( 'mainwp_after_core_update', $information, $website );
420 - } else {
421 - // Success - fire after-action and sync.
422 - /** This action is documented above in this switch case. */
423 - do_action( 'mainwp_after_core_update', $information, $website );
424 -
425 - // Sync site data immediately if child returned sync info.
426 - if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
427 - MainWP_Sync::sync_information_array( $website, $information['sync'] );
597 + // Process each update type.
598 + foreach ( $job['types'] as $type ) {
599 + try {
600 + switch ( $type ) {
601 + case 'core':
602 + if ( empty( $planned_slugs['core'] ) ) {
603 + break;
428 604 }
429 - }
430 - break;
431 605
432 - case 'plugins':
433 - $slugs = $this->get_update_slugs_for_site( $website, 'plugin', $specific_items );
434 - if ( ! empty( $slugs ) ) {
435 - $slugs_list = implode( ',', $slugs );
436 -
437 606 /**
438 - * Fires before plugin/theme/translation update actions.
607 + * Fires before WordPress core update action.
439 608 *
440 - * @since 4.1
609 + * @since 5.4
441 610 *
442 - * @param string $type Update type: 'plugin', 'theme', or 'translation'.
443 - * @param string $list Comma-separated list of slugs being updated.
444 611 * @param object $website Site object.
445 612 */
446 - do_action( 'mainwp_before_plugin_theme_translation_update', 'plugin', $slugs_list, $website );
613 + do_action( 'mainwp_before_core_update', $website );
447 614
448 - $information = MainWP_Connect::fetch_url_authed(
449 - $website,
450 - 'upgradeplugintheme',
451 - array(
452 - 'type' => 'plugin',
453 - 'list' => urldecode( $slugs_list ),
454 - )
455 - );
615 + $site_dispatched = true;
616 + $information = MainWP_Connect::fetch_url_authed( $website, 'upgrade' );
456 617
618 + MainWP_Updates_Handler::activity_log_upgrade( $website, $information ); // see do_action_mainwp_install_actions for more details about the upgrade: plugins, themes, translation.
619 +
457 620 // Validate response - check for transport failure or API error.
458 621 if ( false === $information ) {
459 622 $site_success = false;
460 - $site_errors[] = 'plugins: ' . __( 'Connection failed - no response from child site.', 'mainwp' );
623 + $site_errors[] = 'core: ' . __( 'Connection failed - no response from child site.', 'mainwp' );
461 624 $this->log_debug(
462 625 sprintf(
463 - 'Plugin upgrade failed for site %d (slugs: %s): transport failure (false response)',
464 - $website->id,
465 - $slugs_list
626 + 'Core upgrade failed for site %d: transport failure (false response)',
627 + $website->id
466 628 )
467 629 );
468 630
469 - // Fire after-action even on failure so hooks always run.
470 - /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
471 - do_action( 'mainwp_after_plugin_theme_translation_update', null, 'plugin', $slugs_list, $website );
631 + /**
632 + * Fires after WordPress core update action.
633 + *
634 + * @since 5.4
635 + *
636 + * @param array|false $information Response from child site (false on failure).
637 + * @param object $website Site object.
638 + */
639 + do_action( 'mainwp_after_core_update', $information, $website );
472 640 } elseif ( is_array( $information ) && isset( $information['error'] ) ) {
473 641 $site_success = false;
474 - $site_errors[] = 'plugins: ' . $information['error'];
642 + $site_errors[] = 'core: ' . $information['error'];
475 643 $this->log_debug(
476 644 sprintf(
477 - 'Plugin upgrade failed for site %d (slugs: %s): %s',
645 + 'Core upgrade failed for site %d: %s',
478 646 $website->id,
479 - $slugs_list,
480 647 $information['error']
481 648 )
482 649 );
483 650
484 - // Fire after-action even on failure so hooks always run.
485 - /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
486 - do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'plugin', $slugs_list, $website );
651 + /** This action is documented above in this switch case. */
652 + do_action( 'mainwp_after_core_update', $information, $website );
487 653 } elseif ( ! is_array( $information ) ) {
488 654 $site_success = false;
489 - $site_errors[] = 'plugins: ' . __( 'Invalid response from child site.', 'mainwp' );
655 + $site_errors[] = 'core: ' . __( 'Invalid response from child site.', 'mainwp' );
490 656 $this->log_debug(
491 657 sprintf(
492 - 'Plugin upgrade failed for site %d (slugs: %s): invalid response type (%s), raw: %s',
658 + 'Core upgrade failed for site %d: invalid response type (%s), raw: %s',
493 659 $website->id,
494 - $slugs_list,
495 660 gettype( $information ),
496 661 is_scalar( $information ) ? $information : wp_json_encode( $information )
497 662 )
498 663 );
499 664
500 - // Fire after-action even on failure so hooks always run.
501 - /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
502 - do_action( 'mainwp_after_plugin_theme_translation_update', null, 'plugin', $slugs_list, $website );
503 - } elseif ( isset( $information['upgrades_error'] ) && ! empty( $information['upgrades_error'] ) ) {
504 - // Partial failure - some plugins failed to update.
505 - $error_msgs = array();
506 - foreach ( $information['upgrades_error'] as $slug => $error_msg ) {
507 - $error_msgs[] = $slug . ': ' . $error_msg;
665 + /** This action is documented above in this switch case. */
666 + do_action( 'mainwp_after_core_update', $information, $website );
667 + } else {
668 + // Success - fire after-action and sync.
669 + /** This action is documented above in this switch case. */
670 + do_action( 'mainwp_after_core_update', $information, $website );
671 +
672 + // Sync site data immediately if child returned sync info.
673 + if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
674 + MainWP_Sync::sync_information_array( $website, $information['sync'] );
508 675 }
509 - $site_success = false;
510 - $site_errors[] = 'plugins: ' . implode( '; ', $error_msgs );
511 - $this->log_debug(
512 - sprintf(
513 - 'Plugin upgrade partial failure for site %d (slugs: %s): %s',
514 - $website->id,
515 - $slugs_list,
516 - implode( '; ', $error_msgs )
517 - )
518 - );
676 + }
677 + break;
519 678
520 - // Still fire after-action and sync for partial success.
679 + case 'plugins':
680 + $slugs = $planned_slugs['plugin'];
681 + if ( ! empty( $slugs ) ) {
682 + $slugs_list = implode( ',', $slugs );
683 +
521 684 /**
522 - * Fires after plugin/theme/translation update actions.
685 + * Fires before plugin/theme/translation update actions.
523 686 *
524 687 * @since 4.1
525 688 *
526 - * @param array $information Response from child site.
527 - * @param string $type Update type: 'plugin', 'theme', or 'translation'.
528 - * @param string $list Comma-separated list of slugs that were updated.
529 - * @param object $website Site object.
689 + * @param string $type Update type: 'plugin', 'theme', or 'translation'.
690 + * @param string $list Comma-separated list of slugs being updated.
691 + * @param object $website Site object.
530 692 */
531 - do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'plugin', $slugs_list, $website );
693 + do_action( 'mainwp_before_plugin_theme_translation_update', 'plugin', $slugs_list, $website );
532 694
533 - // Sync site data immediately if child returned sync info.
534 - if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
535 - MainWP_Sync::sync_information_array( $website, $information['sync'] );
695 + $site_dispatched = true;
696 + $information = MainWP_Connect::fetch_url_authed(
697 + $website,
698 + 'upgradeplugintheme',
699 + array(
700 + 'type' => 'plugin',
701 + 'list' => urldecode( $slugs_list ),
702 + )
703 + );
704 +
705 + if ( is_array( $information ) && isset( $information['other_data'] ) ) { // ok.
706 + mainwp_get_actions_handler_instance()->do_action_mainwp_install_actions( $website, 'updated', $information['other_data'], 'plugin' );
536 707 }
537 - } else {
538 - // Success - fire after-action and sync.
539 - /** This action is documented above in this switch case. */
540 - do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'plugin', $slugs_list, $website );
541 708
542 - // Sync site data immediately if child returned sync info.
543 - if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
544 - MainWP_Sync::sync_information_array( $website, $information['sync'] );
709 + // Validate response - check for transport failure or API error.
710 + if ( false === $information ) {
711 + $site_success = false;
712 + $site_errors[] = 'plugins: ' . __( 'Connection failed - no response from child site.', 'mainwp' );
713 + $this->log_debug(
714 + sprintf(
715 + 'Plugin upgrade failed for site %d (slugs: %s): transport failure (false response)',
716 + $website->id,
717 + $slugs_list
718 + )
719 + );
720 +
721 + // Fire after-action even on failure so hooks always run.
722 + /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
723 + do_action( 'mainwp_after_plugin_theme_translation_update', null, 'plugin', $slugs_list, $website );
724 + } elseif ( is_array( $information ) && isset( $information['error'] ) ) {
725 + $site_success = false;
726 + $site_errors[] = 'plugins: ' . $information['error'];
727 + $this->log_debug(
728 + sprintf(
729 + 'Plugin upgrade failed for site %d (slugs: %s): %s',
730 + $website->id,
731 + $slugs_list,
732 + $information['error']
733 + )
734 + );
735 +
736 + // Fire after-action even on failure so hooks always run.
737 + /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
738 + do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'plugin', $slugs_list, $website );
739 + } elseif ( ! is_array( $information ) ) {
740 + $site_success = false;
741 + $site_errors[] = 'plugins: ' . __( 'Invalid response from child site.', 'mainwp' );
742 + $this->log_debug(
743 + sprintf(
744 + 'Plugin upgrade failed for site %d (slugs: %s): invalid response type (%s), raw: %s',
745 + $website->id,
746 + $slugs_list,
747 + gettype( $information ),
748 + is_scalar( $information ) ? $information : wp_json_encode( $information )
749 + )
750 + );
751 +
752 + // Fire after-action even on failure so hooks always run.
753 + /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
754 + do_action( 'mainwp_after_plugin_theme_translation_update', null, 'plugin', $slugs_list, $website );
755 + } elseif ( isset( $information['upgrades_error'] ) && ! empty( $information['upgrades_error'] ) ) {
756 + // Partial failure - some plugins failed to update.
757 + $error_msgs = array();
758 + foreach ( $information['upgrades_error'] as $slug => $error_msg ) {
759 + $error_msgs[] = $slug . ': ' . $error_msg;
760 + }
761 + $site_success = false;
762 + $site_errors[] = 'plugins: ' . implode( '; ', $error_msgs );
763 + $this->log_debug(
764 + sprintf(
765 + 'Plugin upgrade partial failure for site %d (slugs: %s): %s',
766 + $website->id,
767 + $slugs_list,
768 + implode( '; ', $error_msgs )
769 + )
770 + );
771 +
772 + // Still fire after-action and sync for partial success.
773 + /**
774 + * Fires after plugin/theme/translation update actions.
775 + *
776 + * @since 4.1
777 + *
778 + * @param array $information Response from child site.
779 + * @param string $type Update type: 'plugin', 'theme', or 'translation'.
780 + * @param string $list Comma-separated list of slugs that were updated.
781 + * @param object $website Site object.
782 + */
783 + do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'plugin', $slugs_list, $website );
784 +
785 + // Sync site data immediately if child returned sync info.
786 + if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
787 + MainWP_Sync::sync_information_array( $website, $information['sync'] );
788 + }
789 + } else {
790 + // Success - fire after-action and sync.
791 + /** This action is documented above in this switch case. */
792 + do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'plugin', $slugs_list, $website );
793 +
794 + // Sync site data immediately if child returned sync info.
795 + if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
796 + MainWP_Sync::sync_information_array( $website, $information['sync'] );
797 + }
545 798 }
546 799 }
547 - }
548 - break;
800 + break;
549 801
550 - case 'themes':
551 - $slugs = $this->get_update_slugs_for_site( $website, 'theme', $specific_items );
552 - if ( ! empty( $slugs ) ) {
802 + case 'themes':
803 + $slugs = $planned_slugs['theme'];
804 + if ( ! empty( $slugs ) ) {
805 + $slugs_list = implode( ',', $slugs );
806 +
807 + /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
808 + do_action( 'mainwp_before_plugin_theme_translation_update', 'theme', $slugs_list, $website );
809 +
810 + $site_dispatched = true;
811 + $information = MainWP_Connect::fetch_url_authed(
812 + $website,
813 + 'upgradeplugintheme',
814 + array(
815 + 'type' => 'theme',
816 + 'list' => urldecode( $slugs_list ),
817 + )
818 + );
819 +
820 + if ( is_array( $information ) && isset( $information['other_data'] ) ) { // ok.
821 + mainwp_get_actions_handler_instance()->do_action_mainwp_install_actions( $website, 'updated', $information['other_data'], 'theme' );
822 + }
823 +
824 + // Validate response - check for transport failure or API error.
825 + if ( false === $information ) {
826 + $site_success = false;
827 + $site_errors[] = 'themes: ' . __( 'Connection failed - no response from child site.', 'mainwp' );
828 + $this->log_debug(
829 + sprintf(
830 + 'Theme upgrade failed for site %d (slugs: %s): transport failure (false response)',
831 + $website->id,
832 + $slugs_list
833 + )
834 + );
835 +
836 + // Fire after-action even on failure so hooks always run.
837 + /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
838 + do_action( 'mainwp_after_plugin_theme_translation_update', null, 'theme', $slugs_list, $website );
839 + } elseif ( is_array( $information ) && isset( $information['error'] ) ) {
840 + $site_success = false;
841 + $site_errors[] = 'themes: ' . $information['error'];
842 + $this->log_debug(
843 + sprintf(
844 + 'Theme upgrade failed for site %d (slugs: %s): %s',
845 + $website->id,
846 + $slugs_list,
847 + $information['error']
848 + )
849 + );
850 +
851 + // Fire after-action even on failure so hooks always run.
852 + /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
853 + do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'theme', $slugs_list, $website );
854 + } elseif ( ! is_array( $information ) ) {
855 + $site_success = false;
856 + $site_errors[] = 'themes: ' . __( 'Invalid response from child site.', 'mainwp' );
857 + $this->log_debug(
858 + sprintf(
859 + 'Theme upgrade failed for site %d (slugs: %s): invalid response type (%s), raw: %s',
860 + $website->id,
861 + $slugs_list,
862 + gettype( $information ),
863 + is_scalar( $information ) ? $information : wp_json_encode( $information )
864 + )
865 + );
866 +
867 + // Fire after-action even on failure so hooks always run.
868 + /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
869 + do_action( 'mainwp_after_plugin_theme_translation_update', null, 'theme', $slugs_list, $website );
870 + } elseif ( isset( $information['upgrades_error'] ) && ! empty( $information['upgrades_error'] ) ) {
871 + // Partial failure - some themes failed to update.
872 + $error_msgs = array();
873 + foreach ( $information['upgrades_error'] as $slug => $error_msg ) {
874 + $error_msgs[] = $slug . ': ' . $error_msg;
875 + }
876 + $site_success = false;
877 + $site_errors[] = 'themes: ' . implode( '; ', $error_msgs );
878 + $this->log_debug(
879 + sprintf(
880 + 'Theme upgrade partial failure for site %d (slugs: %s): %s',
881 + $website->id,
882 + $slugs_list,
883 + implode( '; ', $error_msgs )
884 + )
885 + );
886 +
887 + // Still fire after-action and sync for partial success.
888 + /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
889 + do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'theme', $slugs_list, $website );
890 +
891 + // Sync site data immediately if child returned sync info.
892 + if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
893 + MainWP_Sync::sync_information_array( $website, $information['sync'] );
894 + }
895 + } else {
896 + // Success - fire after-action and sync.
897 + /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
898 + do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'theme', $slugs_list, $website );
899 +
900 + // Sync site data immediately if child returned sync info.
901 + if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
902 + MainWP_Sync::sync_information_array( $website, $information['sync'] );
903 + }
904 + }
905 + }
906 + break;
907 +
908 + case 'translations':
909 + $slugs = $planned_slugs['translation'];
910 + if ( empty( $slugs ) ) {
911 + break;
912 + }
553 913 $slugs_list = implode( ',', $slugs );
554 914
555 915 /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
556 - do_action( 'mainwp_before_plugin_theme_translation_update', 'theme', $slugs_list, $website );
916 + do_action( 'mainwp_before_plugin_theme_translation_update', 'translation', $slugs_list, $website );
557 917
558 - $information = MainWP_Connect::fetch_url_authed(
918 + $site_dispatched = true;
919 + $information = MainWP_Connect::fetch_url_authed(
559 920 $website,
560 - 'upgradeplugintheme',
921 + 'upgradetranslation',
561 922 array(
562 - 'type' => 'theme',
563 - 'list' => urldecode( $slugs_list ),
564 - )
923 + 'type' => 'translation',
924 + 'list' => $slugs_list,
925 + ),
926 + true
565 927 );
566 928
929 + if ( is_array( $information ) && isset( $information['other_data'] ) ) {
930 + mainwp_get_actions_handler_instance()->do_action_mainwp_install_actions( $website, 'updated', $information['other_data'], 'trans' );
931 + }
932 +
567 933 // Validate response - check for transport failure or API error.
568 934 if ( false === $information ) {
569 935 $site_success = false;
570 - $site_errors[] = 'themes: ' . __( 'Connection failed - no response from child site.', 'mainwp' );
936 + $site_errors[] = 'translations: ' . __( 'Connection failed - no response from child site.', 'mainwp' );
571 937 $this->log_debug(
572 938 sprintf(
573 - 'Theme upgrade failed for site %d (slugs: %s): transport failure (false response)',
574 - $website->id,
575 - $slugs_list
939 + 'Translation upgrade failed for site %d: transport failure (false response)',
940 + $website->id
576 941 )
577 942 );
578 943
579 944 // Fire after-action even on failure so hooks always run.
580 945 /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
581 - do_action( 'mainwp_after_plugin_theme_translation_update', null, 'theme', $slugs_list, $website );
946 + do_action( 'mainwp_after_plugin_theme_translation_update', null, 'translation', $slugs_list, $website );
582 947 } elseif ( is_array( $information ) && isset( $information['error'] ) ) {
583 948 $site_success = false;
584 - $site_errors[] = 'themes: ' . $information['error'];
949 + $site_errors[] = 'translations: ' . $information['error'];
585 950 $this->log_debug(
586 951 sprintf(
587 - 'Theme upgrade failed for site %d (slugs: %s): %s',
952 + 'Translation upgrade failed for site %d: %s',
588 953 $website->id,
589 - $slugs_list,
590 954 $information['error']
591 955 )
592 956 );
593 957
@@ -592,38 +956,35 @@
592 956 );
593 957
594 958 // Fire after-action even on failure so hooks always run.
595 959 /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
596 - do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'theme', $slugs_list, $website );
960 + do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'translation', $slugs_list, $website );
597 961 } elseif ( ! is_array( $information ) ) {
598 962 $site_success = false;
599 - $site_errors[] = 'themes: ' . __( 'Invalid response from child site.', 'mainwp' );
963 + $site_errors[] = 'translations: ' . __( 'Invalid response from child site.', 'mainwp' );
600 964 $this->log_debug(
601 965 sprintf(
602 - 'Theme upgrade failed for site %d (slugs: %s): invalid response type (%s), raw: %s',
966 + 'Translation upgrade failed for site %d: invalid response type (%s)',
603 967 $website->id,
604 - $slugs_list,
605 - gettype( $information ),
606 - is_scalar( $information ) ? $information : wp_json_encode( $information )
968 + gettype( $information )
607 969 )
608 970 );
609 971
610 972 // Fire after-action even on failure so hooks always run.
611 973 /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
612 - do_action( 'mainwp_after_plugin_theme_translation_update', null, 'theme', $slugs_list, $website );
974 + do_action( 'mainwp_after_plugin_theme_translation_update', null, 'translation', $slugs_list, $website );
613 975 } elseif ( isset( $information['upgrades_error'] ) && ! empty( $information['upgrades_error'] ) ) {
614 - // Partial failure - some themes failed to update.
976 + // Partial failure - some translations failed to update.
615 977 $error_msgs = array();
616 978 foreach ( $information['upgrades_error'] as $slug => $error_msg ) {
617 979 $error_msgs[] = $slug . ': ' . $error_msg;
618 980 }
619 981 $site_success = false;
620 - $site_errors[] = 'themes: ' . implode( '; ', $error_msgs );
982 + $site_errors[] = 'translations: ' . implode( '; ', $error_msgs );
621 983 $this->log_debug(
622 984 sprintf(
623 - 'Theme upgrade partial failure for site %d (slugs: %s): %s',
985 + 'Translation upgrade partial failure for site %d: %s',
624 986 $website->id,
625 - $slugs_list,
626 987 implode( '; ', $error_msgs )
627 988 )
628 989 );
629 990
@@ -628,9 +989,9 @@
628 989 );
629 990
630 991 // Still fire after-action and sync for partial success.
631 992 /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
632 - do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'theme', $slugs_list, $website );
993 + do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'translation', $slugs_list, $website );
633 994
634 995 // Sync site data immediately if child returned sync info.
635 996 if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
636 997 MainWP_Sync::sync_information_array( $website, $information['sync'] );
@@ -637,9 +998,9 @@
637 998 }
638 999 } else {
639 1000 // Success - fire after-action and sync.
640 1001 /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
641 - do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'theme', $slugs_list, $website );
1002 + do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'translation', $slugs_list, $website );
642 1003
643 1004 // Sync site data immediately if child returned sync info.
644 1005 if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
645 1006 MainWP_Sync::sync_information_array( $website, $information['sync'] );
@@ -644,193 +1005,84 @@
644 1005 if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
645 1006 MainWP_Sync::sync_information_array( $website, $information['sync'] );
646 1007 }
647 1008 }
648 - }
649 - break;
1009 + break;
1010 + default:
1011 + // do nothing for unknown type.
1012 + break;
1013 + }
1014 + } catch ( \Exception $e ) {
1015 + $site_success = false;
1016 + $site_errors[] = $type . ': ' . $e->getMessage();
1017 + }
1018 + }
650 1019
651 - case 'translations':
652 - /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
653 - do_action( 'mainwp_before_plugin_theme_translation_update', 'translation', '', $website );
654 -
655 - $information = MainWP_Connect::fetch_url_authed( $website, 'upgradetranslation' );
656 -
657 - // Validate response - check for transport failure or API error.
658 - if ( false === $information ) {
659 - $site_success = false;
660 - $site_errors[] = 'translations: ' . __( 'Connection failed - no response from child site.', 'mainwp' );
661 - $this->log_debug(
662 - sprintf(
663 - 'Translation upgrade failed for site %d: transport failure (false response)',
664 - $website->id
665 - )
666 - );
667 -
668 - // Fire after-action even on failure so hooks always run.
669 - /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
670 - do_action( 'mainwp_after_plugin_theme_translation_update', null, 'translation', '', $website );
671 - } elseif ( is_array( $information ) && isset( $information['error'] ) ) {
672 - $site_success = false;
673 - $site_errors[] = 'translations: ' . $information['error'];
674 - $this->log_debug(
675 - sprintf(
676 - 'Translation upgrade failed for site %d: %s',
677 - $website->id,
678 - $information['error']
679 - )
680 - );
681 -
682 - // Fire after-action even on failure so hooks always run.
683 - /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
684 - do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'translation', '', $website );
685 - } elseif ( ! is_array( $information ) ) {
686 - $site_success = false;
687 - $site_errors[] = 'translations: ' . __( 'Invalid response from child site.', 'mainwp' );
688 - $this->log_debug(
689 - sprintf(
690 - 'Translation upgrade failed for site %d: invalid response type (%s)',
691 - $website->id,
692 - gettype( $information )
693 - )
694 - );
695 -
696 - // Fire after-action even on failure so hooks always run.
697 - /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
698 - do_action( 'mainwp_after_plugin_theme_translation_update', null, 'translation', '', $website );
699 - } elseif ( isset( $information['upgrades_error'] ) && ! empty( $information['upgrades_error'] ) ) {
700 - // Partial failure - some translations failed to update.
701 - $error_msgs = array();
702 - foreach ( $information['upgrades_error'] as $slug => $error_msg ) {
703 - $error_msgs[] = $slug . ': ' . $error_msg;
704 - }
705 - $site_success = false;
706 - $site_errors[] = 'translations: ' . implode( '; ', $error_msgs );
707 - $this->log_debug(
708 - sprintf(
709 - 'Translation upgrade partial failure for site %d: %s',
710 - $website->id,
711 - implode( '; ', $error_msgs )
712 - )
713 - );
714 -
715 - // Still fire after-action and sync for partial success.
716 - /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
717 - do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'translation', '', $website );
718 -
719 - // Sync site data immediately if child returned sync info.
720 - if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
721 - MainWP_Sync::sync_information_array( $website, $information['sync'] );
722 - }
723 - } else {
724 - // Success - fire after-action and sync.
725 - /** This action is documented in includes/abilities/class-mainwp-abilities-cron.php */
726 - do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'translation', '', $website );
727 -
728 - // Sync site data immediately if child returned sync info.
729 - if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) {
730 - MainWP_Sync::sync_information_array( $website, $information['sync'] );
731 - }
732 - }
733 - break;
734 - default:
735 - // do nothing for unknown type.
736 - break;
1020 + if ( $site_success && empty( $site_errors ) ) {
1021 + if ( $site_dispatched ) {
1022 + $job['updated'][] = $site_id;
1023 + } else {
1024 + $job['unchanged'][] = $site_id;
737 1025 }
738 - } catch ( \Exception $e ) {
739 - $site_success = false;
740 - $site_errors[] = $type . ': ' . $e->getMessage();
1026 + } else {
1027 + $job['errors'][] = array(
1028 + 'site_id' => $site_id,
1029 + 'code' => 'update_failed',
1030 + 'message' => implode( '; ', $site_errors ),
1031 + );
741 1032 }
742 - }
743 -
744 - if ( $site_success && empty( $site_errors ) ) {
745 - $job['updated'][] = $site_id;
746 - } else {
1033 + } catch ( \Exception $e ) {
747 1034 $job['errors'][] = array(
748 1035 'site_id' => $site_id,
749 - 'code' => 'update_failed',
750 - 'message' => implode( '; ', $site_errors ),
1036 + 'code' => 'exception',
1037 + 'message' => $e->getMessage(),
751 1038 );
752 1039 }
753 - } catch ( \Exception $e ) {
754 - $job['errors'][] = array(
755 - 'site_id' => $site_id,
756 - 'code' => 'exception',
757 - 'message' => $e->getMessage(),
758 - );
1040 +
1041 + ++$job['processed'];
1042 + $job['progress'] = $job['total'] > 0
1043 + ? round( ( $job['processed'] / $job['total'] ) * 100 )
1044 + : 0;
759 1045 }
760 1046
761 - ++$job['processed'];
762 - $job['progress'] = $job['total'] > 0
763 - ? round( ( $job['processed'] / $job['total'] ) * 100 )
764 - : 0;
765 - }
1047 + // Save progress.
1048 + set_transient( 'mainwp_update_job_' . $job_id, $job, DAY_IN_SECONDS );
766 1049
767 - // Save progress.
768 - set_transient( 'mainwp_update_job_' . $job_id, $job, DAY_IN_SECONDS );
769 -
770 - // Recalculate remaining after processing.
771 - $processed_ids = array_merge(
772 - $job['updated'],
773 - array_column( $job['errors'], 'site_id' )
774 - );
775 - $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
776 -
777 - // Reschedule or complete.
778 - if ( count( $remaining ) > 0 ) {
779 - // Schedule next chunk (30 second delay).
780 - if ( ! wp_next_scheduled( 'mainwp_process_update_job', array( $job_id ) ) ) {
781 - wp_schedule_single_event( time() + 30, 'mainwp_process_update_job', array( $job_id ) );
782 - }
783 - $this->log_debug(
784 - sprintf( 'Update job %s rescheduled: %d sites remaining', $job_id, count( $remaining ) )
1050 + // Recalculate remaining after processing.
1051 + $processed_ids = array_merge(
1052 + $job['updated'],
1053 + $job['unchanged'],
1054 + array_column( $job['errors'], 'site_id' )
785 1055 );
786 - } else {
787 - // Job complete.
788 - $job['status'] = 'completed';
789 - $job['completed'] = time();
790 - $job['progress'] = 100;
791 - set_transient( 'mainwp_update_job_' . $job_id, $job, DAY_IN_SECONDS );
792 - $this->log_debug(
793 - sprintf(
794 - 'Update job %s completed: %d updated, %d errors',
795 - $job_id,
796 - count( $job['updated'] ),
797 - count( $job['errors'] )
798 - )
799 - );
800 - }
801 - }
1056 + $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
802 1057
803 - /**
804 - * Get update slugs for a site, optionally filtered by specific items.
805 - *
806 - * @param object $website Site object.
807 - * @param string $type Type: 'plugin' or 'theme'.
808 - * @param array $specific_items Optional array of specific slugs to filter.
809 - * @return array Array of slugs to update.
810 - */
811 - private function get_update_slugs_for_site( $website, string $type, array $specific_items = array() ): array {
812 - $slugs = array();
813 -
814 - if ( 'plugin' === $type ) {
815 - $updates = json_decode( $website->plugin_upgrades, true );
816 - } else {
817 - $updates = json_decode( $website->theme_upgrades, true );
818 - }
819 -
820 - if ( ! is_array( $updates ) ) {
821 - $updates = array();
822 - }
823 -
824 - foreach ( array_keys( $updates ) as $slug ) {
825 - // If specific items provided, filter to only those.
826 - if ( ! empty( $specific_items ) && ! in_array( $slug, $specific_items, true ) ) {
827 - continue;
1058 + // Reschedule or complete.
1059 + if ( count( $remaining ) > 0 ) {
1060 + // Schedule next chunk (30 second delay).
1061 + if ( ! wp_next_scheduled( 'mainwp_process_update_job', array( $job_id ) ) ) {
1062 + wp_schedule_single_event( time() + 30, 'mainwp_process_update_job', array( $job_id ) );
1063 + }
1064 + $this->log_debug(
1065 + sprintf( 'Update job %s rescheduled: %d sites remaining', $job_id, count( $remaining ) )
1066 + );
1067 + } else {
1068 + // Job complete.
1069 + $job['status'] = 'completed';
1070 + $job['completed'] = time();
1071 + $job['progress'] = 100;
1072 + set_transient( 'mainwp_update_job_' . $job_id, $job, DAY_IN_SECONDS );
1073 + $this->log_debug(
1074 + sprintf(
1075 + 'Update job %s completed: %d updated, %d errors',
1076 + $job_id,
1077 + count( $job['updated'] ),
1078 + count( $job['errors'] )
1079 + )
1080 + );
828 1081 }
829 - $slugs[] = $slug;
1082 + } finally {
1083 + wp_set_current_user( $previous_user_id );
830 1084 }
831 -
832 - return $slugs;
833 1085 }
834 1086
835 1087 /**
836 1088 * Process a batch operation job (reconnect, disconnect, check, suspend).
@@ -874,184 +1126,210 @@
874 1126 $this->log_debug( 'Batch job timed out: ' . $job_id );
875 1127 return;
876 1128 }
877 1129
878 - // Status transition: queued -> processing.
879 - if ( 'queued' === $job['status'] ) {
880 - $job['status'] = 'processing';
881 - $job['started'] = time();
882 - }
1130 + // Later callbacks must retain their own user context.
1131 + $previous_user_id = get_current_user_id();
1132 + try {
1133 + if ( ! $this->restore_job_owner( $job ) ) {
1134 + $this->fail_job_owner_revoked( $job, 'mainwp_batch_job_', $job_id );
1135 + return;
1136 + }
1137 + if ( ! $this->owner_holds_required_rights( $job ) ) {
1138 + $this->fail_job_rights_revoked( $job, 'mainwp_batch_job_', $job_id, 'successful' );
1139 + return;
1140 + }
1141 + $allowed_sites = $this->get_job_allowed_site_ids( $job );
883 1142
884 - // Chunk processing (default 20 sites per run).
885 - // Cast to int and clamp to minimum of 1 to prevent infinite loop from misconfigured filter.
886 - $chunk_size = (int) apply_filters( 'mainwp_abilities_cron_chunk_size', 20 );
887 - if ( $chunk_size < 1 ) {
888 - $chunk_size = 1;
889 - }
1143 + // Status transition: queued -> processing.
1144 + if ( 'queued' === $job['status'] ) {
1145 + $job['status'] = 'processing';
1146 + $job['started'] = time();
1147 + }
890 1148
891 - // Normalize result arrays defensively (transient data could be corrupted).
892 - $job['successful'] = isset( $job['successful'] ) && is_array( $job['successful'] ) ? $job['successful'] : array();
893 - $job['errors'] = isset( $job['errors'] ) && is_array( $job['errors'] ) ? $job['errors'] : array();
1149 + // Chunk processing (default 20 sites per run).
1150 + // Cast to int and clamp to minimum of 1 to prevent infinite loop from misconfigured filter.
1151 + $chunk_size = (int) apply_filters( 'mainwp_abilities_cron_chunk_size', 20 );
1152 + if ( $chunk_size < 1 ) {
1153 + $chunk_size = 1;
1154 + }
894 1155
895 - // Calculate already processed site IDs.
896 - $processed_ids = array_merge(
897 - $job['successful'],
898 - array_column( $job['errors'], 'site_id' )
899 - );
1156 + // Normalize result arrays defensively (transient data could be corrupted).
1157 + $job['successful'] = isset( $job['successful'] ) && is_array( $job['successful'] ) ? $job['successful'] : array();
1158 + $job['errors'] = isset( $job['errors'] ) && is_array( $job['errors'] ) ? $job['errors'] : array();
900 1159
901 - // Get remaining sites to process.
902 - $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
903 - $chunk = array_slice( $remaining, 0, $chunk_size );
1160 + // Calculate already processed site IDs.
1161 + $processed_ids = array_merge(
1162 + $job['successful'],
1163 + array_column( $job['errors'], 'site_id' )
1164 + );
904 1165
905 - $this->log_debug(
906 - sprintf(
907 - 'Processing batch job %s (%s): %d sites in chunk, %d remaining',
908 - $job_id,
909 - $job['job_type'],
910 - count( $chunk ),
911 - count( $remaining )
912 - )
913 - );
1166 + // Get remaining sites to process.
1167 + $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
1168 + $chunk = array_slice( $remaining, 0, $chunk_size );
914 1169
915 - // Process each site in the chunk.
916 - foreach ( $chunk as $site_id ) {
917 - try {
918 - $website = MainWP_DB::instance()->get_website_by_id( $site_id );
1170 + $this->log_debug(
1171 + sprintf(
1172 + 'Processing batch job %s (%s): %d sites in chunk, %d remaining',
1173 + $job_id,
1174 + $job['job_type'],
1175 + count( $chunk ),
1176 + count( $remaining )
1177 + )
1178 + );
919 1179
920 - if ( empty( $website ) ) {
921 - $job['errors'][] = array(
922 - 'site_id' => $site_id,
923 - 'code' => 'site_not_found',
924 - 'message' => __( 'Site not found.', 'mainwp' ),
925 - );
926 - ++$job['processed'];
927 - continue;
928 - }
1180 + // Process each site in the chunk.
1181 + foreach ( $chunk as $site_id ) {
1182 + try {
1183 + if ( ! $this->owner_can_access_site( $site_id, $allowed_sites ) ) {
1184 + $job['errors'][] = array(
1185 + 'site_id' => $site_id,
1186 + 'code' => 'access_revoked',
1187 + 'message' => __( 'Access to this site was revoked before it was processed.', 'mainwp' ),
1188 + );
1189 + ++$job['processed'];
1190 + continue;
1191 + }
929 1192
930 - $success = false;
931 - $error = '';
1193 + $website = MainWP_DB::instance()->get_website_by_id( $site_id );
932 1194
933 - // Execute operation based on job type.
934 - switch ( $job['job_type'] ) {
935 - case 'reconnect':
936 - try {
937 - $result = MainWP_Manage_Sites_View::m_reconnect_site( $website );
938 - $success = (bool) $result;
939 - if ( ! $success ) {
940 - $error = __( 'Reconnect returned false.', 'mainwp' );
1195 + if ( empty( $website ) ) {
1196 + $job['errors'][] = array(
1197 + 'site_id' => $site_id,
1198 + 'code' => 'site_not_found',
1199 + 'message' => __( 'Site not found.', 'mainwp' ),
1200 + );
1201 + ++$job['processed'];
1202 + continue;
1203 + }
1204 +
1205 + $success = false;
1206 + $error = '';
1207 +
1208 + // Execute operation based on job type.
1209 + switch ( $job['job_type'] ) {
1210 + case 'reconnect':
1211 + try {
1212 + $result = MainWP_Manage_Sites_View::m_reconnect_site( $website );
1213 + $success = (bool) $result;
1214 + if ( ! $success ) {
1215 + $error = __( 'Reconnect returned false.', 'mainwp' );
1216 + }
1217 + } catch ( \Exception $e ) {
1218 + $error = $e->getMessage();
941 1219 }
942 - } catch ( \Exception $e ) {
943 - $error = $e->getMessage();
944 - }
945 - break;
1220 + break;
946 1221
947 - case 'disconnect':
948 - $result = MainWP_DB::instance()->update_website_sync_values(
949 - $website->id,
950 - array( 'sync_errors' => __( 'Manually disconnected', 'mainwp' ) )
951 - );
952 - // wpdb->update() returns int|false: rows affected or false on error.
953 - $success = ( false !== $result );
954 - if ( ! $success ) {
955 - $error = __( 'Database update failed during disconnect.', 'mainwp' );
956 - $this->log_debug(
957 - sprintf( 'Disconnect failed for site %d: database update returned false', $website->id )
1222 + case 'disconnect':
1223 + $result = MainWP_DB::instance()->update_website_sync_values(
1224 + $website->id,
1225 + array( 'sync_errors' => __( 'Manually disconnected', 'mainwp' ) )
958 1226 );
959 - }
960 - break;
1227 + // wpdb->update() returns int|false: rows affected or false on error.
1228 + $success = ( false !== $result );
1229 + if ( ! $success ) {
1230 + $error = __( 'Database update failed during disconnect.', 'mainwp' );
1231 + $this->log_debug(
1232 + sprintf( 'Disconnect failed for site %d: database update returned false', $website->id )
1233 + );
1234 + }
1235 + break;
961 1236
962 - case 'check':
963 - $result = MainWP_Monitoring_Handler::handle_check_website( $website->id );
964 - if ( is_wp_error( $result ) ) {
965 - $error = $result->get_error_message();
966 - } else {
967 - $success = true;
968 - }
969 - break;
1237 + case 'check':
1238 + $result = MainWP_Monitoring_Handler::handle_check_website( $website->id );
1239 + if ( is_wp_error( $result ) ) {
1240 + $error = $result->get_error_message();
1241 + } else {
1242 + $success = true;
1243 + }
1244 + break;
970 1245
971 - case 'suspend':
972 - $result = MainWP_DB::instance()->update_website_values(
973 - $website->id,
974 - array( 'suspended' => 1 )
975 - );
976 - // wpdb->update() returns int|false: rows affected or false on error.
977 - $success = ( false !== $result );
978 - if ( $success ) {
979 - /**
980 - * Fires when a site is suspended.
981 - *
982 - * @param object $website Site object.
983 - * @param int $status Suspension status (1 = suspended).
984 - */
985 - do_action( 'mainwp_site_suspended', $website, 1 );
986 - } else {
987 - $error = __( 'Database update failed during suspend.', 'mainwp' );
988 - $this->log_debug(
989 - sprintf( 'Suspend failed for site %d: database update returned false', $website->id )
1246 + case 'suspend':
1247 + $result = MainWP_DB::instance()->update_website_values(
1248 + $website->id,
1249 + array( 'suspended' => 1 )
990 1250 );
991 - }
992 - break;
1251 + // wpdb->update() returns int|false: rows affected or false on error.
1252 + $success = ( false !== $result );
1253 + if ( $success ) {
1254 + /**
1255 + * Fires when a site is suspended.
1256 + *
1257 + * @param object $website Site object.
1258 + * @param int $status Suspension status (1 = suspended).
1259 + */
1260 + do_action( 'mainwp_site_suspended', $website, 1 );
1261 + } else {
1262 + $error = __( 'Database update failed during suspend.', 'mainwp' );
1263 + $this->log_debug(
1264 + sprintf( 'Suspend failed for site %d: database update returned false', $website->id )
1265 + );
1266 + }
1267 + break;
993 1268
994 - default:
995 - $error = __( 'Unknown operation type.', 'mainwp' );
996 - }
1269 + default:
1270 + $error = __( 'Unknown operation type.', 'mainwp' );
1271 + }
997 1272
998 - if ( $success ) {
999 - $job['successful'][] = $site_id;
1000 - } else {
1273 + if ( $success ) {
1274 + $job['successful'][] = $site_id;
1275 + } else {
1276 + $job['errors'][] = array(
1277 + 'site_id' => $site_id,
1278 + 'code' => $job['job_type'] . '_failed',
1279 + 'message' => $error,
1280 + );
1281 + }
1282 + } catch ( \Exception $e ) {
1001 1283 $job['errors'][] = array(
1002 1284 'site_id' => $site_id,
1003 - 'code' => $job['job_type'] . '_failed',
1004 - 'message' => $error,
1285 + 'code' => 'exception',
1286 + 'message' => $e->getMessage(),
1005 1287 );
1006 1288 }
1007 - } catch ( \Exception $e ) {
1008 - $job['errors'][] = array(
1009 - 'site_id' => $site_id,
1010 - 'code' => 'exception',
1011 - 'message' => $e->getMessage(),
1012 - );
1289 +
1290 + ++$job['processed'];
1291 + $job['progress'] = $job['total'] > 0
1292 + ? round( ( $job['processed'] / $job['total'] ) * 100 )
1293 + : 0;
1013 1294 }
1014 1295
1015 - ++$job['processed'];
1016 - $job['progress'] = $job['total'] > 0
1017 - ? round( ( $job['processed'] / $job['total'] ) * 100 )
1018 - : 0;
1019 - }
1296 + // Save progress.
1297 + set_transient( 'mainwp_batch_job_' . $job_id, $job, DAY_IN_SECONDS );
1020 1298
1021 - // Save progress.
1022 - set_transient( 'mainwp_batch_job_' . $job_id, $job, DAY_IN_SECONDS );
1299 + // Recalculate remaining after processing.
1300 + $processed_ids = array_merge(
1301 + $job['successful'],
1302 + array_column( $job['errors'], 'site_id' )
1303 + );
1304 + $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
1023 1305
1024 - // Recalculate remaining after processing.
1025 - $processed_ids = array_merge(
1026 - $job['successful'],
1027 - array_column( $job['errors'], 'site_id' )
1028 - );
1029 - $remaining = array_values( array_diff( $job['sites'], $processed_ids ) );
1030 -
1031 - // Reschedule or complete.
1032 - if ( count( $remaining ) > 0 ) {
1033 - // Schedule next chunk (30 second delay).
1034 - if ( ! wp_next_scheduled( 'mainwp_process_batch_job', array( $job_id ) ) ) {
1035 - wp_schedule_single_event( time() + 30, 'mainwp_process_batch_job', array( $job_id ) );
1306 + // Reschedule or complete.
1307 + if ( count( $remaining ) > 0 ) {
1308 + // Schedule next chunk (30 second delay).
1309 + if ( ! wp_next_scheduled( 'mainwp_process_batch_job', array( $job_id ) ) ) {
1310 + wp_schedule_single_event( time() + 30, 'mainwp_process_batch_job', array( $job_id ) );
1311 + }
1312 + $this->log_debug(
1313 + sprintf( 'Batch job %s rescheduled: %d sites remaining', $job_id, count( $remaining ) )
1314 + );
1315 + } else {
1316 + // Job complete.
1317 + $job['status'] = 'completed';
1318 + $job['completed'] = time();
1319 + $job['progress'] = 100;
1320 + set_transient( 'mainwp_batch_job_' . $job_id, $job, DAY_IN_SECONDS );
1321 + $this->log_debug(
1322 + sprintf(
1323 + 'Batch job %s (%s) completed: %d successful, %d errors',
1324 + $job_id,
1325 + $job['job_type'],
1326 + count( $job['successful'] ),
1327 + count( $job['errors'] )
1328 + )
1329 + );
1036 1330 }
1037 - $this->log_debug(
1038 - sprintf( 'Batch job %s rescheduled: %d sites remaining', $job_id, count( $remaining ) )
1039 - );
1040 - } else {
1041 - // Job complete.
1042 - $job['status'] = 'completed';
1043 - $job['completed'] = time();
1044 - $job['progress'] = 100;
1045 - set_transient( 'mainwp_batch_job_' . $job_id, $job, DAY_IN_SECONDS );
1046 - $this->log_debug(
1047 - sprintf(
1048 - 'Batch job %s (%s) completed: %d successful, %d errors',
1049 - $job_id,
1050 - $job['job_type'],
1051 - count( $job['successful'] ),
1052 - count( $job['errors'] )
1053 - )
1054 - );
1331 + } finally {
1332 + wp_set_current_user( $previous_user_id );
1055 1333 }
1056 1334 }
1057 1335 }