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-updates.php

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

4,017 lines 167.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Updates 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_Updates
16 *
17 * Registers and implements update-related abilities for the MainWP Dashboard.
18 *
19 * This class provides 13 abilities:
20 * - mainwp/list-updates-v1: List available updates across sites
21 * - mainwp/run-updates-v1: Execute updates (core, plugins, themes, translations)
22 * - mainwp/list-ignored-updates-v1: List ignored updates
23 * - mainwp/set-ignored-updates-v1: Manage ignored updates
24 * - mainwp/get-site-updates-v1: Get available updates for a single site
25 * - mainwp/update-site-core-v1: Update WordPress core for a single site
26 * - mainwp/update-site-plugins-v1: Update plugins for a single site
27 * - mainwp/update-site-themes-v1: Update themes for a single site
28 * - mainwp/update-site-translations-v1: Update translations for a single site
29 * - mainwp/ignore-site-core-v1: Manage core update ignore status for a single site
30 * - mainwp/ignore-site-plugins-v1: Manage plugin ignore status for a single site
31 * - mainwp/ignore-site-themes-v1: Manage theme ignore status for a single site
32 * - mainwp/update-all-v1: Execute all available updates across all or selected sites
33 *
34 * ## Input Handling for GET Requests
35 *
36 * Read-only abilities (readonly: true) use GET requests. WordPress REST API does NOT
37 * auto-parse JSON from query strings, so:
38 *
39 * - Omit `?input` parameter entirely to use schema defaults (recommended)
40 * - Use `?input=` (empty) which also triggers defaults
41 * - DO NOT use `?input=%7B%7D` (URL-encoded JSON) - it arrives as a string and fails validation
42 *
43 * Our input schemas use `'type' => array('object', 'null')` with defaults, so callers
44 * can simply call the endpoint without any input parameter.
45 *
46 * @see .mwpdev/docs/abilities-api-docs/known-issues.md for detailed explanation
47 */
48 class MainWP_Abilities_Updates { //phpcs:ignore -- NOSONAR - multi methods.
49
50 /**
51 * Batch operation threshold.
52 *
53 * Operations exceeding this number of sites will be queued for background processing.
54 *
55 * @var int
56 */
57 const BATCH_THRESHOLD = 200;
58
59 /**
60 * Register all update abilities.
61 *
62 * @return void
63 */
64 public static function register(): void {
65 if ( ! function_exists( 'wp_register_ability' ) ) {
66 return;
67 }
68
69 self::register_list_updates();
70 self::register_run_updates();
71 self::register_list_ignored_updates();
72 self::register_set_ignored_updates();
73 self::register_get_site_updates();
74 self::register_update_site_core();
75 self::register_update_site_plugins();
76 self::register_update_site_themes();
77 self::register_update_site_translations();
78 self::register_ignore_site_core();
79 self::register_ignore_site_plugins();
80 self::register_ignore_site_themes();
81 self::register_update_all();
82 }
83
84 // =========================================================================
85 // Ability Registration Methods
86 // =========================================================================
87
88 /**
89 * Register mainwp/list-updates-v1 ability.
90 *
91 * @return void
92 */
93 private static function register_list_updates(): void {
94 wp_register_ability(
95 'mainwp/list-updates-v1',
96 array(
97 'label' => __( 'List Available Updates', 'mainwp' ),
98 'description' => __( 'List available updates across MainWP child sites with filtering and pagination.', 'mainwp' ),
99 'category' => 'mainwp-updates',
100 'input_schema' => self::get_list_updates_input_schema(),
101 'output_schema' => self::get_list_updates_output_schema(),
102 'execute_callback' => array( self::class, 'execute_list_updates' ),
103 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_rest_api_permission' ),
104 'meta' => array(
105 'show_in_rest' => true,
106 'annotations' => array(
107 'instructions' => '',
108 'readonly' => true,
109 'destructive' => false,
110 'idempotent' => true,
111 ),
112 ),
113 )
114 );
115 }
116
117 /**
118 * Register mainwp/run-updates-v1 ability.
119 *
120 * @return void
121 */
122 private static function register_run_updates(): void {
123 wp_register_ability(
124 'mainwp/run-updates-v1',
125 array(
126 'label' => __( 'Run Updates', 'mainwp' ),
127 'description' => __( 'Execute updates on one or more MainWP child sites. Operations with >200 sites are automatically queued for background processing.', 'mainwp' ),
128 'category' => 'mainwp-updates',
129 'input_schema' => self::get_run_updates_input_schema(),
130 'output_schema' => self::get_run_updates_output_schema(),
131 'execute_callback' => array( self::class, 'execute_run_updates' ),
132 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
133 'meta' => array(
134 'show_in_rest' => true,
135 'annotations' => array(
136 'instructions' => 'Applies updates to child sites. Operations with >200 sites are automatically queued and return a job_id for status polling. Individual update failures do not fail the entire operation.',
137 'readonly' => false,
138 'destructive' => false,
139 'idempotent' => false,
140 ),
141 ),
142 )
143 );
144 }
145
146 /**
147 * Register mainwp/list-ignored-updates-v1 ability.
148 *
149 * @return void
150 */
151 private static function register_list_ignored_updates(): void {
152 wp_register_ability(
153 'mainwp/list-ignored-updates-v1',
154 array(
155 'label' => __( 'List Ignored Updates', 'mainwp' ),
156 'description' => __( 'List updates that have been marked as ignored.', 'mainwp' ),
157 'category' => 'mainwp-updates',
158 'input_schema' => self::get_list_ignored_updates_input_schema(),
159 'output_schema' => self::get_list_ignored_updates_output_schema(),
160 'execute_callback' => array( self::class, 'execute_list_ignored_updates' ),
161 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_rest_api_permission' ),
162 'meta' => array(
163 'show_in_rest' => true,
164 'annotations' => array(
165 'instructions' => '',
166 'readonly' => true,
167 'destructive' => false,
168 'idempotent' => true,
169 ),
170 ),
171 )
172 );
173 }
174
175 /**
176 * Register mainwp/set-ignored-updates-v1 ability.
177 *
178 * @return void
179 */
180 private static function register_set_ignored_updates(): void {
181 wp_register_ability(
182 'mainwp/set-ignored-updates-v1',
183 array(
184 'label' => __( 'Set Ignored Updates', 'mainwp' ),
185 'description' => __( 'Add or remove items from the ignored updates list for a site.', 'mainwp' ),
186 'category' => 'mainwp-updates',
187 'input_schema' => self::get_set_ignored_updates_input_schema(),
188 'output_schema' => self::get_set_ignored_updates_output_schema(),
189 'execute_callback' => array( self::class, 'execute_set_ignored_updates' ),
190 'permission_callback' => MainWP_Abilities_Util::make_site_permission_callback( 'site_id_or_domain' ),
191 'meta' => array(
192 'show_in_rest' => true,
193 'annotations' => array(
194 'instructions' => '',
195 'readonly' => false,
196 'destructive' => false,
197 'idempotent' => true,
198 ),
199 ),
200 )
201 );
202 }
203
204 /**
205 * Register mainwp/get-site-updates-v1 ability.
206 *
207 * @return void
208 */
209 private static function register_get_site_updates(): void {
210 wp_register_ability(
211 'mainwp/get-site-updates-v1',
212 array(
213 'label' => __( 'Get Site Updates', 'mainwp' ),
214 'description' => __( 'Get available updates for a single site with optional filtering by update type.', 'mainwp' ),
215 'category' => 'mainwp-updates',
216 'input_schema' => self::get_get_site_updates_input_schema(),
217 'output_schema' => self::get_get_site_updates_output_schema(),
218 'execute_callback' => array( self::class, 'execute_get_site_updates' ),
219 'permission_callback' => MainWP_Abilities_Util::make_site_permission_callback( 'site_id_or_domain' ),
220 'meta' => array(
221 'show_in_rest' => true,
222 'annotations' => array(
223 'instructions' => '',
224 'readonly' => true,
225 'destructive' => false,
226 'idempotent' => true,
227 ),
228 ),
229 )
230 );
231 }
232
233 /**
234 * Register mainwp/update-site-core-v1 ability.
235 *
236 * @return void
237 */
238 private static function register_update_site_core(): void {
239 wp_register_ability(
240 'mainwp/update-site-core-v1',
241 array(
242 'label' => __( 'Update Site Core', 'mainwp' ),
243 'description' => __( 'Update WordPress core for a single site.', 'mainwp' ),
244 'category' => 'mainwp-updates',
245 'input_schema' => self::get_update_site_core_input_schema(),
246 'output_schema' => self::get_update_site_core_output_schema(),
247 'execute_callback' => array( self::class, 'execute_update_site_core' ),
248 'permission_callback' => MainWP_Abilities_Util::make_site_permission_callback( 'site_id_or_domain' ),
249 'meta' => array(
250 'show_in_rest' => true,
251 'annotations' => array(
252 'instructions' => 'Updates WordPress core on a single site. For batch operations across multiple sites, use run_updates_v1 with types=[\'core\'].',
253 'readonly' => false,
254 'destructive' => false,
255 'idempotent' => false,
256 ),
257 ),
258 )
259 );
260 }
261
262 /**
263 * Register mainwp/update-site-plugins-v1 ability.
264 *
265 * @return void
266 */
267 private static function register_update_site_plugins(): void {
268 wp_register_ability(
269 'mainwp/update-site-plugins-v1',
270 array(
271 'label' => __( 'Update Site Plugins', 'mainwp' ),
272 'description' => __( 'Update plugins for a single site. Optionally specify slugs to update only specific plugins.', 'mainwp' ),
273 'category' => 'mainwp-updates',
274 'input_schema' => self::get_update_site_plugins_input_schema(),
275 'output_schema' => self::get_update_site_plugins_output_schema(),
276 'execute_callback' => array( self::class, 'execute_update_site_plugins' ),
277 'permission_callback' => MainWP_Abilities_Util::make_site_permission_callback( 'site_id_or_domain' ),
278 'meta' => array(
279 'show_in_rest' => true,
280 'annotations' => array(
281 'instructions' => 'Updates plugins on a single site. For batch operations across multiple sites, use run_updates_v1 with types=[\'plugins\'].',
282 'readonly' => false,
283 'destructive' => false,
284 'idempotent' => false,
285 ),
286 ),
287 )
288 );
289 }
290
291 /**
292 * Register mainwp/update-site-themes-v1 ability.
293 *
294 * @return void
295 */
296 private static function register_update_site_themes(): void {
297 wp_register_ability(
298 'mainwp/update-site-themes-v1',
299 array(
300 'label' => __( 'Update Site Themes', 'mainwp' ),
301 'description' => __( 'Update themes for a single site. Optionally specify slugs to update only specific themes.', 'mainwp' ),
302 'category' => 'mainwp-updates',
303 'input_schema' => self::get_update_site_themes_input_schema(),
304 'output_schema' => self::get_update_site_themes_output_schema(),
305 'execute_callback' => array( self::class, 'execute_update_site_themes' ),
306 'permission_callback' => MainWP_Abilities_Util::make_site_permission_callback( 'site_id_or_domain' ),
307 'meta' => array(
308 'show_in_rest' => true,
309 'annotations' => array(
310 'instructions' => 'Updates themes on a single site. For batch operations across multiple sites, use run_updates_v1 with types=[\'themes\'].',
311 'readonly' => false,
312 'destructive' => false,
313 'idempotent' => false,
314 ),
315 ),
316 )
317 );
318 }
319
320 /**
321 * Register mainwp/update-site-translations-v1 ability.
322 *
323 * @return void
324 */
325 private static function register_update_site_translations(): void {
326 wp_register_ability(
327 'mainwp/update-site-translations-v1',
328 array(
329 'label' => __( 'Update Site Translations', 'mainwp' ),
330 'description' => __( 'Update translations for a single site. Optionally specify slugs to update only specific translations.', 'mainwp' ),
331 'category' => 'mainwp-updates',
332 'input_schema' => self::get_update_site_translations_input_schema(),
333 'output_schema' => self::get_update_site_translations_output_schema(),
334 'execute_callback' => array( self::class, 'execute_update_site_translations' ),
335 'permission_callback' => MainWP_Abilities_Util::make_site_permission_callback( 'site_id_or_domain' ),
336 'meta' => array(
337 'show_in_rest' => true,
338 'annotations' => array(
339 'instructions' => 'Updates translations on a single site. For batch operations across multiple sites, use run_updates_v1 with types=[\'translations\'].',
340 'readonly' => false,
341 'destructive' => false,
342 'idempotent' => false,
343 ),
344 ),
345 )
346 );
347 }
348
349 /**
350 * Register mainwp/ignore-site-core-v1 ability.
351 *
352 * @return void
353 */
354 private static function register_ignore_site_core(): void {
355 wp_register_ability(
356 'mainwp/ignore-site-core-v1',
357 array(
358 'label' => __( 'Ignore Site Core', 'mainwp' ),
359 'description' => __( 'Add or remove WordPress core from the ignored updates list for a single site.', 'mainwp' ),
360 'category' => 'mainwp-updates',
361 'input_schema' => self::get_ignore_site_core_input_schema(),
362 'output_schema' => self::get_ignore_site_output_schema(),
363 'execute_callback' => array( self::class, 'execute_ignore_site_core' ),
364 'permission_callback' => MainWP_Abilities_Util::make_site_permission_callback( 'site_id_or_domain' ),
365 'meta' => array(
366 'show_in_rest' => true,
367 'annotations' => array(
368 'instructions' => 'Manages core update ignore status. Use action=add to ignore core updates, action=remove to unignore.',
369 'readonly' => false,
370 'destructive' => false,
371 'idempotent' => true,
372 ),
373 ),
374 )
375 );
376 }
377
378 /**
379 * Register mainwp/ignore-site-plugins-v1 ability.
380 *
381 * @return void
382 */
383 private static function register_ignore_site_plugins(): void {
384 wp_register_ability(
385 'mainwp/ignore-site-plugins-v1',
386 array(
387 'label' => __( 'Ignore Site Plugins', 'mainwp' ),
388 'description' => __( 'Add or remove plugins from the ignored updates list for a single site.', 'mainwp' ),
389 'category' => 'mainwp-updates',
390 'input_schema' => self::get_ignore_site_plugins_input_schema(),
391 'output_schema' => self::get_ignore_site_output_schema(),
392 'execute_callback' => array( self::class, 'execute_ignore_site_plugins' ),
393 'permission_callback' => MainWP_Abilities_Util::make_site_permission_callback( 'site_id_or_domain' ),
394 'meta' => array(
395 'show_in_rest' => true,
396 'annotations' => array(
397 'instructions' => 'Manages plugin ignore status. Provide slugs array of plugins to ignore/unignore.',
398 'readonly' => false,
399 'destructive' => false,
400 'idempotent' => true,
401 ),
402 ),
403 )
404 );
405 }
406
407 /**
408 * Register mainwp/ignore-site-themes-v1 ability.
409 *
410 * @return void
411 */
412 private static function register_ignore_site_themes(): void {
413 wp_register_ability(
414 'mainwp/ignore-site-themes-v1',
415 array(
416 'label' => __( 'Ignore Site Themes', 'mainwp' ),
417 'description' => __( 'Add or remove themes from the ignored updates list for a single site.', 'mainwp' ),
418 'category' => 'mainwp-updates',
419 'input_schema' => self::get_ignore_site_themes_input_schema(),
420 'output_schema' => self::get_ignore_site_output_schema(),
421 'execute_callback' => array( self::class, 'execute_ignore_site_themes' ),
422 'permission_callback' => MainWP_Abilities_Util::make_site_permission_callback( 'site_id_or_domain' ),
423 'meta' => array(
424 'show_in_rest' => true,
425 'annotations' => array(
426 'instructions' => 'Manages theme ignore status. Provide slugs array of themes to ignore/unignore.',
427 'readonly' => false,
428 'destructive' => false,
429 'idempotent' => true,
430 ),
431 ),
432 )
433 );
434 }
435
436 /**
437 * Register mainwp/update-all-v1 ability.
438 *
439 * @return void
440 */
441 private static function register_update_all(): void {
442 wp_register_ability(
443 'mainwp/update-all-v1',
444 array(
445 'label' => __( 'Update All', 'mainwp' ),
446 'description' => __( 'Execute ALL available updates (core, plugins, themes, translations) across all sites or specified sites. WARNING: This is a broad operation. For targeted updates, use run_updates_v1 with specific types[] and site_ids_or_domains[]. Operations with >200 sites are queued.', 'mainwp' ),
447 'category' => 'mainwp-updates',
448 'input_schema' => self::get_update_all_input_schema(),
449 'output_schema' => self::get_update_all_output_schema(),
450 'execute_callback' => array( self::class, 'execute_update_all' ),
451 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
452 'meta' => array(
453 'show_in_rest' => true,
454 'annotations' => array(
455 'instructions' => 'Applies all available updates (core, plugins, themes, translations) to specified sites or all sites. Operations with >200 sites are automatically queued and return a job_id for status polling.',
456 'readonly' => false,
457 'destructive' => false,
458 'idempotent' => false,
459 ),
460 ),
461 )
462 );
463 }
464
465 // =========================================================================
466 // Input Schema Definitions
467 // =========================================================================
468
469 /**
470 * Get input schema for list-updates-v1.
471 *
472 * Note: Uses 'type' => array('object', 'null') to allow callers to omit the input
473 * parameter entirely on GET requests. All properties have defaults, so no input
474 * is required. See class docblock for GET request input handling details.
475 *
476 * @return array
477 */
478 public static function get_list_updates_input_schema(): array {
479 return array(
480 'type' => array( 'object', 'null' ),
481 'properties' => array(
482 'site_ids_or_domains' => array(
483 'type' => 'array',
484 'description' => __( 'Filter to specific sites. Empty array means all sites.', 'mainwp' ),
485 'items' => array(
486 'type' => array( 'integer', 'string' ),
487 ),
488 'default' => array(),
489 ),
490 'types' => array(
491 'type' => 'array',
492 'description' => __( 'Update types to include. Empty array means all types.', 'mainwp' ),
493 'items' => array(
494 'type' => 'string',
495 'enum' => array( 'core', 'plugins', 'themes', 'translations' ),
496 ),
497 'default' => array(),
498 ),
499 'page' => array(
500 'type' => 'integer',
501 'description' => __( 'Page number (1-based).', 'mainwp' ),
502 'default' => 1,
503 'minimum' => 1,
504 ),
505 'per_page' => array(
506 'type' => 'integer',
507 'description' => __( 'Items per page.', 'mainwp' ),
508 'default' => 50,
509 'minimum' => 1,
510 'maximum' => 200,
511 ),
512 ),
513 'additionalProperties' => false,
514 );
515 }
516
517 /**
518 * Get input schema for run-updates-v1.
519 *
520 * @return array
521 */
522 public static function get_run_updates_input_schema(): array {
523 return array(
524 'type' => array( 'object', 'null' ),
525 'properties' => array(
526 'site_ids_or_domains' => array(
527 'type' => 'array',
528 'description' => __( 'Sites to update. Empty array means all sites with available updates.', 'mainwp' ),
529 'items' => array(
530 'type' => array( 'integer', 'string' ),
531 ),
532 'default' => array(),
533 ),
534 'types' => array(
535 'type' => 'array',
536 'description' => __( 'Update types to apply. Empty array means all types.', 'mainwp' ),
537 'items' => array(
538 'type' => 'string',
539 'enum' => array( 'core', 'plugins', 'themes', 'translations' ),
540 ),
541 'default' => array(),
542 ),
543 'specific_items' => array(
544 'type' => 'array',
545 'description' => __( 'Specific items to update (by slug). If empty, updates all items of selected types.', 'mainwp' ),
546 'items' => array( 'type' => 'string' ),
547 'default' => array(),
548 ),
549 ),
550 'additionalProperties' => false,
551 );
552 }
553
554 /**
555 * Get input schema for list-ignored-updates-v1.
556 *
557 * Note: Uses 'type' => array('object', 'null') to allow callers to omit the input
558 * parameter entirely on GET requests. All properties have defaults, so no input
559 * is required. See class docblock for GET request input handling details.
560 *
561 * @return array
562 */
563 public static function get_list_ignored_updates_input_schema(): array {
564 return array(
565 'type' => array( 'object', 'null' ),
566 'properties' => array(
567 'site_ids_or_domains' => array(
568 'type' => 'array',
569 'description' => __( 'Filter to specific sites. Empty array means all sites.', 'mainwp' ),
570 'items' => array(
571 'type' => array( 'integer', 'string' ),
572 ),
573 'default' => array(),
574 ),
575 'types' => array(
576 'type' => 'array',
577 'description' => __( 'Filter by update type. Empty array means all types.', 'mainwp' ),
578 'items' => array(
579 'type' => 'string',
580 'enum' => array( 'core', 'plugins', 'themes' ),
581 ),
582 'default' => array(),
583 ),
584 ),
585 'additionalProperties' => false,
586 );
587 }
588
589 /**
590 * Get input schema for set-ignored-updates-v1.
591 *
592 * @return array
593 */
594 public static function get_set_ignored_updates_input_schema(): array {
595 return array(
596 'type' => 'object',
597 'properties' => array(
598 'action' => array(
599 'type' => 'string',
600 'enum' => array( 'ignore', 'unignore' ),
601 'description' => __( 'Action to perform.', 'mainwp' ),
602 ),
603 'site_id_or_domain' => array(
604 'type' => array( 'integer', 'string' ),
605 'description' => __( 'Site ID or domain/URL.', 'mainwp' ),
606 ),
607 'type' => array(
608 'type' => 'string',
609 'enum' => array( 'core', 'plugin', 'theme' ),
610 'description' => __( 'Type of update to ignore/unignore.', 'mainwp' ),
611 ),
612 'slug' => array(
613 'type' => 'string',
614 'description' => __( 'Slug of the item to ignore/unignore. Use "wordpress" for core updates.', 'mainwp' ),
615 ),
616 ),
617 'required' => array( 'action', 'site_id_or_domain', 'type', 'slug' ),
618 'additionalProperties' => false,
619 );
620 }
621
622 // =========================================================================
623 // Output Schema Definitions
624 // =========================================================================
625
626 /**
627 * Get output schema for list-updates-v1.
628 *
629 * @return array
630 */
631 public static function get_list_updates_output_schema(): array {
632 return array(
633 'type' => 'object',
634 'properties' => array(
635 'updates' => array(
636 'type' => 'array',
637 'description' => __( 'List of available updates.', 'mainwp' ),
638 'items' => array(
639 'type' => 'object',
640 'properties' => array(
641 'site_id' => array( 'type' => 'integer' ),
642 'site_url' => array(
643 'type' => 'string',
644 'format' => 'uri',
645 ),
646 'site_name' => array( 'type' => 'string' ),
647 'type' => array(
648 'type' => 'string',
649 'enum' => array( 'core', 'plugin', 'theme', 'translation' ),
650 ),
651 'slug' => array( 'type' => 'string' ),
652 'name' => array( 'type' => 'string' ),
653 'current_version' => array( 'type' => 'string' ),
654 'new_version' => array( 'type' => 'string' ),
655 ),
656 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'name', 'current_version', 'new_version' ),
657 ),
658 ),
659 'summary' => array(
660 'type' => 'object',
661 'description' => __( 'Summary counts by type.', 'mainwp' ),
662 'properties' => array(
663 'core' => array( 'type' => 'integer' ),
664 'plugins' => array( 'type' => 'integer' ),
665 'themes' => array( 'type' => 'integer' ),
666 'translations' => array( 'type' => 'integer' ),
667 'total' => array( 'type' => 'integer' ),
668 ),
669 'required' => array( 'core', 'plugins', 'themes', 'translations', 'total' ),
670 ),
671 'page' => array( 'type' => 'integer' ),
672 'per_page' => array( 'type' => 'integer' ),
673 'total' => array(
674 'type' => 'integer',
675 'description' => __( 'Total number of updates matching filters.', 'mainwp' ),
676 ),
677 'errors' => array(
678 'type' => 'array',
679 'description' => __( 'Site resolution or access errors. Check errors.length > 0 to detect partial failures.', 'mainwp' ),
680 'items' => array(
681 'type' => 'object',
682 'properties' => array(
683 'site_id' => array( 'type' => 'integer' ),
684 'site_url' => array(
685 'type' => 'string',
686 'format' => 'uri',
687 ),
688 'site_name' => array( 'type' => 'string' ),
689 'type' => array(
690 'type' => 'string',
691 'description' => __( 'Value is "site" for site-level errors (resolution, access).', 'mainwp' ),
692 ),
693 'slug' => array( 'type' => 'string' ),
694 'code' => array(
695 'type' => 'string',
696 'description' => __( 'Error code for programmatic handling (e.g., mainwp_site_not_found, mainwp_access_denied).', 'mainwp' ),
697 ),
698 'message' => array( 'type' => 'string' ),
699 ),
700 'required' => array( 'site_id', 'site_url', 'type', 'slug', 'code', 'message' ),
701 ),
702 ),
703 ),
704 'required' => array( 'updates', 'summary', 'page', 'per_page', 'total' ),
705 );
706 }
707
708 /**
709 * Get output schema for run-updates-v1.
710 *
711 * Response has two modes:
712 * - Immediate mode (≤200 sites): Returns updated, errors, summary
713 * - Queued mode (>200 sites): Returns queued, job_id, status_url, updates_queued, errors
714 *
715 * The errors array is present in BOTH modes - for immediate mode it contains update failures,
716 * for queued mode it contains pre-queue failures (site resolution/access errors).
717 *
718 * @return array
719 */
720 public static function get_run_updates_output_schema(): array {
721 // Common error item schema used in both response modes.
722 $error_item_schema = array(
723 'type' => 'object',
724 'properties' => array(
725 'site_id' => array( 'type' => 'integer' ),
726 'site_url' => array(
727 'type' => 'string',
728 'format' => 'uri',
729 ),
730 'site_name' => array( 'type' => 'string' ),
731 'type' => array(
732 'type' => 'string',
733 'description' => __( 'Update type (plugin, theme, core, translation) or "site" for site-level errors.', 'mainwp' ),
734 ),
735 'slug' => array( 'type' => 'string' ),
736 'code' => array(
737 'type' => 'string',
738 'description' => __( 'Error code for programmatic handling (e.g., mainwp_site_offline, mainwp_access_denied).', 'mainwp' ),
739 ),
740 'message' => array( 'type' => 'string' ),
741 ),
742 'required' => array( 'site_id', 'site_url', 'type', 'slug', 'code', 'message' ),
743 );
744
745 // Common errors array schema.
746 $errors_schema = array(
747 'type' => 'array',
748 'description' => __( 'Failed updates or site-level errors. Present in both immediate and queued modes.', 'mainwp' ),
749 'items' => $error_item_schema,
750 );
751
752 return array(
753 'type' => 'object',
754 'description' => __( 'Response varies by operation mode. Immediate mode (≤200 sites) returns updated/errors/summary. Queued mode (>200 sites) returns queued/job_id/status_url/updates_queued/errors.', 'mainwp' ),
755 'oneOf' => array(
756 // Immediate execution response (≤ threshold sites).
757 array(
758 'type' => 'object',
759 'description' => __( 'Immediate execution response for operations with ≤200 sites.', 'mainwp' ),
760 'properties' => array(
761 'updated' => array(
762 'type' => 'array',
763 'description' => __( 'Successfully updated items.', 'mainwp' ),
764 'items' => array(
765 'type' => 'object',
766 'properties' => array(
767 'site_id' => array( 'type' => 'integer' ),
768 'site_url' => array(
769 'type' => 'string',
770 'format' => 'uri',
771 ),
772 'site_name' => array( 'type' => 'string' ),
773 'type' => array( 'type' => 'string' ),
774 'slug' => array( 'type' => 'string' ),
775 'name' => array( 'type' => 'string' ),
776 'old_version' => array( 'type' => 'string' ),
777 'new_version' => array( 'type' => 'string' ),
778 ),
779 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'name', 'old_version', 'new_version' ),
780 ),
781 ),
782 'errors' => $errors_schema,
783 'summary' => array(
784 'type' => 'object',
785 'properties' => array(
786 'total_updated' => array( 'type' => 'integer' ),
787 'total_errors' => array( 'type' => 'integer' ),
788 'sites_updated' => array( 'type' => 'integer' ),
789 ),
790 'required' => array( 'total_updated', 'total_errors', 'sites_updated' ),
791 ),
792 ),
793 'required' => array( 'updated', 'errors', 'summary' ),
794 ),
795 // Queued execution response (> threshold sites).
796 array(
797 'type' => 'object',
798 'description' => __( 'Queued execution response for operations with >200 sites.', 'mainwp' ),
799 'properties' => array(
800 'queued' => array(
801 'type' => 'boolean',
802 'description' => __( 'Always true for queued responses.', 'mainwp' ),
803 'const' => true,
804 ),
805 'job_id' => array(
806 'type' => 'string',
807 'description' => __( 'Background job ID for status polling.', 'mainwp' ),
808 ),
809 'status_url' => array(
810 'type' => 'string',
811 'format' => 'uri',
812 'description' => __( 'URL to poll for job status.', 'mainwp' ),
813 ),
814 'updates_queued' => array(
815 'type' => 'integer',
816 'description' => __( 'Number of update operations queued.', 'mainwp' ),
817 ),
818 'errors' => $errors_schema,
819 ),
820 'required' => array( 'queued', 'job_id', 'status_url', 'updates_queued', 'errors' ),
821 ),
822 ),
823 );
824 }
825
826 /**
827 * Get output schema for list-ignored-updates-v1.
828 *
829 * Note: This endpoint provides only a flat total count without per-type breakdown.
830 * Use the 'type' field on each ignored item for client-side grouping if needed.
831 *
832 * @return array
833 */
834 public static function get_list_ignored_updates_output_schema(): array {
835 return array(
836 'type' => 'object',
837 'properties' => array(
838 'ignored' => array(
839 'type' => 'array',
840 'description' => __( 'List of ignored updates. Each item includes its type (core, plugin, theme) for client-side filtering/grouping.', 'mainwp' ),
841 'items' => array(
842 'type' => 'object',
843 'properties' => array(
844 'site_id' => array( 'type' => 'integer' ),
845 'site_url' => array(
846 'type' => 'string',
847 'format' => 'uri',
848 ),
849 'site_name' => array( 'type' => 'string' ),
850 'type' => array(
851 'type' => 'string',
852 'enum' => array( 'core', 'plugin', 'theme' ),
853 'description' => __( 'Update type. Use for filtering or grouping results.', 'mainwp' ),
854 ),
855 'slug' => array( 'type' => 'string' ),
856 'name' => array( 'type' => 'string' ),
857 'ignored_version' => array( 'type' => 'string' ),
858 ),
859 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'name', 'ignored_version' ),
860 ),
861 ),
862 'total' => array(
863 'type' => 'integer',
864 'description' => __( 'Total number of ignored updates (flat count, no per-type breakdown).', 'mainwp' ),
865 ),
866 'errors' => array(
867 'type' => 'array',
868 'description' => __( 'Site resolution or access errors. Check errors.length > 0 to detect partial failures.', 'mainwp' ),
869 'items' => array(
870 'type' => 'object',
871 'properties' => array(
872 'site_id' => array( 'type' => 'integer' ),
873 'site_url' => array(
874 'type' => 'string',
875 'format' => 'uri',
876 ),
877 'site_name' => array( 'type' => 'string' ),
878 'type' => array(
879 'type' => 'string',
880 'description' => __( 'Value is "site" for site-level errors (resolution, access).', 'mainwp' ),
881 ),
882 'slug' => array( 'type' => 'string' ),
883 'code' => array(
884 'type' => 'string',
885 'description' => __( 'Error code for programmatic handling (e.g., mainwp_site_not_found, mainwp_access_denied).', 'mainwp' ),
886 ),
887 'message' => array( 'type' => 'string' ),
888 ),
889 'required' => array( 'site_id', 'site_url', 'type', 'slug', 'code', 'message' ),
890 ),
891 ),
892 ),
893 'required' => array( 'ignored', 'total' ),
894 );
895 }
896
897 /**
898 * Get output schema for set-ignored-updates-v1.
899 *
900 * @return array
901 */
902 public static function get_set_ignored_updates_output_schema(): array {
903 return array(
904 'type' => 'object',
905 'properties' => array(
906 'success' => array(
907 'type' => 'boolean',
908 'description' => __( 'Whether the action was successful.', 'mainwp' ),
909 ),
910 'action' => array(
911 'type' => 'string',
912 'enum' => array( 'ignore', 'unignore' ),
913 'description' => __( 'Action that was performed.', 'mainwp' ),
914 ),
915 'site_id' => array(
916 'type' => 'integer',
917 'description' => __( 'Site ID.', 'mainwp' ),
918 ),
919 'type' => array(
920 'type' => 'string',
921 'description' => __( 'Update type.', 'mainwp' ),
922 ),
923 'slug' => array(
924 'type' => 'string',
925 'description' => __( 'Item slug.', 'mainwp' ),
926 ),
927 ),
928 'required' => array( 'success', 'action', 'site_id', 'type', 'slug' ),
929 );
930 }
931
932 /**
933 * Get input schema for get-site-updates-v1.
934 *
935 * @return array
936 */
937 public static function get_get_site_updates_input_schema(): array {
938 return array(
939 'type' => 'object',
940 'properties' => array(
941 'site_id_or_domain' => array(
942 'type' => array( 'integer', 'string' ),
943 'description' => __( 'Site ID (numeric) or domain.', 'mainwp' ),
944 ),
945 'types' => array(
946 'type' => 'array',
947 'items' => array(
948 'type' => 'string',
949 'enum' => array( 'core', 'plugins', 'themes', 'translations' ),
950 ),
951 'default' => array( 'core', 'plugins', 'themes', 'translations' ),
952 'description' => __( 'Update types to retrieve.', 'mainwp' ),
953 ),
954 ),
955 'required' => array( 'site_id_or_domain' ),
956 'additionalProperties' => false,
957 );
958 }
959
960 /**
961 * Get output schema for get-site-updates-v1.
962 *
963 * @return array
964 */
965 public static function get_get_site_updates_output_schema(): array {
966 return array(
967 'type' => 'object',
968 'properties' => array(
969 'site_id' => array(
970 'type' => 'integer',
971 'description' => __( 'Site ID.', 'mainwp' ),
972 ),
973 'site_url' => array(
974 'type' => 'string',
975 'format' => 'uri',
976 'description' => __( 'Site URL.', 'mainwp' ),
977 ),
978 'site_name' => array(
979 'type' => 'string',
980 'description' => __( 'Site name.', 'mainwp' ),
981 ),
982 'updates' => array(
983 'type' => 'array',
984 'description' => __( 'Available updates.', 'mainwp' ),
985 'items' => array(
986 'type' => 'object',
987 'properties' => array(
988 'type' => array(
989 'type' => 'string',
990 'enum' => array( 'core', 'plugin', 'theme', 'translation' ),
991 ),
992 'slug' => array( 'type' => 'string' ),
993 'name' => array( 'type' => 'string' ),
994 'current_version' => array( 'type' => 'string' ),
995 'new_version' => array( 'type' => 'string' ),
996 ),
997 ),
998 ),
999 'rollback_items' => array(
1000 'type' => 'object',
1001 'description' => __( 'Available rollback versions.', 'mainwp' ),
1002 'properties' => array(
1003 'plugins' => array(
1004 'type' => 'array',
1005 'items' => array( 'type' => 'object' ),
1006 ),
1007 'themes' => array(
1008 'type' => 'array',
1009 'items' => array( 'type' => 'object' ),
1010 ),
1011 ),
1012 ),
1013 'summary' => array(
1014 'type' => 'object',
1015 'description' => __( 'Update count summary.', 'mainwp' ),
1016 'properties' => array(
1017 'core' => array( 'type' => 'integer' ),
1018 'plugins' => array( 'type' => 'integer' ),
1019 'themes' => array( 'type' => 'integer' ),
1020 'translations' => array( 'type' => 'integer' ),
1021 'total' => array( 'type' => 'integer' ),
1022 ),
1023 ),
1024 ),
1025 'required' => array( 'site_id', 'site_url', 'site_name', 'updates', 'rollback_items', 'summary' ),
1026 );
1027 }
1028
1029 /**
1030 * Get input schema for update-site-core-v1.
1031 *
1032 * @return array
1033 */
1034 public static function get_update_site_core_input_schema(): array {
1035 return array(
1036 'type' => 'object',
1037 'properties' => array(
1038 'site_id_or_domain' => array(
1039 'type' => array( 'integer', 'string' ),
1040 'description' => __( 'Site ID (numeric) or domain.', 'mainwp' ),
1041 ),
1042 ),
1043 'required' => array( 'site_id_or_domain' ),
1044 'additionalProperties' => false,
1045 );
1046 }
1047
1048 /**
1049 * Get output schema for update-site-core-v1.
1050 *
1051 * @return array
1052 */
1053 public static function get_update_site_core_output_schema(): array {
1054 return array(
1055 'type' => 'object',
1056 'properties' => array(
1057 'updated' => array(
1058 'type' => 'object',
1059 'properties' => array(
1060 'site_id' => array( 'type' => 'integer' ),
1061 'site_url' => array(
1062 'type' => 'string',
1063 'format' => 'uri',
1064 ),
1065 'site_name' => array( 'type' => 'string' ),
1066 'type' => array(
1067 'type' => 'string',
1068 'const' => 'core',
1069 ),
1070 'slug' => array( 'type' => 'string' ),
1071 'name' => array( 'type' => 'string' ),
1072 'old_version' => array( 'type' => 'string' ),
1073 'new_version' => array( 'type' => 'string' ),
1074 ),
1075 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'name', 'old_version', 'new_version' ),
1076 ),
1077 ),
1078 'required' => array( 'updated' ),
1079 );
1080 }
1081
1082 /**
1083 * Get input schema for update-site-plugins-v1.
1084 *
1085 * @return array
1086 */
1087 public static function get_update_site_plugins_input_schema(): array {
1088 return array(
1089 'type' => 'object',
1090 'properties' => array(
1091 'site_id_or_domain' => array(
1092 'type' => array( 'integer', 'string' ),
1093 'description' => __( 'Site ID (numeric) or domain.', 'mainwp' ),
1094 ),
1095 'slugs' => array(
1096 'type' => 'array',
1097 'items' => array( 'type' => 'string' ),
1098 'default' => array(),
1099 'description' => __( 'Specific plugin slugs to update. Empty = all available.', 'mainwp' ),
1100 ),
1101 ),
1102 'required' => array( 'site_id_or_domain' ),
1103 'additionalProperties' => false,
1104 );
1105 }
1106
1107 /**
1108 * Get output schema for update-site-plugins-v1.
1109 *
1110 * @return array
1111 */
1112 public static function get_update_site_plugins_output_schema(): array {
1113 $update_item_schema = array(
1114 'type' => 'object',
1115 'properties' => array(
1116 'site_id' => array( 'type' => 'integer' ),
1117 'site_url' => array(
1118 'type' => 'string',
1119 'format' => 'uri',
1120 ),
1121 'site_name' => array( 'type' => 'string' ),
1122 'type' => array(
1123 'type' => 'string',
1124 'const' => 'plugin',
1125 ),
1126 'slug' => array( 'type' => 'string' ),
1127 'name' => array( 'type' => 'string' ),
1128 'old_version' => array( 'type' => 'string' ),
1129 'new_version' => array( 'type' => 'string' ),
1130 ),
1131 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'name', 'old_version', 'new_version' ),
1132 );
1133
1134 $error_item_schema = array(
1135 'type' => 'object',
1136 'properties' => array(
1137 'site_id' => array( 'type' => 'integer' ),
1138 'site_url' => array(
1139 'type' => 'string',
1140 'format' => 'uri',
1141 ),
1142 'site_name' => array( 'type' => 'string' ),
1143 'type' => array(
1144 'type' => 'string',
1145 'const' => 'plugin',
1146 ),
1147 'slug' => array( 'type' => 'string' ),
1148 'code' => array( 'type' => 'string' ),
1149 'message' => array( 'type' => 'string' ),
1150 ),
1151 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'code', 'message' ),
1152 );
1153
1154 return array(
1155 'type' => 'object',
1156 'properties' => array(
1157 'updated' => array(
1158 'type' => 'array',
1159 'items' => $update_item_schema,
1160 ),
1161 'errors' => array(
1162 'type' => 'array',
1163 'items' => $error_item_schema,
1164 ),
1165 'summary' => array(
1166 'type' => 'object',
1167 'properties' => array(
1168 'total_updated' => array( 'type' => 'integer' ),
1169 'total_errors' => array( 'type' => 'integer' ),
1170 ),
1171 'required' => array( 'total_updated', 'total_errors' ),
1172 ),
1173 ),
1174 'required' => array( 'updated', 'errors', 'summary' ),
1175 );
1176 }
1177
1178 /**
1179 * Get input schema for update-site-themes-v1.
1180 *
1181 * @return array
1182 */
1183 public static function get_update_site_themes_input_schema(): array {
1184 return array(
1185 'type' => 'object',
1186 'properties' => array(
1187 'site_id_or_domain' => array(
1188 'type' => array( 'integer', 'string' ),
1189 'description' => __( 'Site ID (numeric) or domain.', 'mainwp' ),
1190 ),
1191 'slugs' => array(
1192 'type' => 'array',
1193 'items' => array( 'type' => 'string' ),
1194 'default' => array(),
1195 'description' => __( 'Specific theme slugs to update. Empty = all available.', 'mainwp' ),
1196 ),
1197 ),
1198 'required' => array( 'site_id_or_domain' ),
1199 'additionalProperties' => false,
1200 );
1201 }
1202
1203 /**
1204 * Get output schema for update-site-themes-v1.
1205 *
1206 * @return array
1207 */
1208 public static function get_update_site_themes_output_schema(): array {
1209 $update_item_schema = array(
1210 'type' => 'object',
1211 'properties' => array(
1212 'site_id' => array( 'type' => 'integer' ),
1213 'site_url' => array(
1214 'type' => 'string',
1215 'format' => 'uri',
1216 ),
1217 'site_name' => array( 'type' => 'string' ),
1218 'type' => array(
1219 'type' => 'string',
1220 'const' => 'theme',
1221 ),
1222 'slug' => array( 'type' => 'string' ),
1223 'name' => array( 'type' => 'string' ),
1224 'old_version' => array( 'type' => 'string' ),
1225 'new_version' => array( 'type' => 'string' ),
1226 ),
1227 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'name', 'old_version', 'new_version' ),
1228 );
1229
1230 $error_item_schema = array(
1231 'type' => 'object',
1232 'properties' => array(
1233 'site_id' => array( 'type' => 'integer' ),
1234 'site_url' => array(
1235 'type' => 'string',
1236 'format' => 'uri',
1237 ),
1238 'site_name' => array( 'type' => 'string' ),
1239 'type' => array(
1240 'type' => 'string',
1241 'const' => 'theme',
1242 ),
1243 'slug' => array( 'type' => 'string' ),
1244 'code' => array( 'type' => 'string' ),
1245 'message' => array( 'type' => 'string' ),
1246 ),
1247 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'code', 'message' ),
1248 );
1249
1250 return array(
1251 'type' => 'object',
1252 'properties' => array(
1253 'updated' => array(
1254 'type' => 'array',
1255 'items' => $update_item_schema,
1256 ),
1257 'errors' => array(
1258 'type' => 'array',
1259 'items' => $error_item_schema,
1260 ),
1261 'summary' => array(
1262 'type' => 'object',
1263 'properties' => array(
1264 'total_updated' => array( 'type' => 'integer' ),
1265 'total_errors' => array( 'type' => 'integer' ),
1266 ),
1267 'required' => array( 'total_updated', 'total_errors' ),
1268 ),
1269 ),
1270 'required' => array( 'updated', 'errors', 'summary' ),
1271 );
1272 }
1273
1274 /**
1275 * Get input schema for update-site-translations-v1.
1276 *
1277 * @return array
1278 */
1279 public static function get_update_site_translations_input_schema(): array {
1280 return array(
1281 'type' => 'object',
1282 'properties' => array(
1283 'site_id_or_domain' => array(
1284 'type' => array( 'integer', 'string' ),
1285 'description' => __( 'Site ID (numeric) or domain.', 'mainwp' ),
1286 ),
1287 'slugs' => array(
1288 'type' => 'array',
1289 'items' => array( 'type' => 'string' ),
1290 'default' => array(),
1291 'description' => __( 'Specific translation slugs to update. Empty = all available.', 'mainwp' ),
1292 ),
1293 ),
1294 'required' => array( 'site_id_or_domain' ),
1295 'additionalProperties' => false,
1296 );
1297 }
1298
1299 /**
1300 * Get output schema for update-site-translations-v1.
1301 *
1302 * @return array
1303 */
1304 public static function get_update_site_translations_output_schema(): array {
1305 $update_item_schema = array(
1306 'type' => 'object',
1307 'properties' => array(
1308 'site_id' => array( 'type' => 'integer' ),
1309 'site_url' => array(
1310 'type' => 'string',
1311 'format' => 'uri',
1312 ),
1313 'site_name' => array( 'type' => 'string' ),
1314 'type' => array(
1315 'type' => 'string',
1316 'const' => 'translation',
1317 ),
1318 'slug' => array( 'type' => 'string' ),
1319 'name' => array( 'type' => 'string' ),
1320 'old_version' => array( 'type' => 'string' ),
1321 'new_version' => array( 'type' => 'string' ),
1322 ),
1323 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'name', 'old_version', 'new_version' ),
1324 );
1325
1326 $error_item_schema = array(
1327 'type' => 'object',
1328 'properties' => array(
1329 'site_id' => array( 'type' => 'integer' ),
1330 'site_url' => array(
1331 'type' => 'string',
1332 'format' => 'uri',
1333 ),
1334 'site_name' => array( 'type' => 'string' ),
1335 'type' => array(
1336 'type' => 'string',
1337 'const' => 'translation',
1338 ),
1339 'slug' => array( 'type' => 'string' ),
1340 'code' => array( 'type' => 'string' ),
1341 'message' => array( 'type' => 'string' ),
1342 ),
1343 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'code', 'message' ),
1344 );
1345
1346 return array(
1347 'type' => 'object',
1348 'properties' => array(
1349 'updated' => array(
1350 'type' => 'array',
1351 'items' => $update_item_schema,
1352 ),
1353 'errors' => array(
1354 'type' => 'array',
1355 'items' => $error_item_schema,
1356 ),
1357 'summary' => array(
1358 'type' => 'object',
1359 'properties' => array(
1360 'total_updated' => array( 'type' => 'integer' ),
1361 'total_errors' => array( 'type' => 'integer' ),
1362 ),
1363 'required' => array( 'total_updated', 'total_errors' ),
1364 ),
1365 ),
1366 'required' => array( 'updated', 'errors', 'summary' ),
1367 );
1368 }
1369
1370 /**
1371 * Get input schema for ignore-site-core-v1.
1372 *
1373 * @return array
1374 */
1375 public static function get_ignore_site_core_input_schema(): array {
1376 return array(
1377 'type' => 'object',
1378 'properties' => array(
1379 'site_id_or_domain' => array(
1380 'type' => array( 'integer', 'string' ),
1381 'description' => __( 'Site ID (numeric) or domain.', 'mainwp' ),
1382 ),
1383 'action' => array(
1384 'type' => 'string',
1385 'enum' => array( 'add', 'remove' ),
1386 'default' => 'add',
1387 'description' => __( 'Add to or remove from ignored list.', 'mainwp' ),
1388 ),
1389 ),
1390 'required' => array( 'site_id_or_domain' ),
1391 'additionalProperties' => false,
1392 );
1393 }
1394
1395 /**
1396 * Get input schema for ignore-site-plugins-v1.
1397 *
1398 * @return array
1399 */
1400 public static function get_ignore_site_plugins_input_schema(): array {
1401 return array(
1402 'type' => 'object',
1403 'properties' => array(
1404 'site_id_or_domain' => array(
1405 'type' => array( 'integer', 'string' ),
1406 'description' => __( 'Site ID (numeric) or domain.', 'mainwp' ),
1407 ),
1408 'slugs' => array(
1409 'type' => 'array',
1410 'items' => array( 'type' => 'string' ),
1411 'description' => __( 'Plugin slugs to ignore.', 'mainwp' ),
1412 ),
1413 'action' => array(
1414 'type' => 'string',
1415 'enum' => array( 'add', 'remove' ),
1416 'default' => 'add',
1417 'description' => __( 'Add to or remove from ignored list.', 'mainwp' ),
1418 ),
1419 ),
1420 'required' => array( 'site_id_or_domain', 'slugs' ),
1421 'additionalProperties' => false,
1422 );
1423 }
1424
1425 /**
1426 * Get input schema for ignore-site-themes-v1.
1427 *
1428 * @return array
1429 */
1430 public static function get_ignore_site_themes_input_schema(): array {
1431 return array(
1432 'type' => 'object',
1433 'properties' => array(
1434 'site_id_or_domain' => array(
1435 'type' => array( 'integer', 'string' ),
1436 'description' => __( 'Site ID (numeric) or domain.', 'mainwp' ),
1437 ),
1438 'slugs' => array(
1439 'type' => 'array',
1440 'items' => array( 'type' => 'string' ),
1441 'description' => __( 'Theme slugs to ignore.', 'mainwp' ),
1442 ),
1443 'action' => array(
1444 'type' => 'string',
1445 'enum' => array( 'add', 'remove' ),
1446 'default' => 'add',
1447 'description' => __( 'Add to or remove from ignored list.', 'mainwp' ),
1448 ),
1449 ),
1450 'required' => array( 'site_id_or_domain', 'slugs' ),
1451 'additionalProperties' => false,
1452 );
1453 }
1454
1455 /**
1456 * Get output schema for ignore-site-* abilities (shared).
1457 *
1458 * @return array
1459 */
1460 public static function get_ignore_site_output_schema(): array {
1461 return array(
1462 'type' => 'object',
1463 'properties' => array(
1464 'success' => array(
1465 'type' => 'boolean',
1466 'description' => __( 'Whether the action was successful.', 'mainwp' ),
1467 ),
1468 'message' => array(
1469 'type' => 'string',
1470 'description' => __( 'Success or error message.', 'mainwp' ),
1471 ),
1472 'ignored_count' => array(
1473 'type' => 'integer',
1474 'description' => __( 'Number of items affected.', 'mainwp' ),
1475 ),
1476 ),
1477 'required' => array( 'success', 'message', 'ignored_count' ),
1478 );
1479 }
1480
1481 /**
1482 * Get input schema for update-all-v1.
1483 *
1484 * @return array
1485 */
1486 public static function get_update_all_input_schema(): array {
1487 return array(
1488 'type' => array( 'object', 'null' ),
1489 'properties' => array(
1490 'site_ids_or_domains' => array(
1491 'type' => 'array',
1492 'items' => array(
1493 'type' => array( 'integer', 'string' ),
1494 ),
1495 'default' => array(),
1496 'description' => __( 'Sites to update. Empty array means all sites.', 'mainwp' ),
1497 ),
1498 'types' => array(
1499 'type' => 'array',
1500 'items' => array(
1501 'type' => 'string',
1502 'enum' => array( 'core', 'plugins', 'themes', 'translations' ),
1503 ),
1504 'default' => array( 'core', 'plugins', 'themes', 'translations' ),
1505 'description' => __( 'Update types to execute.', 'mainwp' ),
1506 ),
1507 ),
1508 'additionalProperties' => false,
1509 );
1510 }
1511
1512 /**
1513 * Get output schema for update-all-v1.
1514 *
1515 * @return array
1516 */
1517 public static function get_update_all_output_schema(): array {
1518 $error_item_schema = array(
1519 'type' => 'object',
1520 'properties' => array(
1521 'site_id' => array( 'type' => 'integer' ),
1522 'site_url' => array(
1523 'type' => 'string',
1524 'format' => 'uri',
1525 ),
1526 'site_name' => array( 'type' => 'string' ),
1527 'type' => array(
1528 'type' => 'string',
1529 'description' => __( 'Update type or "site" for site-level errors.', 'mainwp' ),
1530 ),
1531 'slug' => array( 'type' => 'string' ),
1532 'code' => array(
1533 'type' => 'string',
1534 'description' => __( 'Error code.', 'mainwp' ),
1535 ),
1536 'message' => array( 'type' => 'string' ),
1537 ),
1538 'required' => array( 'site_id', 'site_url', 'type', 'slug', 'code', 'message' ),
1539 );
1540
1541 $errors_schema = array(
1542 'type' => 'array',
1543 'description' => __( 'Failed updates or site-level errors.', 'mainwp' ),
1544 'items' => $error_item_schema,
1545 );
1546
1547 return array(
1548 'type' => 'object',
1549 'description' => __( 'Response varies by operation mode. Immediate mode (≤200 sites) returns updated/errors/summary. Queued mode (>200 sites) returns queued/job_id/status_url/updates_queued/errors.', 'mainwp' ),
1550 'oneOf' => array(
1551 array(
1552 'type' => 'object',
1553 'description' => __( 'Immediate execution response for operations with ≤200 sites.', 'mainwp' ),
1554 'properties' => array(
1555 'updated' => array(
1556 'type' => 'array',
1557 'description' => __( 'Successfully updated items.', 'mainwp' ),
1558 'items' => array(
1559 'type' => 'object',
1560 'properties' => array(
1561 'site_id' => array( 'type' => 'integer' ),
1562 'site_url' => array(
1563 'type' => 'string',
1564 'format' => 'uri',
1565 ),
1566 'site_name' => array( 'type' => 'string' ),
1567 'type' => array( 'type' => 'string' ),
1568 'slug' => array( 'type' => 'string' ),
1569 'name' => array( 'type' => 'string' ),
1570 'old_version' => array( 'type' => 'string' ),
1571 'new_version' => array( 'type' => 'string' ),
1572 ),
1573 'required' => array( 'site_id', 'site_url', 'site_name', 'type', 'slug', 'name', 'old_version', 'new_version' ),
1574 ),
1575 ),
1576 'errors' => $errors_schema,
1577 'summary' => array(
1578 'type' => 'object',
1579 'properties' => array(
1580 'total_updated' => array( 'type' => 'integer' ),
1581 'total_errors' => array( 'type' => 'integer' ),
1582 'sites_updated' => array( 'type' => 'integer' ),
1583 ),
1584 'required' => array( 'total_updated', 'total_errors', 'sites_updated' ),
1585 ),
1586 ),
1587 'required' => array( 'updated', 'errors', 'summary' ),
1588 ),
1589 array(
1590 'type' => 'object',
1591 'description' => __( 'Queued execution response for operations with >200 sites.', 'mainwp' ),
1592 'properties' => array(
1593 'queued' => array(
1594 'type' => 'boolean',
1595 'description' => __( 'Always true for queued responses.', 'mainwp' ),
1596 'const' => true,
1597 ),
1598 'job_id' => array(
1599 'type' => 'string',
1600 'description' => __( 'Background job ID for status polling.', 'mainwp' ),
1601 ),
1602 'status_url' => array(
1603 'type' => 'string',
1604 'format' => 'uri',
1605 'description' => __( 'URL to poll for job status.', 'mainwp' ),
1606 ),
1607 'updates_queued' => array(
1608 'type' => 'integer',
1609 'description' => __( 'Number of update operations queued.', 'mainwp' ),
1610 ),
1611 'errors' => $errors_schema,
1612 ),
1613 'required' => array( 'queued', 'job_id', 'status_url', 'updates_queued', 'errors' ),
1614 ),
1615 ),
1616 );
1617 }
1618
1619 // =========================================================================
1620 // Execute Callbacks
1621 // =========================================================================
1622
1623 /**
1624 * Execute callback for mainwp/list-updates-v1.
1625 *
1626 * @param array|null $input Validated input from Abilities API.
1627 * @return array|\WP_Error
1628 */
1629 public static function execute_list_updates( $input ) { // phpcs:ignore -- NOSONAR - complex method.
1630 $input = is_array( $input ) ? $input : array();
1631 $site_ids_or_domains = $input['site_ids_or_domains'] ?? array();
1632 $types = $input['types'] ?? array();
1633 $page = (int) ( $input['page'] ?? 1 );
1634 $per_page = (int) ( $input['per_page'] ?? 50 );
1635
1636 // If empty types, include all.
1637 if ( empty( $types ) ) {
1638 $types = array( 'core', 'plugins', 'themes', 'translations' );
1639 }
1640
1641 $sites = array();
1642 $errors = array();
1643
1644 // When specific site identifiers are provided, enforce per-site ACLs.
1645 // When empty (all sites), rely on get_websites_for_current_user() which respects user access.
1646 if ( ! empty( $site_ids_or_domains ) ) {
1647 // Check per-site ACLs and filter to allowed sites.
1648 $access_check = MainWP_Abilities_Util::check_batch_site_access( $site_ids_or_domains, $input );
1649
1650 $sites = $access_check['allowed'];
1651
1652 // Normalize ACL-denied errors to match the documented error item schema.
1653 foreach ( $access_check['denied'] as $denied_entry ) {
1654 $errors[] = array(
1655 'site_id' => 0,
1656 'site_url' => is_scalar( $denied_entry['identifier'] ) ? (string) $denied_entry['identifier'] : '',
1657 'site_name' => '',
1658 'type' => 'site',
1659 'slug' => '',
1660 'code' => $denied_entry['code'],
1661 'message' => $denied_entry['message'],
1662 );
1663 }
1664 } else {
1665 // Get all sites for current user (respects user access by default).
1666 // Must request update fields explicitly - default only returns id, url, name, client_id.
1667 $websites = MainWP_DB::instance()->get_websites_for_current_user(
1668 array(
1669 'selectgroups' => false,
1670 'fields' => array(
1671 'plugin_upgrades',
1672 'theme_upgrades',
1673 'translation_upgrades',
1674 'ignored_plugins',
1675 'ignored_themes',
1676 'is_ignoreCoreUpdates',
1677 'is_ignorePluginUpdates',
1678 'is_ignoreThemeUpdates',
1679 ),
1680 )
1681 );
1682
1683 if ( is_wp_error( $websites ) ) {
1684 return $websites;
1685 }
1686
1687 $sites = $websites ? $websites : array();
1688 }
1689
1690 // Get global ignored lists.
1691 // Defensively handle case where user_extension row doesn't exist.
1692 $user_extension = MainWP_DB_Common::instance()->get_user_extension_by_user_id();
1693 $global_ignored_plugins = array();
1694 $global_ignored_themes = array();
1695 $global_ignored_core = array();
1696
1697 if ( is_object( $user_extension ) ) {
1698 $global_ignored_plugins = ! empty( $user_extension->ignored_plugins ) ? json_decode( $user_extension->ignored_plugins, true ) : array();
1699 $global_ignored_themes = ! empty( $user_extension->ignored_themes ) ? json_decode( $user_extension->ignored_themes, true ) : array();
1700 $global_ignored_core = ! empty( $user_extension->ignored_wp_upgrades ) ? json_decode( $user_extension->ignored_wp_upgrades, true ) : array();
1701 }
1702
1703 if ( ! is_array( $global_ignored_plugins ) ) {
1704 $global_ignored_plugins = array();
1705 }
1706 if ( ! is_array( $global_ignored_themes ) ) {
1707 $global_ignored_themes = array();
1708 }
1709 if ( ! is_array( $global_ignored_core ) ) {
1710 $global_ignored_core = array();
1711 }
1712
1713 // Collect all updates.
1714 $all_updates = array();
1715 $summary = array(
1716 'core' => 0,
1717 'plugins' => 0,
1718 'themes' => 0,
1719 'translations' => 0,
1720 'total' => 0,
1721 );
1722
1723 foreach ( $sites as $site ) {
1724 $site_updates = self::get_site_updates(
1725 $site,
1726 $types,
1727 $global_ignored_plugins,
1728 $global_ignored_themes,
1729 $global_ignored_core
1730 );
1731
1732 foreach ( $site_updates as $update ) {
1733 $all_updates[] = $update;
1734
1735 // Update summary counts.
1736 $type_key = $update['type'];
1737 if ( 'plugin' === $type_key ) {
1738 $type_key = 'plugins';
1739 } elseif ( 'theme' === $type_key ) {
1740 $type_key = 'themes';
1741 } elseif ( 'translation' === $type_key ) {
1742 $type_key = 'translations';
1743 }
1744
1745 if ( isset( $summary[ $type_key ] ) ) {
1746 ++$summary[ $type_key ];
1747 }
1748 ++$summary['total'];
1749 }
1750 }
1751
1752 // Apply pagination.
1753 $total = count( $all_updates );
1754 $offset = ( $page - 1 ) * $per_page;
1755 $paged_items = array_slice( $all_updates, $offset, $per_page );
1756
1757 return array(
1758 'updates' => $paged_items,
1759 'summary' => $summary,
1760 'page' => $page,
1761 'per_page' => $per_page,
1762 'total' => $total,
1763 'errors' => $errors,
1764 );
1765 }
1766
1767 /**
1768 * Execute callback for mainwp/run-updates-v1.
1769 *
1770 * @param array $input Validated input from Abilities API.
1771 * @return array|\WP_Error
1772 */
1773 public static function execute_run_updates( ?array $input ) { // phpcs:ignore -- NOSONAR - complex method.
1774 // Handle null input (when called without input parameter).
1775 $input = $input ?? array();
1776
1777 $site_ids_or_domains = $input['site_ids_or_domains'] ?? array();
1778 $types = $input['types'] ?? array();
1779 $specific_items = $input['specific_items'] ?? array();
1780
1781 // If empty types, include all.
1782 if ( empty( $types ) ) {
1783 $types = array( 'core', 'plugins', 'themes', 'translations' );
1784 }
1785
1786 // If empty, get all sites for current user.
1787 if ( empty( $site_ids_or_domains ) ) {
1788 $all_sites = MainWP_DB::instance()->get_websites_for_current_user( array( 'selectgroups' => false ) );
1789
1790 if ( is_wp_error( $all_sites ) ) {
1791 return $all_sites;
1792 }
1793
1794 $site_ids_or_domains = array_map(
1795 function ( $s ) {
1796 return (int) $s->id;
1797 },
1798 $all_sites ? $all_sites : array()
1799 );
1800 }
1801
1802 // Check per-site ACLs and filter to allowed sites.
1803 $access_check = MainWP_Abilities_Util::check_batch_site_access( $site_ids_or_domains, $input );
1804
1805 $threshold = apply_filters( 'mainwp_abilities_batch_threshold', self::BATCH_THRESHOLD );
1806
1807 if ( count( $access_check['allowed'] ) > $threshold ) {
1808 $job_id = MainWP_Abilities_Util::queue_batch_updates(
1809 $access_check['allowed'],
1810 array(
1811 'types' => $types,
1812 'specific_items' => $specific_items,
1813 )
1814 );
1815
1816 if ( is_wp_error( $job_id ) ) {
1817 return $job_id;
1818 }
1819
1820 // Normalize ACL-denied errors to match the documented error item schema.
1821 // This ensures clients can handle partial failures uniformly whether immediate or queued.
1822 $errors = array();
1823 foreach ( $access_check['denied'] as $denied_entry ) {
1824 $errors[] = array(
1825 'site_id' => 0, // Not resolvable.
1826 'site_url' => is_scalar( $denied_entry['identifier'] ) ? (string) $denied_entry['identifier'] : '',
1827 'type' => 'site',
1828 'slug' => '',
1829 'code' => $denied_entry['code'],
1830 'message' => $denied_entry['message'],
1831 );
1832 }
1833
1834 return array(
1835 'queued' => true,
1836 'job_id' => $job_id,
1837 'status_url' => rest_url( "mainwp/v2/jobs/{$job_id}" ),
1838 'updates_queued' => count( $access_check['allowed'] ),
1839 'errors' => $errors,
1840 );
1841 }
1842
1843 // Immediate execution for ≤ BATCH_THRESHOLD sites.
1844 $updated = array();
1845 $sites_updated = array();
1846
1847 // Normalize ACL-denied errors to match the documented error item schema.
1848 // The check_batch_site_access() returns errors with 'identifier', 'code', 'message', 'status' keys,
1849 // but the output schema expects 'site_id', 'site_url', 'type', 'slug', 'code', 'message'.
1850 $errors = array();
1851 foreach ( $access_check['denied'] as $denied_entry ) {
1852 $errors[] = array(
1853 'site_id' => 0, // Not resolvable.
1854 'site_url' => is_scalar( $denied_entry['identifier'] ) ? (string) $denied_entry['identifier'] : '',
1855 'type' => 'site',
1856 'slug' => '',
1857 'code' => $denied_entry['code'],
1858 'message' => $denied_entry['message'],
1859 );
1860 }
1861
1862 // Get global ignored lists.
1863 // Defensively handle case where user_extension row doesn't exist.
1864 $user_extension = MainWP_DB_Common::instance()->get_user_extension_by_user_id();
1865 $global_ignored_plugins = array();
1866 $global_ignored_themes = array();
1867 $global_ignored_core = array();
1868
1869 if ( is_object( $user_extension ) ) {
1870 $global_ignored_plugins = ! empty( $user_extension->ignored_plugins ) ? json_decode( $user_extension->ignored_plugins, true ) : array();
1871 $global_ignored_themes = ! empty( $user_extension->ignored_themes ) ? json_decode( $user_extension->ignored_themes, true ) : array();
1872 $global_ignored_core = ! empty( $user_extension->ignored_wp_upgrades ) ? json_decode( $user_extension->ignored_wp_upgrades, true ) : array();
1873 }
1874
1875 if ( ! is_array( $global_ignored_plugins ) ) {
1876 $global_ignored_plugins = array();
1877 }
1878 if ( ! is_array( $global_ignored_themes ) ) {
1879 $global_ignored_themes = array();
1880 }
1881 if ( ! is_array( $global_ignored_core ) ) {
1882 $global_ignored_core = array();
1883 }
1884
1885 foreach ( $access_check['allowed'] as $site ) {
1886 // Check if site is known offline before attempting updates.
1887 if ( isset( $site->offline_check_result ) && -1 === (int) $site->offline_check_result ) {
1888 $errors[] = array(
1889 'site_id' => (int) $site->id,
1890 'site_url' => $site->url,
1891 'type' => 'site',
1892 'slug' => '',
1893 'code' => 'mainwp_site_offline',
1894 'message' => __( 'Site is known to be offline.', 'mainwp' ),
1895 );
1896 continue;
1897 }
1898
1899 // Check child version before updates.
1900 $version_check = MainWP_Abilities_Util::check_child_version( $site );
1901 if ( is_wp_error( $version_check ) ) {
1902 $errors[] = array(
1903 'site_id' => (int) $site->id,
1904 'site_url' => $site->url,
1905 'type' => 'site',
1906 'slug' => '',
1907 'code' => $version_check->get_error_code(),
1908 'message' => $version_check->get_error_message(),
1909 );
1910 continue;
1911 }
1912
1913 // Get available updates for this site.
1914 $site_updates = self::get_site_updates(
1915 $site,
1916 $types,
1917 $global_ignored_plugins,
1918 $global_ignored_themes,
1919 $global_ignored_core
1920 );
1921
1922 // Filter by specific_items if provided.
1923 if ( ! empty( $specific_items ) ) {
1924 $site_updates = array_filter(
1925 $site_updates,
1926 function ( $update ) use ( $specific_items ) {
1927 return in_array( $update['slug'], $specific_items, true );
1928 }
1929 );
1930 }
1931
1932 // Group updates by type.
1933 $grouped = array(
1934 'core' => array(),
1935 'plugin' => array(),
1936 'theme' => array(),
1937 'translation' => array(),
1938 );
1939
1940 foreach ( $site_updates as $update ) {
1941 $grouped[ $update['type'] ][] = $update;
1942 }
1943
1944 // Execute updates by type.
1945 $site_had_updates = false;
1946
1947 // Allow filtering of update result for testing/extension purposes.
1948 // Filter can return WP_Error to simulate site-level failure.
1949 $pre_result = apply_filters( 'mainwp_run_update_result', null, (int) $site->id, $types );
1950 if ( is_wp_error( $pre_result ) ) {
1951 $errors[] = array(
1952 'site_id' => (int) $site->id,
1953 'site_url' => $site->url,
1954 'type' => 'site',
1955 'slug' => '',
1956 'code' => $pre_result->get_error_code(),
1957 'message' => $pre_result->get_error_message(),
1958 );
1959 continue;
1960 }
1961
1962 // Core updates.
1963 if ( in_array( 'core', $types, true ) && ! empty( $grouped['core'] ) ) {
1964 $core_result = self::execute_core_update( $site, $grouped['core'][0] );
1965 if ( is_wp_error( $core_result ) ) {
1966 $errors[] = array(
1967 'site_id' => (int) $site->id,
1968 'site_url' => $site->url,
1969 'type' => 'core',
1970 'slug' => 'wordpress',
1971 'code' => $core_result->get_error_code(),
1972 'message' => $core_result->get_error_message(),
1973 );
1974 } else {
1975 $updated[] = $core_result;
1976 $site_had_updates = true;
1977 }
1978 }
1979
1980 // Plugin updates.
1981 if ( in_array( 'plugins', $types, true ) && ! empty( $grouped['plugin'] ) ) {
1982 $plugin_results = self::execute_plugin_updates( $site, $grouped['plugin'] );
1983 foreach ( $plugin_results['updated'] as $result ) {
1984 $updated[] = $result;
1985 $site_had_updates = true;
1986 }
1987 foreach ( $plugin_results['errors'] as $error ) {
1988 $errors[] = $error;
1989 }
1990 }
1991
1992 // Theme updates.
1993 if ( in_array( 'themes', $types, true ) && ! empty( $grouped['theme'] ) ) {
1994 $theme_results = self::execute_theme_updates( $site, $grouped['theme'] );
1995 foreach ( $theme_results['updated'] as $result ) {
1996 $updated[] = $result;
1997 $site_had_updates = true;
1998 }
1999 foreach ( $theme_results['errors'] as $error ) {
2000 $errors[] = $error;
2001 }
2002 }
2003
2004 // Translation updates.
2005 if ( in_array( 'translations', $types, true ) && ! empty( $grouped['translation'] ) ) {
2006 $translation_results = self::execute_translation_updates( $site, $grouped['translation'] );
2007 foreach ( $translation_results['updated'] as $result ) {
2008 $updated[] = $result;
2009 $site_had_updates = true;
2010 }
2011 foreach ( $translation_results['errors'] as $error ) {
2012 $errors[] = $error;
2013 }
2014 }
2015
2016 if ( $site_had_updates ) {
2017 $sites_updated[ (int) $site->id ] = true;
2018 }
2019 }
2020
2021 return array(
2022 'updated' => $updated,
2023 'errors' => $errors,
2024 'summary' => array(
2025 'total_updated' => count( $updated ),
2026 'total_errors' => count( $errors ),
2027 'sites_updated' => count( $sites_updated ),
2028 ),
2029 );
2030 }
2031
2032 /**
2033 * Execute callback for mainwp/list-ignored-updates-v1.
2034 *
2035 * @param array|null $input Validated input from Abilities API.
2036 * @return array|\WP_Error
2037 */
2038 public static function execute_list_ignored_updates( $input ) { // phpcs:ignore -- NOSONAR - complex method.
2039 $input = is_array( $input ) ? $input : array();
2040 $site_ids_or_domains = $input['site_ids_or_domains'] ?? array();
2041 $types = $input['types'] ?? array();
2042
2043 // If empty types, include all.
2044 if ( empty( $types ) ) {
2045 $types = array( 'core', 'plugins', 'themes' );
2046 }
2047
2048 $sites = array();
2049 $errors = array();
2050
2051 // When specific site identifiers are provided, enforce per-site ACLs.
2052 // When empty (all sites), rely on get_websites_for_current_user() which respects user access.
2053 if ( ! empty( $site_ids_or_domains ) ) {
2054 // Check per-site ACLs and filter to allowed sites.
2055 $access_check = MainWP_Abilities_Util::check_batch_site_access( $site_ids_or_domains, $input );
2056
2057 $sites = $access_check['allowed'];
2058
2059 // Normalize ACL-denied errors to match the documented error item schema.
2060 foreach ( $access_check['denied'] as $denied_entry ) {
2061 $errors[] = array(
2062 'site_id' => 0,
2063 'site_url' => is_scalar( $denied_entry['identifier'] ) ? (string) $denied_entry['identifier'] : '',
2064 'site_name' => '',
2065 'type' => 'site',
2066 'slug' => '',
2067 'code' => $denied_entry['code'],
2068 'message' => $denied_entry['message'],
2069 );
2070 }
2071 } else {
2072 // Get all sites for current user (respects user access by default).
2073 // Must request ignored fields explicitly - default only returns id, url, name, client_id.
2074 $websites = MainWP_DB::instance()->get_websites_for_current_user(
2075 array(
2076 'selectgroups' => false,
2077 'fields' => array(
2078 'ignored_plugins',
2079 'ignored_themes',
2080 ),
2081 )
2082 );
2083
2084 if ( is_wp_error( $websites ) ) {
2085 return $websites;
2086 }
2087
2088 $sites = $websites ? $websites : array();
2089 }
2090
2091 // Get global ignored lists.
2092 // Defensively handle case where user_extension row doesn't exist.
2093 $user_extension = MainWP_DB_Common::instance()->get_user_extension_by_user_id();
2094 $global_ignored_plugins = array();
2095 $global_ignored_themes = array();
2096 $global_ignored_core = array();
2097
2098 if ( is_object( $user_extension ) ) {
2099 $global_ignored_plugins = ! empty( $user_extension->ignored_plugins ) ? json_decode( $user_extension->ignored_plugins, true ) : array();
2100 $global_ignored_themes = ! empty( $user_extension->ignored_themes ) ? json_decode( $user_extension->ignored_themes, true ) : array();
2101 $global_ignored_core = ! empty( $user_extension->ignored_wp_upgrades ) ? json_decode( $user_extension->ignored_wp_upgrades, true ) : array();
2102 }
2103
2104 if ( ! is_array( $global_ignored_plugins ) ) {
2105 $global_ignored_plugins = array();
2106 }
2107 if ( ! is_array( $global_ignored_themes ) ) {
2108 $global_ignored_themes = array();
2109 }
2110 if ( ! is_array( $global_ignored_core ) ) {
2111 $global_ignored_core = array();
2112 }
2113
2114 // Collect all ignored updates.
2115 $all_ignored = array();
2116
2117 foreach ( $sites as $site ) {
2118 $site_id = (int) $site->id;
2119 $site_url = $site->url;
2120 $site_name = MainWP_Utility::remove_http_prefix( (string) $site->name, true );
2121
2122 // Get per-site ignored lists.
2123 $site_ignored_plugins = ! empty( $site->ignored_plugins ) ? json_decode( $site->ignored_plugins, true ) : array();
2124 $site_ignored_themes = ! empty( $site->ignored_themes ) ? json_decode( $site->ignored_themes, true ) : array();
2125
2126 // Get core ignored from website option.
2127 $site_ignored_core = MainWP_DB::instance()->get_website_option( $site, 'ignored_wp_upgrades' );
2128 $site_ignored_core = ! empty( $site_ignored_core ) ? json_decode( $site_ignored_core, true ) : array();
2129
2130 if ( ! is_array( $site_ignored_plugins ) ) {
2131 $site_ignored_plugins = array();
2132 }
2133 if ( ! is_array( $site_ignored_themes ) ) {
2134 $site_ignored_themes = array();
2135 }
2136 if ( ! is_array( $site_ignored_core ) ) {
2137 $site_ignored_core = array();
2138 }
2139
2140 // Merge per-site with global (per-site takes precedence for plugins/themes keyed by slug).
2141 $merged_plugins = array_merge( $global_ignored_plugins, $site_ignored_plugins );
2142 $merged_themes = array_merge( $global_ignored_themes, $site_ignored_themes );
2143
2144 // Core ignored: Structure is { 'ignored_versions' => [...] }, not keyed by slug.
2145 // Per-site takes precedence if it has ignored_versions, otherwise use global.
2146 // We check both separately rather than merging, as they share the same key structure.
2147 if ( in_array( 'core', $types, true ) ) {
2148 // Determine the effective ignored_versions for core.
2149 // Per-site takes precedence if it has any ignored versions set.
2150 $effective_core_ignored = array();
2151 if ( ! empty( $site_ignored_core['ignored_versions'] ) && is_array( $site_ignored_core['ignored_versions'] ) ) {
2152 $effective_core_ignored = $site_ignored_core;
2153 } elseif ( ! empty( $global_ignored_core['ignored_versions'] ) && is_array( $global_ignored_core['ignored_versions'] ) ) {
2154 $effective_core_ignored = $global_ignored_core;
2155 }
2156
2157 if ( ! empty( $effective_core_ignored['ignored_versions'] ) ) {
2158 $ignored_version = implode( ', ', $effective_core_ignored['ignored_versions'] );
2159
2160 $all_ignored[] = array(
2161 'site_id' => $site_id,
2162 'site_url' => $site_url,
2163 'site_name' => $site_name,
2164 'type' => 'core',
2165 'slug' => 'wordpress',
2166 'name' => 'WordPress',
2167 'ignored_version' => $ignored_version,
2168 );
2169 }
2170 }
2171
2172 // Plugin ignored.
2173 if ( in_array( 'plugins', $types, true ) ) {
2174 foreach ( $merged_plugins as $slug => $info ) {
2175 $name = is_array( $info ) && isset( $info['Name'] ) ? $info['Name'] : $slug;
2176 $ignored_version = 'all_versions';
2177
2178 if ( is_array( $info ) && isset( $info['ignored_versions'] ) && is_array( $info['ignored_versions'] ) ) {
2179 $ignored_version = implode( ', ', $info['ignored_versions'] );
2180 } elseif ( is_string( $info ) ) {
2181 // Old format: just the name.
2182 $name = $info;
2183 }
2184
2185 $all_ignored[] = array(
2186 'site_id' => $site_id,
2187 'site_url' => $site_url,
2188 'site_name' => $site_name,
2189 'type' => 'plugin',
2190 'slug' => $slug,
2191 'name' => $name,
2192 'ignored_version' => $ignored_version,
2193 );
2194 }
2195 }
2196
2197 // Theme ignored.
2198 if ( in_array( 'themes', $types, true ) ) {
2199 foreach ( $merged_themes as $slug => $info ) {
2200 $name = is_array( $info ) && isset( $info['Name'] ) ? $info['Name'] : $slug;
2201 $ignored_version = 'all_versions';
2202
2203 if ( is_array( $info ) && isset( $info['ignored_versions'] ) && is_array( $info['ignored_versions'] ) ) {
2204 $ignored_version = implode( ', ', $info['ignored_versions'] );
2205 } elseif ( is_string( $info ) ) {
2206 // Old format: just the name.
2207 $name = $info;
2208 }
2209
2210 $all_ignored[] = array(
2211 'site_id' => $site_id,
2212 'site_url' => $site_url,
2213 'site_name' => $site_name,
2214 'type' => 'theme',
2215 'slug' => $slug,
2216 'name' => $name,
2217 'ignored_version' => $ignored_version,
2218 );
2219 }
2220 }
2221 }
2222
2223 return array(
2224 'ignored' => $all_ignored,
2225 'total' => count( $all_ignored ),
2226 'errors' => $errors,
2227 );
2228 }
2229
2230 /**
2231 * Execute callback for mainwp/set-ignored-updates-v1.
2232 *
2233 * @param array $input Validated input from Abilities API.
2234 * @return array|\WP_Error
2235 */
2236 public static function execute_set_ignored_updates( array $input ) {
2237 $action = $input['action'];
2238 $site_id_or_domain = $input['site_id_or_domain'];
2239 $type = $input['type'];
2240 $slug = $input['slug'];
2241
2242 // Validate action (defense-in-depth beyond schema validation).
2243 if ( ! in_array( $action, array( 'ignore', 'unignore' ), true ) ) {
2244 return new \WP_Error(
2245 'mainwp_invalid_input',
2246 /* translators: %s: The invalid action value provided */
2247 sprintf( __( 'Invalid action: %s. Must be "ignore" or "unignore".', 'mainwp' ), $action ),
2248 array( 'status' => 400 )
2249 );
2250 }
2251
2252 // Validate type (defense-in-depth beyond schema validation).
2253 if ( ! in_array( $type, array( 'core', 'plugin', 'theme' ), true ) ) {
2254 return new \WP_Error(
2255 'mainwp_invalid_input',
2256 /* translators: %s: The invalid type value provided */
2257 sprintf( __( 'Invalid type: %s. Must be "core", "plugin", or "theme".', 'mainwp' ), $type ),
2258 array( 'status' => 400 )
2259 );
2260 }
2261
2262 // Resolve site.
2263 $site = MainWP_Abilities_Util::resolve_site( $site_id_or_domain );
2264 if ( is_wp_error( $site ) ) {
2265 return $site;
2266 }
2267
2268 // Check site access.
2269 $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2270 if ( is_wp_error( $access_check ) ) {
2271 return $access_check;
2272 }
2273
2274 $site_id = (int) $site->id;
2275
2276 if ( 'ignore' === $action ) {
2277 $result = self::add_to_ignored_list( $site, $type, $slug );
2278 } else {
2279 $result = self::remove_from_ignored_list( $site, $type, $slug );
2280 }
2281
2282 if ( is_wp_error( $result ) ) {
2283 return $result;
2284 }
2285
2286 return array(
2287 'success' => true,
2288 'action' => $action,
2289 'site_id' => $site_id,
2290 'type' => $type,
2291 'slug' => $slug,
2292 );
2293 }
2294
2295 /**
2296 * Execute callback for mainwp/get-site-updates-v1.
2297 *
2298 * @param array $input Validated input from Abilities API.
2299 * @return array|\WP_Error
2300 */
2301 public static function execute_get_site_updates( array $input ) { //phpcs:ignore -- NOSONAR - complex method.
2302 $site_id_or_domain = $input['site_id_or_domain'];
2303 $types = $input['types'] ?? array( 'core', 'plugins', 'themes', 'translations' );
2304
2305 $site = MainWP_Abilities_Util::resolve_site( $site_id_or_domain );
2306 if ( is_wp_error( $site ) ) {
2307 return $site;
2308 }
2309
2310 $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2311 if ( is_wp_error( $access_check ) ) {
2312 return $access_check;
2313 }
2314
2315 $user_extension = MainWP_DB_Common::instance()->get_user_extension_by_user_id();
2316 $global_ignored_plugins = array();
2317 $global_ignored_themes = array();
2318 $global_ignored_core = array();
2319
2320 if ( is_object( $user_extension ) ) {
2321 $global_ignored_plugins = ! empty( $user_extension->ignored_plugins ) ? json_decode( $user_extension->ignored_plugins, true ) : array();
2322 $global_ignored_themes = ! empty( $user_extension->ignored_themes ) ? json_decode( $user_extension->ignored_themes, true ) : array();
2323 $global_ignored_core = ! empty( $user_extension->ignored_wp_upgrades ) ? json_decode( $user_extension->ignored_wp_upgrades, true ) : array();
2324 }
2325
2326 if ( ! is_array( $global_ignored_plugins ) ) {
2327 $global_ignored_plugins = array();
2328 }
2329 if ( ! is_array( $global_ignored_themes ) ) {
2330 $global_ignored_themes = array();
2331 }
2332 if ( ! is_array( $global_ignored_core ) ) {
2333 $global_ignored_core = array();
2334 }
2335
2336 $updates = self::get_site_updates( $site, $types, $global_ignored_plugins, $global_ignored_themes, $global_ignored_core );
2337
2338 $rollback_data = array(
2339 'plugins' => array(),
2340 'themes' => array(),
2341 );
2342
2343 if ( class_exists( '\MainWP\Dashboard\MainWP_Updates_Helper' ) ) {
2344 $rollback_items = MainWP_Updates_Helper::instance()->get_roll_items_updates_of_site( $site );
2345 if ( is_array( $rollback_items ) ) {
2346 // Merge returned data into defaults to ensure plugins/themes keys always exist.
2347 $rollback_data = array_merge( $rollback_data, $rollback_items );
2348 }
2349 }
2350
2351 $summary = array(
2352 'core' => 0,
2353 'plugins' => 0,
2354 'themes' => 0,
2355 'translations' => 0,
2356 'total' => count( $updates ),
2357 );
2358
2359 foreach ( $updates as $update ) {
2360 if ( 'core' === $update['type'] ) {
2361 ++$summary['core'];
2362 } elseif ( 'plugin' === $update['type'] ) {
2363 ++$summary['plugins'];
2364 } elseif ( 'theme' === $update['type'] ) {
2365 ++$summary['themes'];
2366 } elseif ( 'translation' === $update['type'] ) {
2367 ++$summary['translations'];
2368 }
2369 }
2370
2371 return array(
2372 'site_id' => (int) $site->id,
2373 'site_url' => $site->url,
2374 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
2375 'updates' => $updates,
2376 'rollback_items' => $rollback_data,
2377 'summary' => $summary,
2378 );
2379 }
2380
2381 /**
2382 * Execute callback for mainwp/update-site-core-v1.
2383 *
2384 * @param array $input Validated input from Abilities API.
2385 * @return array|\WP_Error
2386 */
2387 public static function execute_update_site_core( array $input ) {
2388 $site_id_or_domain = $input['site_id_or_domain'];
2389
2390 $site = MainWP_Abilities_Util::resolve_site( $site_id_or_domain );
2391 if ( is_wp_error( $site ) ) {
2392 return $site;
2393 }
2394
2395 $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2396 if ( is_wp_error( $access_check ) ) {
2397 return $access_check;
2398 }
2399
2400 if ( isset( $site->offline_check_result ) && -1 === (int) $site->offline_check_result ) {
2401 return new \WP_Error(
2402 'mainwp_site_offline',
2403 __( 'Site is known to be offline.', 'mainwp' ),
2404 array( 'status' => 400 )
2405 );
2406 }
2407
2408 $child_version_check = MainWP_Abilities_Util::check_child_version( $site, '4.0' );
2409 if ( is_wp_error( $child_version_check ) ) {
2410 return $child_version_check;
2411 }
2412
2413 $wp_upgrades = MainWP_DB::instance()->get_website_option( $site, 'wp_upgrades' );
2414 $wp_upgrades = ! empty( $wp_upgrades ) ? json_decode( $wp_upgrades, true ) : array();
2415
2416 if ( empty( $wp_upgrades ) || ! isset( $wp_upgrades['current'] ) || ! isset( $wp_upgrades['new'] ) ) {
2417 return new \WP_Error(
2418 'mainwp_no_updates',
2419 __( 'No core updates available for this site.', 'mainwp' ),
2420 array( 'status' => 404 )
2421 );
2422 }
2423
2424 $update_info = array(
2425 'current_version' => $wp_upgrades['current'],
2426 'new_version' => $wp_upgrades['new'],
2427 );
2428
2429 $result = self::execute_core_update( $site, $update_info );
2430
2431 if ( is_wp_error( $result ) ) {
2432 return $result;
2433 }
2434
2435 return array(
2436 'updated' => $result,
2437 );
2438 }
2439
2440 /**
2441 * Execute callback for mainwp/update-site-plugins-v1.
2442 *
2443 * @param array $input Validated input from Abilities API.
2444 * @return array|\WP_Error
2445 */
2446 public static function execute_update_site_plugins( array $input ) { // phpcs:ignore -- NOSONAR - complex method.
2447 $site_id_or_domain = $input['site_id_or_domain'];
2448 $slugs = $input['slugs'] ?? array();
2449
2450 $site = MainWP_Abilities_Util::resolve_site( $site_id_or_domain );
2451 if ( is_wp_error( $site ) ) {
2452 return $site;
2453 }
2454
2455 $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2456 if ( is_wp_error( $access_check ) ) {
2457 return $access_check;
2458 }
2459
2460 if ( isset( $site->offline_check_result ) && -1 === (int) $site->offline_check_result ) {
2461 return new \WP_Error(
2462 'mainwp_site_offline',
2463 __( 'Site is known to be offline.', 'mainwp' ),
2464 array( 'status' => 400 )
2465 );
2466 }
2467
2468 $child_version_check = MainWP_Abilities_Util::check_child_version( $site, '4.0' );
2469 if ( is_wp_error( $child_version_check ) ) {
2470 return $child_version_check;
2471 }
2472
2473 $user_extension = MainWP_DB_Common::instance()->get_user_extension_by_user_id();
2474 $global_ignored_plugins = array();
2475 if ( is_object( $user_extension ) ) {
2476 $global_ignored_plugins = ! empty( $user_extension->ignored_plugins ) ? json_decode( $user_extension->ignored_plugins, true ) : array();
2477 }
2478 if ( ! is_array( $global_ignored_plugins ) ) {
2479 $global_ignored_plugins = array();
2480 }
2481
2482 $site_ignored_plugins = ! empty( $site->ignored_plugins ) ? json_decode( $site->ignored_plugins, true ) : array();
2483 if ( ! is_array( $site_ignored_plugins ) ) {
2484 $site_ignored_plugins = array();
2485 }
2486
2487 $plugin_upgrades = ! empty( $site->plugin_upgrades ) ? json_decode( $site->plugin_upgrades, true ) : array();
2488 if ( ! is_array( $plugin_upgrades ) || empty( $plugin_upgrades ) ) {
2489 return array(
2490 'updated' => array(),
2491 'errors' => array(),
2492 'summary' => array(
2493 'total_updated' => 0,
2494 'total_errors' => 0,
2495 ),
2496 );
2497 }
2498
2499 if ( ! empty( $site_ignored_plugins ) ) {
2500 $plugin_upgrades = MainWP_Common_Functions::instance()->get_not_ignored_updates_themesplugins( $plugin_upgrades, $site_ignored_plugins );
2501 }
2502 if ( ! empty( $global_ignored_plugins ) ) {
2503 $plugin_upgrades = MainWP_Common_Functions::instance()->get_not_ignored_updates_themesplugins( $plugin_upgrades, $global_ignored_plugins );
2504 }
2505
2506 if ( ! empty( $slugs ) ) {
2507 $plugin_upgrades = array_filter(
2508 $plugin_upgrades,
2509 function ( $slug ) use ( $slugs ) {
2510 return in_array( $slug, $slugs, true );
2511 },
2512 ARRAY_FILTER_USE_KEY
2513 );
2514 }
2515
2516 if ( empty( $plugin_upgrades ) ) {
2517 return array(
2518 'updated' => array(),
2519 'errors' => array(),
2520 'summary' => array(
2521 'total_updated' => 0,
2522 'total_errors' => 0,
2523 ),
2524 );
2525 }
2526
2527 $updates = array();
2528 foreach ( $plugin_upgrades as $slug => $plugin ) {
2529
2530 $new_version = '';
2531
2532 if ( isset( $plugin['update']['new_version'] ) ) {
2533 $new_version = $plugin['update']['new_version'];
2534 } elseif ( isset( $plugin['new_version'] ) ) {
2535 $new_version = $plugin['new_version'];
2536 }
2537
2538 $updates[] = array(
2539 'slug' => $slug,
2540 'name' => isset( $plugin['Name'] ) ? $plugin['Name'] : $slug,
2541 'current_version' => isset( $plugin['Version'] ) ? $plugin['Version'] : '',
2542 'new_version' => $new_version,
2543 );
2544 }
2545
2546 $result = self::execute_plugin_updates( $site, $updates );
2547
2548 return array(
2549 'updated' => $result['updated'],
2550 'errors' => $result['errors'],
2551 'summary' => array(
2552 'total_updated' => count( $result['updated'] ),
2553 'total_errors' => count( $result['errors'] ),
2554 ),
2555 );
2556 }
2557
2558 /**
2559 * Execute callback for mainwp/update-site-themes-v1.
2560 *
2561 * @param array $input Validated input from Abilities API.
2562 * @return array|\WP_Error
2563 */
2564 public static function execute_update_site_themes( array $input ) { // phpcs:ignore -- NOSONAR - complex method.
2565 $site_id_or_domain = $input['site_id_or_domain'];
2566 $slugs = $input['slugs'] ?? array();
2567
2568 $site = MainWP_Abilities_Util::resolve_site( $site_id_or_domain );
2569 if ( is_wp_error( $site ) ) {
2570 return $site;
2571 }
2572
2573 $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2574 if ( is_wp_error( $access_check ) ) {
2575 return $access_check;
2576 }
2577
2578 if ( isset( $site->offline_check_result ) && -1 === (int) $site->offline_check_result ) {
2579 return new \WP_Error(
2580 'mainwp_site_offline',
2581 __( 'Site is known to be offline.', 'mainwp' ),
2582 array( 'status' => 400 )
2583 );
2584 }
2585
2586 $child_version_check = MainWP_Abilities_Util::check_child_version( $site, '4.0' );
2587 if ( is_wp_error( $child_version_check ) ) {
2588 return $child_version_check;
2589 }
2590
2591 $user_extension = MainWP_DB_Common::instance()->get_user_extension_by_user_id();
2592 $global_ignored_themes = array();
2593 if ( is_object( $user_extension ) ) {
2594 $global_ignored_themes = ! empty( $user_extension->ignored_themes ) ? json_decode( $user_extension->ignored_themes, true ) : array();
2595 }
2596 if ( ! is_array( $global_ignored_themes ) ) {
2597 $global_ignored_themes = array();
2598 }
2599
2600 $site_ignored_themes = ! empty( $site->ignored_themes ) ? json_decode( $site->ignored_themes, true ) : array();
2601 if ( ! is_array( $site_ignored_themes ) ) {
2602 $site_ignored_themes = array();
2603 }
2604
2605 $theme_upgrades = ! empty( $site->theme_upgrades ) ? json_decode( $site->theme_upgrades, true ) : array();
2606 if ( ! is_array( $theme_upgrades ) || empty( $theme_upgrades ) ) {
2607 return array(
2608 'updated' => array(),
2609 'errors' => array(),
2610 'summary' => array(
2611 'total_updated' => 0,
2612 'total_errors' => 0,
2613 ),
2614 );
2615 }
2616
2617 if ( ! empty( $site_ignored_themes ) ) {
2618 $theme_upgrades = MainWP_Common_Functions::instance()->get_not_ignored_updates_themesplugins( $theme_upgrades, $site_ignored_themes );
2619 }
2620 if ( ! empty( $global_ignored_themes ) ) {
2621 $theme_upgrades = MainWP_Common_Functions::instance()->get_not_ignored_updates_themesplugins( $theme_upgrades, $global_ignored_themes );
2622 }
2623
2624 if ( ! empty( $slugs ) ) {
2625 $theme_upgrades = array_filter(
2626 $theme_upgrades,
2627 function ( $slug ) use ( $slugs ) {
2628 return in_array( $slug, $slugs, true );
2629 },
2630 ARRAY_FILTER_USE_KEY
2631 );
2632 }
2633
2634 if ( empty( $theme_upgrades ) ) {
2635 return array(
2636 'updated' => array(),
2637 'errors' => array(),
2638 'summary' => array(
2639 'total_updated' => 0,
2640 'total_errors' => 0,
2641 ),
2642 );
2643 }
2644
2645 $updates = array();
2646 foreach ( $theme_upgrades as $slug => $theme ) {
2647 $updates[] = array(
2648 'slug' => $slug,
2649 'name' => isset( $theme['Name'] ) ? $theme['Name'] : $slug,
2650 'current_version' => isset( $theme['Version'] ) ? $theme['Version'] : '',
2651 'new_version' => $theme['update']['new_version'] ?? $theme['new_version'] ?? '',
2652 );
2653 }
2654
2655 $result = self::execute_theme_updates( $site, $updates );
2656
2657 return array(
2658 'updated' => $result['updated'],
2659 'errors' => $result['errors'],
2660 'summary' => array(
2661 'total_updated' => count( $result['updated'] ),
2662 'total_errors' => count( $result['errors'] ),
2663 ),
2664 );
2665 }
2666
2667 /**
2668 * Execute callback for mainwp/update-site-translations-v1.
2669 *
2670 * @param array $input Validated input from Abilities API.
2671 * @return array|\WP_Error
2672 */
2673 public static function execute_update_site_translations( array $input ) { // phpcs:ignore -- NOSONAR - complex method.
2674 $site_id_or_domain = $input['site_id_or_domain'];
2675 $slugs = $input['slugs'] ?? array();
2676
2677 $site = MainWP_Abilities_Util::resolve_site( $site_id_or_domain );
2678 if ( is_wp_error( $site ) ) {
2679 return $site;
2680 }
2681
2682 $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2683 if ( is_wp_error( $access_check ) ) {
2684 return $access_check;
2685 }
2686
2687 if ( isset( $site->offline_check_result ) && -1 === (int) $site->offline_check_result ) {
2688 return new \WP_Error(
2689 'mainwp_site_offline',
2690 __( 'Site is known to be offline.', 'mainwp' ),
2691 array( 'status' => 400 )
2692 );
2693 }
2694
2695 $child_version_check = MainWP_Abilities_Util::check_child_version( $site, '4.0' );
2696 if ( is_wp_error( $child_version_check ) ) {
2697 return $child_version_check;
2698 }
2699
2700 $translation_upgrades = ! empty( $site->translation_upgrades ) ? json_decode( $site->translation_upgrades, true ) : array();
2701 if ( ! is_array( $translation_upgrades ) || empty( $translation_upgrades ) ) {
2702 return array(
2703 'updated' => array(),
2704 'errors' => array(),
2705 'summary' => array(
2706 'total_updated' => 0,
2707 'total_errors' => 0,
2708 ),
2709 );
2710 }
2711
2712 if ( ! empty( $slugs ) ) {
2713 $translation_upgrades = array_filter(
2714 $translation_upgrades,
2715 function ( $translation ) use ( $slugs ) {
2716 return isset( $translation['slug'] ) && in_array( $translation['slug'], $slugs, true );
2717 }
2718 );
2719 }
2720
2721 if ( empty( $translation_upgrades ) ) {
2722 return array(
2723 'updated' => array(),
2724 'errors' => array(),
2725 'summary' => array(
2726 'total_updated' => 0,
2727 'total_errors' => 0,
2728 ),
2729 );
2730 }
2731
2732 $updates = array();
2733 foreach ( $translation_upgrades as $translation ) {
2734
2735 $name = '';
2736
2737 if ( isset( $translation['name'] ) ) {
2738 $name = $translation['name'];
2739 } elseif ( isset( $translation['slug'] ) ) {
2740 $name = $translation['slug'];
2741 }
2742
2743 $updates[] = array(
2744 'slug' => isset( $translation['slug'] ) ? $translation['slug'] : '',
2745 'name' => $name,
2746 'current_version' => isset( $translation['version'] ) ? $translation['version'] : '',
2747 'new_version' => isset( $translation['new_version'] ) ? $translation['new_version'] : '',
2748 );
2749 }
2750
2751 $result = self::execute_translation_updates( $site, $updates );
2752
2753 return array(
2754 'updated' => $result['updated'],
2755 'errors' => $result['errors'],
2756 'summary' => array(
2757 'total_updated' => count( $result['updated'] ),
2758 'total_errors' => count( $result['errors'] ),
2759 ),
2760 );
2761 }
2762
2763 /**
2764 * Execute callback for mainwp/ignore-site-core-v1.
2765 *
2766 * @param array $input Validated input from Abilities API.
2767 * @return array|\WP_Error
2768 */
2769 public static function execute_ignore_site_core( array $input ) {
2770 $site_id_or_domain = $input['site_id_or_domain'];
2771 $action = $input['action'] ?? 'add';
2772
2773 $site = MainWP_Abilities_Util::resolve_site( $site_id_or_domain );
2774 if ( is_wp_error( $site ) ) {
2775 return $site;
2776 }
2777
2778 $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2779 if ( is_wp_error( $access_check ) ) {
2780 return $access_check;
2781 }
2782
2783 if ( 'add' === $action ) {
2784 $result = self::add_to_ignored_list( $site, 'core', 'wordpress' );
2785 $message = __( 'Core updates ignored for this site.', 'mainwp' );
2786 } else {
2787 $result = self::remove_from_ignored_list( $site, 'core', 'wordpress' );
2788 $message = __( 'Core updates unignored for this site.', 'mainwp' );
2789 }
2790
2791 if ( is_wp_error( $result ) ) {
2792 return $result;
2793 }
2794
2795 return array(
2796 'success' => true,
2797 'message' => $message,
2798 'ignored_count' => 1,
2799 );
2800 }
2801
2802 /**
2803 * Execute callback for mainwp/ignore-site-plugins-v1.
2804 *
2805 * @param array $input Validated input from Abilities API.
2806 * @return array|\WP_Error
2807 */
2808 public static function execute_ignore_site_plugins( array $input ) {
2809 $site_id_or_domain = $input['site_id_or_domain'];
2810 $slugs = $input['slugs'];
2811 $action = $input['action'] ?? 'add';
2812
2813 $site = MainWP_Abilities_Util::resolve_site( $site_id_or_domain );
2814 if ( is_wp_error( $site ) ) {
2815 return $site;
2816 }
2817
2818 $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2819 if ( is_wp_error( $access_check ) ) {
2820 return $access_check;
2821 }
2822
2823 $count = 0;
2824 foreach ( $slugs as $slug ) {
2825 if ( 'add' === $action ) {
2826 $result = self::add_to_ignored_list( $site, 'plugin', $slug );
2827 } else {
2828 $result = self::remove_from_ignored_list( $site, 'plugin', $slug );
2829 }
2830
2831 if ( ! is_wp_error( $result ) ) {
2832 ++$count;
2833 }
2834 }
2835
2836 if ( 'add' === $action && $count > 0 ) {
2837 $message = sprintf(
2838 /* translators: %d: Number of plugins */
2839 _n( '%d plugin ignored.', '%d plugins ignored.', $count, 'mainwp' ),
2840 $count
2841 );
2842 } elseif ( 'add' === $action ) {
2843 $message = __( 'No plugins were ignored.', 'mainwp' );
2844 } elseif ( $count > 0 ) {
2845 $message = sprintf(
2846 /* translators: %d: Number of plugins */
2847 _n( '%d plugin unignored.', '%d plugins unignored.', $count, 'mainwp' ),
2848 $count
2849 );
2850 } else {
2851 $message = __( 'No plugins were unignored.', 'mainwp' );
2852 }
2853
2854 return array(
2855 'success' => $count > 0,
2856 'message' => $message,
2857 'ignored_count' => $count,
2858 );
2859 }
2860
2861 /**
2862 * Execute callback for mainwp/ignore-site-themes-v1.
2863 *
2864 * @param array $input Validated input from Abilities API.
2865 * @return array|\WP_Error
2866 */
2867 public static function execute_ignore_site_themes( array $input ) {
2868 $site_id_or_domain = $input['site_id_or_domain'];
2869 $slugs = $input['slugs'];
2870 $action = $input['action'] ?? 'add';
2871
2872 $site = MainWP_Abilities_Util::resolve_site( $site_id_or_domain );
2873 if ( is_wp_error( $site ) ) {
2874 return $site;
2875 }
2876
2877 $access_check = MainWP_Abilities_Util::check_site_access( $site, $input );
2878 if ( is_wp_error( $access_check ) ) {
2879 return $access_check;
2880 }
2881
2882 $count = 0;
2883 foreach ( $slugs as $slug ) {
2884 if ( 'add' === $action ) {
2885 $result = self::add_to_ignored_list( $site, 'theme', $slug );
2886 } else {
2887 $result = self::remove_from_ignored_list( $site, 'theme', $slug );
2888 }
2889
2890 if ( ! is_wp_error( $result ) ) {
2891 ++$count;
2892 }
2893 }
2894
2895 if ( 'add' === $action && $count > 0 ) {
2896 $message = sprintf(
2897 /* translators: %d: Number of themes */
2898 _n( '%d theme ignored.', '%d themes ignored.', $count, 'mainwp' ),
2899 $count
2900 );
2901 } elseif ( 'add' === $action ) {
2902 $message = __( 'No themes were ignored.', 'mainwp' );
2903 } elseif ( $count > 0 ) {
2904 $message = sprintf(
2905 /* translators: %d: Number of themes */
2906 _n( '%d theme unignored.', '%d themes unignored.', $count, 'mainwp' ),
2907 $count
2908 );
2909 } else {
2910 $message = __( 'No themes were unignored.', 'mainwp' );
2911 }
2912
2913 return array(
2914 'success' => $count > 0,
2915 'message' => $message,
2916 'ignored_count' => $count,
2917 );
2918 }
2919
2920 /**
2921 * Execute callback for mainwp/update-all-v1.
2922 *
2923 * @param array|null $input Validated input from Abilities API.
2924 * @return array|\WP_Error
2925 */
2926 public static function execute_update_all( $input ) { // phpcs:ignore -- NOSONAR - complex method.
2927 $input = is_array( $input ) ? $input : array();
2928 $site_ids_or_domains = $input['site_ids_or_domains'] ?? array();
2929 $types = $input['types'] ?? array( 'core', 'plugins', 'themes', 'translations' );
2930
2931 // If empty, get all sites for current user.
2932 if ( empty( $site_ids_or_domains ) ) {
2933 $all_sites = MainWP_DB::instance()->get_websites_for_current_user( array( 'selectgroups' => false ) );
2934
2935 if ( is_wp_error( $all_sites ) ) {
2936 return $all_sites;
2937 }
2938
2939 $site_ids_or_domains = array_map(
2940 function ( $s ) {
2941 return (int) $s->id;
2942 },
2943 $all_sites ? $all_sites : array()
2944 );
2945 }
2946
2947 // Check per-site ACLs and filter to allowed sites.
2948 $access_check = MainWP_Abilities_Util::check_batch_site_access( $site_ids_or_domains, $input );
2949
2950 // Normalize ACL-denied errors to match the documented error item schema.
2951 $acl_errors = array();
2952 foreach ( $access_check['denied'] as $denied_entry ) {
2953 $acl_errors[] = array(
2954 'identifier' => $denied_entry['identifier'],
2955 'code' => $denied_entry['code'],
2956 'message' => $denied_entry['message'],
2957 );
2958 }
2959
2960 $sites = $access_check['allowed'];
2961
2962 if ( empty( $sites ) ) {
2963 $normalized_errors = self::normalize_resolution_errors( $acl_errors );
2964 return array(
2965 'updated' => array(),
2966 'errors' => $normalized_errors,
2967 'summary' => array(
2968 'total_updated' => 0,
2969 'total_errors' => count( $normalized_errors ),
2970 'sites_updated' => 0,
2971 ),
2972 );
2973 }
2974
2975 $threshold = apply_filters( 'mainwp_abilities_batch_threshold', self::BATCH_THRESHOLD );
2976
2977 if ( count( $sites ) > $threshold ) {
2978 $job_id = MainWP_Abilities_Util::queue_batch_updates(
2979 $sites,
2980 array(
2981 'types' => $types,
2982 )
2983 );
2984
2985 if ( is_wp_error( $job_id ) ) {
2986 return $job_id;
2987 }
2988
2989 return array(
2990 'queued' => true,
2991 'job_id' => $job_id,
2992 'status_url' => rest_url( "mainwp/v2/jobs/{$job_id}" ),
2993 'updates_queued' => count( $sites ),
2994 'errors' => self::normalize_resolution_errors( $acl_errors ),
2995 );
2996 }
2997
2998 $user_extension = MainWP_DB_Common::instance()->get_user_extension_by_user_id();
2999 $global_ignored_plugins = array();
3000 $global_ignored_themes = array();
3001 $global_ignored_core = array();
3002
3003 if ( is_object( $user_extension ) ) {
3004 $global_ignored_plugins = ! empty( $user_extension->ignored_plugins ) ? json_decode( $user_extension->ignored_plugins, true ) : array();
3005 $global_ignored_themes = ! empty( $user_extension->ignored_themes ) ? json_decode( $user_extension->ignored_themes, true ) : array();
3006 $global_ignored_core = ! empty( $user_extension->ignored_wp_upgrades ) ? json_decode( $user_extension->ignored_wp_upgrades, true ) : array();
3007 }
3008
3009 if ( ! is_array( $global_ignored_plugins ) ) {
3010 $global_ignored_plugins = array();
3011 }
3012 if ( ! is_array( $global_ignored_themes ) ) {
3013 $global_ignored_themes = array();
3014 }
3015 if ( ! is_array( $global_ignored_core ) ) {
3016 $global_ignored_core = array();
3017 }
3018
3019 $all_updated = array();
3020 $all_errors = self::normalize_resolution_errors( $acl_errors );
3021 $sites_updated_set = array();
3022
3023 foreach ( $sites as $site ) {
3024 // Check if site is known offline before attempting updates.
3025 if ( isset( $site->offline_check_result ) && -1 === (int) $site->offline_check_result ) {
3026 $all_errors[] = array(
3027 'site_id' => (int) $site->id,
3028 'site_url' => $site->url,
3029 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3030 'type' => 'site',
3031 'slug' => '',
3032 'code' => 'mainwp_site_offline',
3033 'message' => __( 'Site is known to be offline.', 'mainwp' ),
3034 );
3035 continue;
3036 }
3037
3038 $site_updates = self::get_site_updates( $site, $types, $global_ignored_plugins, $global_ignored_themes, $global_ignored_core );
3039
3040 if ( empty( $site_updates ) ) {
3041 continue;
3042 }
3043
3044 $child_version_check = MainWP_Abilities_Util::check_child_version( $site, '4.0' );
3045 if ( is_wp_error( $child_version_check ) ) {
3046 foreach ( $site_updates as $update ) {
3047 $all_errors[] = array(
3048 'site_id' => (int) $site->id,
3049 'site_url' => $site->url,
3050 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3051 'type' => $update['type'],
3052 'slug' => $update['slug'],
3053 'code' => $child_version_check->get_error_code(),
3054 'message' => $child_version_check->get_error_message(),
3055 );
3056 }
3057 continue;
3058 }
3059
3060 $core_updates = array();
3061 $plugin_updates = array();
3062 $theme_updates = array();
3063 $translation_updates = array();
3064
3065 foreach ( $site_updates as $update ) {
3066 if ( 'core' === $update['type'] ) {
3067 $core_updates[] = $update;
3068 } elseif ( 'plugin' === $update['type'] ) {
3069 $plugin_updates[] = $update;
3070 } elseif ( 'theme' === $update['type'] ) {
3071 $theme_updates[] = $update;
3072 } elseif ( 'translation' === $update['type'] ) {
3073 $translation_updates[] = $update;
3074 }
3075 }
3076
3077 if ( ! empty( $core_updates ) && in_array( 'core', $types, true ) ) {
3078 $core_update = $core_updates[0];
3079 $result = self::execute_core_update( $site, $core_update );
3080 if ( is_wp_error( $result ) ) {
3081 $all_errors[] = array(
3082 'site_id' => (int) $site->id,
3083 'site_url' => $site->url,
3084 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3085 'type' => 'core',
3086 'slug' => 'wordpress',
3087 'code' => $result->get_error_code(),
3088 'message' => $result->get_error_message(),
3089 );
3090 } else {
3091 $all_updated[] = $result;
3092 $sites_updated_set[ (int) $site->id ] = true;
3093 }
3094 }
3095
3096 if ( ! empty( $plugin_updates ) && in_array( 'plugins', $types, true ) ) {
3097 $result = self::execute_plugin_updates( $site, $plugin_updates );
3098 if ( ! empty( $result['updated'] ) ) {
3099 $all_updated = array_merge( $all_updated, $result['updated'] );
3100 $sites_updated_set[ (int) $site->id ] = true;
3101 }
3102 if ( ! empty( $result['errors'] ) ) {
3103 $all_errors = array_merge( $all_errors, $result['errors'] );
3104 }
3105 }
3106
3107 if ( ! empty( $theme_updates ) && in_array( 'themes', $types, true ) ) {
3108 $result = self::execute_theme_updates( $site, $theme_updates );
3109 if ( ! empty( $result['updated'] ) ) {
3110 $all_updated = array_merge( $all_updated, $result['updated'] );
3111 $sites_updated_set[ (int) $site->id ] = true;
3112 }
3113 if ( ! empty( $result['errors'] ) ) {
3114 $all_errors = array_merge( $all_errors, $result['errors'] );
3115 }
3116 }
3117
3118 if ( ! empty( $translation_updates ) && in_array( 'translations', $types, true ) ) {
3119 $result = self::execute_translation_updates( $site, $translation_updates );
3120 if ( ! empty( $result['updated'] ) ) {
3121 $all_updated = array_merge( $all_updated, $result['updated'] );
3122 $sites_updated_set[ (int) $site->id ] = true;
3123 }
3124 if ( ! empty( $result['errors'] ) ) {
3125 $all_errors = array_merge( $all_errors, $result['errors'] );
3126 }
3127 }
3128 }
3129
3130 return array(
3131 'updated' => $all_updated,
3132 'errors' => $all_errors,
3133 'summary' => array(
3134 'total_updated' => count( $all_updated ),
3135 'total_errors' => count( $all_errors ),
3136 'sites_updated' => count( $sites_updated_set ),
3137 ),
3138 );
3139 }
3140
3141 // =========================================================================
3142 // Helper Methods
3143 // =========================================================================
3144
3145 /**
3146 * Normalize resolution errors to match the documented error schema.
3147 *
3148 * Converts errors from resolve_sites() format (identifier, code, message, status)
3149 * to the update-all-v1 output schema format (site_id, site_url, site_name, type, slug, code, message).
3150 *
3151 * @param array $resolution_errors Raw resolution errors from resolve_sites().
3152 * @return array Normalized errors matching the output schema.
3153 */
3154 private static function normalize_resolution_errors( array $resolution_errors ): array {
3155 $normalized = array();
3156
3157 foreach ( $resolution_errors as $err ) {
3158 // Convert identifier to site_url if it's a scalar (string or int).
3159 $site_url = '';
3160 if ( isset( $err['identifier'] ) && is_scalar( $err['identifier'] ) ) {
3161 $site_url = (string) $err['identifier'];
3162 }
3163
3164 $normalized[] = array(
3165 'site_id' => 0,
3166 'site_url' => $site_url,
3167 'site_name' => '',
3168 'type' => 'site',
3169 'slug' => '',
3170 'code' => isset( $err['code'] ) ? $err['code'] : 'mainwp_unknown_error',
3171 'message' => isset( $err['message'] ) ? $err['message'] : __( 'Unknown error during site resolution.', 'mainwp' ),
3172 );
3173 }
3174
3175 return $normalized;
3176 }
3177
3178 /**
3179 * Allowed columns for get_site_column_value() to prevent SQL injection.
3180 *
3181 * @var array
3182 */
3183 private static $allowed_site_columns = array(
3184 'plugin_upgrades',
3185 'theme_upgrades',
3186 'translation_upgrades',
3187 'ignored_plugins',
3188 'ignored_themes',
3189 );
3190
3191 /**
3192 * Get a site column value with fallback to direct database query.
3193 *
3194 * This method first tries to read from the site object property. If not found
3195 * (empty), it falls back to a direct database query. This ensures update data
3196 * is retrieved regardless of how the site object was constructed.
3197 *
3198 * @param object $site Site object.
3199 * @param string $column Column name from wp table (must be in allowed list).
3200 * @return string Column value or empty string.
3201 */
3202 private static function get_site_column_value( $site, string $column ): string {
3203 // Validate column is in allowed list (security).
3204 if ( ! in_array( $column, self::$allowed_site_columns, true ) ) {
3205 return '';
3206 }
3207
3208 // Validate site has an ID.
3209 if ( empty( $site->id ) ) {
3210 return '';
3211 }
3212
3213 // Try object property first (fast path).
3214 if ( ! empty( $site->{$column} ) ) {
3215 return $site->{$column};
3216 }
3217
3218 // Fallback: direct database query.
3219 global $wpdb;
3220 $table = MainWP_DB::instance()->get_table_name( 'wp' );
3221 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Fallback query; $column and $table are validated.
3222 $value = $wpdb->get_var(
3223 $wpdb->prepare(
3224 "SELECT {$column} FROM {$table} WHERE id = %d",
3225 (int) $site->id
3226 )
3227 );
3228 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
3229
3230 return $value ?? '';
3231 }
3232
3233 /**
3234 * Get updates for a single site.
3235 *
3236 * @param object $site Site object.
3237 * @param array $types Update types to include.
3238 * @param array $global_ignored_plugins Global ignored plugins list.
3239 * @param array $global_ignored_themes Global ignored themes list.
3240 * @param array $global_ignored_core Global ignored core list.
3241 * @return array Array of update items.
3242 */
3243 private static function get_site_updates( $site, array $types, array $global_ignored_plugins, array $global_ignored_themes, array $global_ignored_core ): array { // phpcs:ignore -- NOSONAR - complex method.
3244 $updates = array();
3245
3246 $site_id = (int) $site->id;
3247 $site_url = $site->url;
3248 $site_name = MainWP_Utility::remove_http_prefix( (string) $site->name, true );
3249
3250 // Get per-site ignored lists (with fallback to database query).
3251 $raw_ignored_plugins = self::get_site_column_value( $site, 'ignored_plugins' );
3252 $site_ignored_plugins = ! empty( $raw_ignored_plugins ) ? json_decode( $raw_ignored_plugins, true ) : array();
3253 $raw_ignored_themes = self::get_site_column_value( $site, 'ignored_themes' );
3254 $site_ignored_themes = ! empty( $raw_ignored_themes ) ? json_decode( $raw_ignored_themes, true ) : array();
3255
3256 if ( ! is_array( $site_ignored_plugins ) ) {
3257 $site_ignored_plugins = array();
3258 }
3259 if ( ! is_array( $site_ignored_themes ) ) {
3260 $site_ignored_themes = array();
3261 }
3262
3263 // Core updates.
3264 // Guard against missing ignore flag property (treat missing/falsy as "not ignored").
3265 if ( in_array( 'core', $types, true ) && empty( $site->is_ignoreCoreUpdates ) ) {
3266 $wp_upgrades = MainWP_DB::instance()->get_website_option( $site, 'wp_upgrades' );
3267 $wp_upgrades = ! empty( $wp_upgrades ) ? json_decode( $wp_upgrades, true ) : array();
3268
3269 if ( is_array( $wp_upgrades ) && ! empty( $wp_upgrades ) ) {
3270 // Check if ignored.
3271 $site_ignored_core = MainWP_DB::instance()->get_website_option( $site, 'ignored_wp_upgrades' );
3272 $site_ignored_core = ! empty( $site_ignored_core ) ? json_decode( $site_ignored_core, true ) : array();
3273
3274 $is_ignored = false;
3275 if ( ! empty( $site_ignored_core ) && MainWP_Common_Functions::instance()->is_ignored_updates( $wp_upgrades, $site_ignored_core, 'core' ) ) {
3276 $is_ignored = true;
3277 }
3278 if ( ! $is_ignored && ! empty( $global_ignored_core ) && MainWP_Common_Functions::instance()->is_ignored_updates( $wp_upgrades, $global_ignored_core, 'core' ) ) {
3279 $is_ignored = true;
3280 }
3281
3282 if ( ! $is_ignored ) {
3283 $updates[] = array(
3284 'site_id' => $site_id,
3285 'site_url' => $site_url,
3286 'site_name' => $site_name,
3287 'type' => 'core',
3288 'slug' => 'wordpress',
3289 'name' => 'WordPress',
3290 'current_version' => isset( $wp_upgrades['current'] ) ? $wp_upgrades['current'] : '',
3291 'new_version' => isset( $wp_upgrades['new'] ) ? $wp_upgrades['new'] : '',
3292 );
3293 }
3294 }
3295 }
3296
3297 // Plugin updates.
3298 // Guard against missing ignore flag property (treat missing/falsy as "not ignored").
3299 if ( in_array( 'plugins', $types, true ) && empty( $site->is_ignorePluginUpdates ) ) {
3300 $raw_plugin_upgrades = self::get_site_column_value( $site, 'plugin_upgrades' );
3301 $plugin_upgrades = ! empty( $raw_plugin_upgrades ) ? json_decode( $raw_plugin_upgrades, true ) : array();
3302
3303 if ( is_array( $plugin_upgrades ) && ! empty( $plugin_upgrades ) ) {
3304 // Filter by per-site ignored.
3305 if ( ! empty( $site_ignored_plugins ) ) {
3306 $plugin_upgrades = MainWP_Common_Functions::instance()->get_not_ignored_updates_themesplugins( $plugin_upgrades, $site_ignored_plugins );
3307 }
3308 // Filter by global ignored.
3309 if ( ! empty( $global_ignored_plugins ) ) {
3310 $plugin_upgrades = MainWP_Common_Functions::instance()->get_not_ignored_updates_themesplugins( $plugin_upgrades, $global_ignored_plugins );
3311 }
3312
3313 foreach ( $plugin_upgrades as $slug => $plugin ) {
3314
3315 $new_version = '';
3316
3317 if ( isset( $plugin['update']['new_version'] ) ) {
3318 $new_version = $plugin['update']['new_version'];
3319 } elseif ( isset( $plugin['new_version'] ) ) {
3320 $new_version = $plugin['new_version'];
3321 }
3322
3323 $updates[] = array(
3324 'site_id' => $site_id,
3325 'site_url' => $site_url,
3326 'site_name' => $site_name,
3327 'type' => 'plugin',
3328 'slug' => $slug,
3329 'name' => isset( $plugin['Name'] ) ? $plugin['Name'] : $slug,
3330 'current_version' => isset( $plugin['Version'] ) ? $plugin['Version'] : '',
3331 'new_version' => $new_version,
3332 );
3333 }
3334 }
3335 }
3336
3337 // Theme updates.
3338 // Guard against missing ignore flag property (treat missing/falsy as "not ignored").
3339 if ( in_array( 'themes', $types, true ) && empty( $site->is_ignoreThemeUpdates ) ) {
3340 $raw_theme_upgrades = self::get_site_column_value( $site, 'theme_upgrades' );
3341 $theme_upgrades = ! empty( $raw_theme_upgrades ) ? json_decode( $raw_theme_upgrades, true ) : array();
3342
3343 if ( is_array( $theme_upgrades ) && ! empty( $theme_upgrades ) ) {
3344 // Filter by per-site ignored.
3345 if ( ! empty( $site_ignored_themes ) ) {
3346 $theme_upgrades = MainWP_Common_Functions::instance()->get_not_ignored_updates_themesplugins( $theme_upgrades, $site_ignored_themes );
3347 }
3348 // Filter by global ignored.
3349 if ( ! empty( $global_ignored_themes ) ) {
3350 $theme_upgrades = MainWP_Common_Functions::instance()->get_not_ignored_updates_themesplugins( $theme_upgrades, $global_ignored_themes );
3351 }
3352
3353 foreach ( $theme_upgrades as $slug => $theme ) {
3354
3355 $new_version = '';
3356
3357 if ( isset( $theme['update']['new_version'] ) ) {
3358 $new_version = $theme['update']['new_version'];
3359 } elseif ( isset( $theme['new_version'] ) ) {
3360 $new_version = $theme['new_version'];
3361 }
3362
3363 $updates[] = array(
3364 'site_id' => $site_id,
3365 'site_url' => $site_url,
3366 'site_name' => $site_name,
3367 'type' => 'theme',
3368 'slug' => $slug,
3369 'name' => isset( $theme['Name'] ) ? $theme['Name'] : $slug,
3370 'current_version' => isset( $theme['Version'] ) ? $theme['Version'] : '',
3371 'new_version' => $new_version,
3372 );
3373 }
3374 }
3375 }
3376
3377 // Translation updates.
3378 if ( in_array( 'translations', $types, true ) ) {
3379 $raw_translation_upgrades = self::get_site_column_value( $site, 'translation_upgrades' );
3380 $translation_upgrades = ! empty( $raw_translation_upgrades ) ? json_decode( $raw_translation_upgrades, true ) : array();
3381
3382 if ( is_array( $translation_upgrades ) && ! empty( $translation_upgrades ) ) {
3383 foreach ( $translation_upgrades as $translation ) {
3384
3385 $name = '';
3386
3387 if ( isset( $translation['name'] ) ) {
3388 $name = $translation['name'];
3389 } elseif ( isset( $translation['slug'] ) ) {
3390 $name = $translation['slug'];
3391 }
3392
3393 $updates[] = array(
3394 'site_id' => $site_id,
3395 'site_url' => $site_url,
3396 'site_name' => $site_name,
3397 'type' => 'translation',
3398 'slug' => isset( $translation['slug'] ) ? $translation['slug'] : '',
3399 'name' => $name,
3400 'current_version' => isset( $translation['version'] ) ? $translation['version'] : '',
3401 'new_version' => isset( $translation['new_version'] ) ? $translation['new_version'] : '',
3402 );
3403 }
3404 }
3405 }
3406
3407 return $updates;
3408 }
3409
3410 /**
3411 * Execute core update for a site.
3412 *
3413 * @param object $site Site object.
3414 * @param array $update_info Update information.
3415 * @return array|\WP_Error Update result or error.
3416 */
3417 private static function execute_core_update( $site, array $update_info ) {
3418 try {
3419 /**
3420 * Action: mainwp_before_wp_update
3421 *
3422 * Fires before WP update.
3423 *
3424 * @since 4.1
3425 */
3426 do_action( 'mainwp_before_wp_update', $site );
3427
3428 $information = MainWP_Connect::fetch_url_authed( $site, 'upgrade' );
3429
3430 /**
3431 * Action: mainwp_after_wp_update
3432 *
3433 * Fires after WP update.
3434 *
3435 * @since 4.1
3436 */
3437 do_action( 'mainwp_after_wp_update', $information, $site );
3438
3439 if ( is_array( $information ) && isset( $information['upgrade'] ) && 'SUCCESS' === $information['upgrade'] ) {
3440 // Clear wp_upgrades option.
3441 MainWP_DB::instance()->update_website_option( $site, 'wp_upgrades', wp_json_encode( array() ) );
3442
3443 do_action( 'mainwp_after_upgrade_wp_success', $site, $information );
3444
3445 return array(
3446 'site_id' => (int) $site->id,
3447 'site_url' => $site->url,
3448 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3449 'type' => 'core',
3450 'slug' => 'wordpress',
3451 'name' => 'WordPress',
3452 'old_version' => $update_info['current_version'],
3453 'new_version' => isset( $information['version'] ) ? $information['version'] : $update_info['new_version'],
3454 );
3455 }
3456
3457 // Handle WP_Error from fetch_url_authed.
3458 if ( is_wp_error( $information ) ) {
3459 return new \WP_Error( 'mainwp_update_failed', $information->get_error_message() );
3460 }
3461
3462 $error_message = __( 'Core update failed.', 'mainwp' );
3463 if ( is_array( $information ) ) {
3464 if ( isset( $information['error'] ) ) {
3465 $error_message = $information['error'];
3466 } elseif ( isset( $information['upgrade'] ) ) {
3467 if ( 'LOCALIZATION' === $information['upgrade'] ) {
3468 $error_message = __( 'No update found for the set locale.', 'mainwp' );
3469 } elseif ( 'NORESPONSE' === $information['upgrade'] ) {
3470 $error_message = __( 'No response from the child site server.', 'mainwp' );
3471 }
3472 }
3473 }
3474
3475 return new \WP_Error( 'mainwp_update_failed', $error_message );
3476 } catch ( \Exception $e ) {
3477 return new \WP_Error( 'mainwp_update_exception', $e->getMessage() );
3478 }
3479 }
3480
3481 /**
3482 * Execute plugin updates for a site.
3483 *
3484 * @param object $site Site object.
3485 * @param array $updates Array of plugin updates.
3486 * @return array Array with 'updated' and 'errors' keys.
3487 */
3488 private static function execute_plugin_updates( $site, array $updates ): array { // phpcs:ignore -- NOSONAR - complex method.
3489 $updated = array();
3490 $errors = array();
3491
3492 $slugs = array_map(
3493 function ( $u ) {
3494 return $u['slug'];
3495 },
3496 $updates
3497 );
3498
3499 // Build slug-to-update map.
3500 $update_map = array();
3501 foreach ( $updates as $update ) {
3502 $update_map[ $update['slug'] ] = $update;
3503 }
3504
3505 try {
3506 /**
3507 * Action: mainwp_before_plugin_theme_translation_update
3508 *
3509 * Fires before plugin/theme/translation update actions.
3510 *
3511 * @since 4.1
3512 */
3513 do_action( 'mainwp_before_plugin_theme_translation_update', 'plugin', implode( ',', $slugs ), $site );
3514
3515 $information = MainWP_Connect::fetch_url_authed(
3516 $site,
3517 'upgradeplugintheme',
3518 array(
3519 'type' => 'plugin',
3520 'list' => implode( ',', $slugs ),
3521 ),
3522 true
3523 );
3524
3525 /**
3526 * Action: mainwp_after_plugin_theme_translation_update
3527 *
3528 * Fires after plugin/theme/translation update actions.
3529 *
3530 * @since 4.1
3531 */
3532 do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'plugin', implode( ',', $slugs ), $site );
3533
3534 if ( is_array( $information ) && isset( $information['upgrades'] ) ) {
3535 foreach ( $information['upgrades'] as $slug => $result ) {
3536 if ( ! empty( $result ) && isset( $update_map[ $slug ] ) ) {
3537 $updated[] = array(
3538 'site_id' => (int) $site->id,
3539 'site_url' => $site->url,
3540 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3541 'type' => 'plugin',
3542 'slug' => $slug,
3543 'name' => $update_map[ $slug ]['name'],
3544 'old_version' => $update_map[ $slug ]['current_version'],
3545 'new_version' => $update_map[ $slug ]['new_version'],
3546 );
3547 }
3548 }
3549 }
3550
3551 if ( is_array( $information ) && isset( $information['upgrades_error'] ) ) {
3552 foreach ( $information['upgrades_error'] as $slug => $error_msg ) {
3553 $errors[] = array(
3554 'site_id' => (int) $site->id,
3555 'site_url' => $site->url,
3556 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3557 'type' => 'plugin',
3558 'slug' => $slug,
3559 'code' => 'mainwp_update_failed',
3560 'message' => $error_msg,
3561 );
3562 }
3563 }
3564
3565 // Update stored plugin_upgrades to remove updated items.
3566 if ( ! empty( $updated ) ) {
3567 $current_upgrades = ! empty( $site->plugin_upgrades ) ? json_decode( $site->plugin_upgrades, true ) : array();
3568 if ( is_array( $current_upgrades ) ) {
3569 foreach ( $updated as $u ) {
3570 unset( $current_upgrades[ $u['slug'] ] );
3571 }
3572 MainWP_DB::instance()->update_website_values( $site->id, array( 'plugin_upgrades' => wp_json_encode( $current_upgrades ) ) );
3573 }
3574 }
3575 } catch ( \Exception $e ) {
3576 foreach ( $slugs as $slug ) {
3577 $errors[] = array(
3578 'site_id' => (int) $site->id,
3579 'site_url' => $site->url,
3580 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3581 'type' => 'plugin',
3582 'slug' => $slug,
3583 'code' => 'mainwp_update_exception',
3584 'message' => $e->getMessage(),
3585 );
3586 }
3587 }
3588
3589 return array(
3590 'updated' => $updated,
3591 'errors' => $errors,
3592 );
3593 }
3594
3595 /**
3596 * Execute theme updates for a site.
3597 *
3598 * @param object $site Site object.
3599 * @param array $updates Array of theme updates.
3600 * @return array Array with 'updated' and 'errors' keys.
3601 */
3602 private static function execute_theme_updates( $site, array $updates ): array { // phpcs:ignore -- NOSONAR - complex method.
3603 $updated = array();
3604 $errors = array();
3605
3606 $slugs = array_map(
3607 function ( $u ) {
3608 return $u['slug'];
3609 },
3610 $updates
3611 );
3612
3613 // Build slug-to-update map.
3614 $update_map = array();
3615 foreach ( $updates as $update ) {
3616 $update_map[ $update['slug'] ] = $update;
3617 }
3618
3619 try {
3620 do_action( 'mainwp_before_plugin_theme_translation_update', 'theme', implode( ',', $slugs ), $site );
3621
3622 $information = MainWP_Connect::fetch_url_authed(
3623 $site,
3624 'upgradeplugintheme',
3625 array(
3626 'type' => 'theme',
3627 'list' => implode( ',', $slugs ),
3628 ),
3629 true
3630 );
3631
3632 do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'theme', implode( ',', $slugs ), $site );
3633
3634 if ( is_array( $information ) && isset( $information['upgrades'] ) ) {
3635 foreach ( $information['upgrades'] as $slug => $result ) {
3636 if ( ! empty( $result ) && isset( $update_map[ $slug ] ) ) {
3637 $updated[] = array(
3638 'site_id' => (int) $site->id,
3639 'site_url' => $site->url,
3640 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3641 'type' => 'theme',
3642 'slug' => $slug,
3643 'name' => $update_map[ $slug ]['name'],
3644 'old_version' => $update_map[ $slug ]['current_version'],
3645 'new_version' => $update_map[ $slug ]['new_version'],
3646 );
3647 }
3648 }
3649 }
3650
3651 if ( is_array( $information ) && isset( $information['upgrades_error'] ) ) {
3652 foreach ( $information['upgrades_error'] as $slug => $error_msg ) {
3653 $errors[] = array(
3654 'site_id' => (int) $site->id,
3655 'site_url' => $site->url,
3656 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3657 'type' => 'theme',
3658 'slug' => $slug,
3659 'code' => 'mainwp_update_failed',
3660 'message' => $error_msg,
3661 );
3662 }
3663 }
3664
3665 // Update stored theme_upgrades to remove updated items.
3666 if ( ! empty( $updated ) ) {
3667 $current_upgrades = ! empty( $site->theme_upgrades ) ? json_decode( $site->theme_upgrades, true ) : array();
3668 if ( is_array( $current_upgrades ) ) {
3669 foreach ( $updated as $u ) {
3670 unset( $current_upgrades[ $u['slug'] ] );
3671 }
3672 MainWP_DB::instance()->update_website_values( $site->id, array( 'theme_upgrades' => wp_json_encode( $current_upgrades ) ) );
3673 }
3674 }
3675 } catch ( \Exception $e ) {
3676 foreach ( $slugs as $slug ) {
3677 $errors[] = array(
3678 'site_id' => (int) $site->id,
3679 'site_url' => $site->url,
3680 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3681 'type' => 'theme',
3682 'slug' => $slug,
3683 'code' => 'mainwp_update_exception',
3684 'message' => $e->getMessage(),
3685 );
3686 }
3687 }
3688
3689 return array(
3690 'updated' => $updated,
3691 'errors' => $errors,
3692 );
3693 }
3694
3695 /**
3696 * Execute translation updates for a site.
3697 *
3698 * @param object $site Site object.
3699 * @param array $updates Array of translation updates.
3700 * @return array Array with 'updated' and 'errors' keys.
3701 */
3702 private static function execute_translation_updates( $site, array $updates ): array { // phpcs:ignore -- NOSONAR - complex method.
3703 $updated = array();
3704 $errors = array();
3705
3706 $slugs = array_map(
3707 function ( $u ) {
3708 return $u['slug'];
3709 },
3710 $updates
3711 );
3712
3713 // Build slug-to-update map.
3714 $update_map = array();
3715 foreach ( $updates as $update ) {
3716 $update_map[ $update['slug'] ] = $update;
3717 }
3718
3719 try {
3720 do_action( 'mainwp_before_plugin_theme_translation_update', 'translation', implode( ',', $slugs ), $site );
3721
3722 $information = MainWP_Connect::fetch_url_authed(
3723 $site,
3724 'upgradetranslation',
3725 array(
3726 'type' => 'translation',
3727 'list' => implode( ',', $slugs ),
3728 ),
3729 true
3730 );
3731
3732 do_action( 'mainwp_after_plugin_theme_translation_update', $information, 'translation', implode( ',', $slugs ), $site );
3733
3734 if ( is_array( $information ) && isset( $information['upgrades'] ) ) {
3735 foreach ( $information['upgrades'] as $slug => $result ) {
3736 if ( ! empty( $result ) && isset( $update_map[ $slug ] ) ) {
3737 $updated[] = array(
3738 'site_id' => (int) $site->id,
3739 'site_url' => $site->url,
3740 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3741 'type' => 'translation',
3742 'slug' => $slug,
3743 'name' => $update_map[ $slug ]['name'],
3744 'old_version' => $update_map[ $slug ]['current_version'],
3745 'new_version' => $update_map[ $slug ]['new_version'],
3746 );
3747 }
3748 }
3749 }
3750
3751 if ( is_array( $information ) && isset( $information['upgrades_error'] ) ) {
3752 foreach ( $information['upgrades_error'] as $slug => $error_msg ) {
3753 $errors[] = array(
3754 'site_id' => (int) $site->id,
3755 'site_url' => $site->url,
3756 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3757 'type' => 'translation',
3758 'slug' => $slug,
3759 'code' => 'mainwp_update_failed',
3760 'message' => $error_msg,
3761 );
3762 }
3763 }
3764
3765 // Update stored translation_upgrades to remove updated items.
3766 if ( ! empty( $updated ) ) {
3767 $current_upgrades = ! empty( $site->translation_upgrades ) ? json_decode( $site->translation_upgrades, true ) : array();
3768 if ( is_array( $current_upgrades ) ) {
3769 // Translation upgrades are stored as indexed array with 'slug' key, not keyed by slug.
3770 $updated_slugs = array();
3771 foreach ( $updated as $u ) {
3772 $updated_slugs[] = $u['slug'];
3773 }
3774
3775 $current_upgrades = array_filter(
3776 $current_upgrades,
3777 function ( $translation ) use ( $updated_slugs ) {
3778 return ! isset( $translation['slug'] ) || ! in_array( $translation['slug'], $updated_slugs, true );
3779 }
3780 );
3781
3782 // Re-index the array.
3783 $current_upgrades = array_values( $current_upgrades );
3784
3785 MainWP_DB::instance()->update_website_values( $site->id, array( 'translation_upgrades' => wp_json_encode( $current_upgrades ) ) );
3786 }
3787 }
3788 } catch ( \Exception $e ) {
3789 foreach ( $slugs as $slug ) {
3790 $errors[] = array(
3791 'site_id' => (int) $site->id,
3792 'site_url' => $site->url,
3793 'site_name' => MainWP_Utility::remove_http_prefix( (string) $site->name, true ),
3794 'type' => 'translation',
3795 'slug' => $slug,
3796 'code' => 'mainwp_update_exception',
3797 'message' => $e->getMessage(),
3798 );
3799 }
3800 }
3801
3802 return array(
3803 'updated' => $updated,
3804 'errors' => $errors,
3805 );
3806 }
3807
3808 /**
3809 * Add an item to the ignored list.
3810 *
3811 * @param object $site Site object.
3812 * @param string $type Update type (core, plugin, theme).
3813 * @param string $slug Item slug.
3814 * @return true|\WP_Error True on success, WP_Error on failure.
3815 */
3816 private static function add_to_ignored_list( $site, string $type, string $slug ) {
3817 $site_id = (int) $site->id;
3818
3819 if ( 'core' === $type ) {
3820 $ignored_info = MainWP_DB::instance()->get_website_option( $site, 'ignored_wp_upgrades' );
3821 $ignored_info = ! empty( $ignored_info ) ? json_decode( $ignored_info, true ) : array();
3822
3823 if ( ! is_array( $ignored_info ) ) {
3824 $ignored_info = array();
3825 }
3826
3827 $ignored_info['ignored_versions'] = array( 'all_versions' );
3828
3829 MainWP_DB::instance()->update_website_option( $site, 'ignored_wp_upgrades', wp_json_encode( $ignored_info ) );
3830
3831 return true;
3832 }
3833
3834 if ( 'plugin' === $type ) {
3835 $ignored_plugins = ! empty( $site->ignored_plugins ) ? json_decode( $site->ignored_plugins, true ) : array();
3836 if ( ! is_array( $ignored_plugins ) ) {
3837 $ignored_plugins = array();
3838 }
3839
3840 /**
3841 * Action: mainwp_before_plugin_ignore
3842 *
3843 * Fires before plugin ignore.
3844 *
3845 * @since 4.1
3846 */
3847 do_action( 'mainwp_before_plugin_ignore', $ignored_plugins, $site );
3848
3849 $ignored_plugins[ $slug ] = array(
3850 'Name' => $slug,
3851 'ignored_versions' => array( 'all_versions' ),
3852 );
3853
3854 $encoded = wp_json_encode( $ignored_plugins );
3855 MainWP_DB::instance()->update_website_values( $site_id, array( 'ignored_plugins' => $encoded ) );
3856 // Keep in-memory site object in sync for multiple operations in one request.
3857 $site->ignored_plugins = $encoded;
3858
3859 /**
3860 * Action: mainwp_after_plugin_ignore
3861 *
3862 * Fires after plugin ignore.
3863 *
3864 * @since 4.1
3865 */
3866 do_action( 'mainwp_after_plugin_ignore', $ignored_plugins, $site );
3867
3868 return true;
3869 }
3870
3871 if ( 'theme' === $type ) {
3872 $ignored_themes = ! empty( $site->ignored_themes ) ? json_decode( $site->ignored_themes, true ) : array();
3873 if ( ! is_array( $ignored_themes ) ) {
3874 $ignored_themes = array();
3875 }
3876
3877 /**
3878 * Action: mainwp_before_theme_ignore
3879 *
3880 * Fires before theme ignore.
3881 *
3882 * @since 4.1
3883 */
3884 do_action( 'mainwp_before_theme_ignore', $ignored_themes, $site );
3885
3886 $ignored_themes[ $slug ] = array(
3887 'Name' => $slug,
3888 'ignored_versions' => array( 'all_versions' ),
3889 );
3890
3891 $encoded = wp_json_encode( $ignored_themes );
3892 MainWP_DB::instance()->update_website_values( $site_id, array( 'ignored_themes' => $encoded ) );
3893 // Keep in-memory site object in sync for multiple operations in one request.
3894 $site->ignored_themes = $encoded;
3895
3896 /**
3897 * Action: mainwp_after_theme_ignore
3898 *
3899 * Fires after theme ignore.
3900 *
3901 * @since 4.1
3902 */
3903 do_action( 'mainwp_after_theme_ignore', $site, $ignored_themes );
3904
3905 return true;
3906 }
3907
3908 return new \WP_Error( 'mainwp_invalid_type', __( 'Invalid update type.', 'mainwp' ) );
3909 }
3910
3911 /**
3912 * Remove an item from the ignored list.
3913 *
3914 * @param object $site Site object.
3915 * @param string $type Update type (core, plugin, theme).
3916 * @param string $slug Item slug.
3917 * @return true|\WP_Error True on success, WP_Error on failure.
3918 */
3919 private static function remove_from_ignored_list( $site, string $type, string $slug ) {
3920 $site_id = (int) $site->id;
3921
3922 if ( 'core' === $type ) {
3923 /**
3924 * Action: mainwp_before_core_unignore
3925 *
3926 * Fires before core unignore.
3927 *
3928 * @since 5.2
3929 */
3930 do_action( 'mainwp_before_core_unignore', array(), $site );
3931
3932 MainWP_DB::instance()->update_website_option( $site, 'ignored_wp_upgrades', wp_json_encode( array() ) );
3933
3934 /**
3935 * Action: mainwp_after_core_unignore
3936 *
3937 * Fires after core unignore.
3938 *
3939 * @since 5.2
3940 */
3941 do_action( 'mainwp_after_core_unignore', array(), $site );
3942
3943 return true;
3944 }
3945
3946 if ( 'plugin' === $type ) {
3947 $ignored_plugins = ! empty( $site->ignored_plugins ) ? json_decode( $site->ignored_plugins, true ) : array();
3948 if ( ! is_array( $ignored_plugins ) ) {
3949 $ignored_plugins = array();
3950 }
3951
3952 /**
3953 * Action: mainwp_before_plugin_unignore
3954 *
3955 * Fires before plugin unignore.
3956 *
3957 * @since 4.1
3958 */
3959 do_action( 'mainwp_before_plugin_unignore', $ignored_plugins, $site );
3960
3961 unset( $ignored_plugins[ $slug ] );
3962
3963 $encoded = wp_json_encode( $ignored_plugins );
3964 MainWP_DB::instance()->update_website_values( $site_id, array( 'ignored_plugins' => $encoded ) );
3965 // Keep in-memory site object in sync for multiple operations in one request.
3966 $site->ignored_plugins = $encoded;
3967
3968 /**
3969 * Action: mainwp_after_plugin_unignore
3970 *
3971 * Fires after plugin unignore.
3972 *
3973 * @since 4.1
3974 */
3975 do_action( 'mainwp_after_plugin_unignore', $ignored_plugins, $site );
3976
3977 return true;
3978 }
3979
3980 if ( 'theme' === $type ) {
3981 $ignored_themes = ! empty( $site->ignored_themes ) ? json_decode( $site->ignored_themes, true ) : array();
3982 if ( ! is_array( $ignored_themes ) ) {
3983 $ignored_themes = array();
3984 }
3985
3986 /**
3987 * Action: mainwp_before_theme_unignore
3988 *
3989 * Fires before theme unignore.
3990 *
3991 * @since 4.1
3992 */
3993 do_action( 'mainwp_before_theme_unignore', $ignored_themes, $site );
3994
3995 unset( $ignored_themes[ $slug ] );
3996
3997 $encoded = wp_json_encode( $ignored_themes );
3998 MainWP_DB::instance()->update_website_values( $site_id, array( 'ignored_themes' => $encoded ) );
3999 // Keep in-memory site object in sync for multiple operations in one request.
4000 $site->ignored_themes = $encoded;
4001
4002 /**
4003 * Action: mainwp_after_theme_unignore
4004 *
4005 * Fires after theme unignore.
4006 *
4007 * @since 4.1
4008 */
4009 do_action( 'mainwp_after_theme_unignore', $ignored_themes, $site );
4010
4011 return true;
4012 }
4013
4014 return new \WP_Error( 'mainwp_invalid_type', __( 'Invalid update type.', 'mainwp' ) );
4015 }
4016 }
4017