PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
extendify / app / Mcp / Maintenance.php

Maintenance.php in Extendify 3.2.2, at app/Mcp/Maintenance.php

544 lines 19.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The maintenance work no core REST route offers.
5 */
6
7 namespace Extendify\Mcp;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 /**
12 * Core's plugin routes install, activate and delete but never update, and its
13 * theme routes only read. Each operation here checks the capability core's own
14 * screen checks, then calls what that screen calls.
15 */
16 class Maintenance
17 {
18 /**
19 * Each already answers as its own field, with more than a status.
20 */
21 // phpcs:disable PSR12.Properties.ConstantVisibility.NotFound
22 const NAMED_CHECKS = ['php_version', 'scheduled_events'];
23 // phpcs:enable PSR12.Properties.ConstantVisibility.NotFound
24
25 /**
26 * @param array $slugs - Plugins to consider, or [] for every one.
27 * @return array - Each plugin with an update waiting.
28 */
29 public static function pluginUpdates(array $slugs = [])
30 {
31 self::loadAdmin();
32 \wp_update_plugins();
33 $installed = \get_plugins();
34
35 $items = [];
36 foreach (self::updates('update_plugins') as $file => $update) {
37 $slug = preg_replace('/\.php$/', '', $file);
38 $named = !$slugs || in_array($slug, $slugs, true) || in_array($file, $slugs, true);
39 if (!isset($installed[$file]) || !$named) {
40 continue;
41 }
42
43 $items[] = [
44 'slug' => $slug,
45 'name' => \wp_specialchars_decode((string) ($installed[$file]['Name'] ?? ''), ENT_QUOTES),
46 'version' => (string) ($installed[$file]['Version'] ?? ''),
47 'new_version' => (string) (((array) $update)['new_version'] ?? ''),
48 ];
49 }
50
51 return $items;
52 }
53
54 /**
55 * @param array $stylesheets - Themes to consider, or [] for every one.
56 * @return array - Each theme with an update waiting.
57 */
58 public static function themeUpdates(array $stylesheets = [])
59 {
60 self::loadAdmin();
61 \wp_update_themes();
62 $installed = \wp_get_themes();
63
64 $items = [];
65 foreach (self::updates('update_themes') as $stylesheet => $update) {
66 $named = !$stylesheets || in_array($stylesheet, $stylesheets, true);
67 if (!isset($installed[$stylesheet]) || !$named) {
68 continue;
69 }
70
71 $items[] = [
72 'stylesheet' => $stylesheet,
73 'name' => \wp_specialchars_decode((string) $installed[$stylesheet]->get('Name'), ENT_QUOTES),
74 'version' => (string) $installed[$stylesheet]->get('Version'),
75 'new_version' => (string) (((array) $update)['new_version'] ?? ''),
76 ];
77 }
78
79 return $items;
80 }
81
82 /**
83 * @return array|null - The versions WordPress would move between, or null when it is current.
84 */
85 public static function coreUpdate()
86 {
87 self::loadAdmin();
88 \wp_version_check();
89 foreach ((array) \get_core_updates() as $update) {
90 if (is_object($update) && $update->response === 'upgrade') {
91 return [
92 'from' => $GLOBALS['wp_version'],
93 'to' => (string) $update->current,
94 'locale' => (string) $update->locale,
95 ];
96 }
97 }
98
99 return null;
100 }
101
102 /**
103 * @param array $exclude - Stylesheets to keep regardless.
104 * @param boolean $keepDefault - Whether the newest bundled theme stays as a fallback.
105 * @return array
106 */
107 public static function inactiveThemes(array $exclude, $keepDefault)
108 {
109 $kept = array_merge([\get_stylesheet(), \get_template()], $exclude);
110 $default = $keepDefault ? \WP_Theme::get_core_default_theme() : false;
111 if ($default) {
112 $kept[] = $default->get_stylesheet();
113 }
114
115 $items = [];
116 foreach (\wp_get_themes() as $stylesheet => $theme) {
117 if (in_array($stylesheet, $kept, true)) {
118 continue;
119 }
120
121 $items[] = [
122 'stylesheet' => $stylesheet,
123 'name' => \wp_specialchars_decode((string) $theme->get('Name'), ENT_QUOTES),
124 'version' => (string) $theme->get('Version'),
125 ];
126 }
127
128 return $items;
129 }
130
131 /**
132 * @param string $stylesheet - The theme to delete.
133 * @return string|null - Why it was not deleted, or null when it was.
134 */
135 public static function deleteTheme($stylesheet)
136 {
137 if (!\current_user_can('delete_themes')) {
138 return 'This user may not delete themes.';
139 }
140
141 self::loadAdmin();
142 // Without file access delete_theme() prints a credentials form and exits mid-request.
143 ob_start();
144 $credentials = \request_filesystem_credentials('');
145 ob_end_clean();
146 if ($credentials === false || !\WP_Filesystem($credentials)) {
147 return 'The theme files could not be reached.';
148 }
149
150 $result = \delete_theme($stylesheet);
151 if (\is_wp_error($result)) {
152 return $result->get_error_message();
153 }
154
155 return $result ? null : 'The theme files could not be reached.';
156 }
157
158 /**
159 * @param boolean $enabled - Whether the named plugins and themes update on their own.
160 * @param array $plugins - Plugin slugs.
161 * @param array $themes - Theme stylesheets.
162 * @param boolean $all - Whether every installed plugin and theme is meant.
163 * @return array|\WP_Error
164 */
165 public static function setAutoUpdates($enabled, array $plugins, array $themes, $all)
166 {
167 self::loadAdmin();
168 $installed = array_keys(\get_plugins());
169 $stylesheets = array_keys(\wp_get_themes());
170 $unknown = [];
171 if ($all) {
172 $files = $installed;
173 $chosen = $stylesheets;
174 } else {
175 $files = [];
176 foreach ($plugins as $slug) {
177 $file = in_array($slug . '.php', $installed, true) ? $slug . '.php' : null;
178 $file ? $files[] = $file : $unknown[] = $slug;
179 }
180
181 $chosen = array_values(array_intersect($themes, $stylesheets));
182 $unknown = array_merge($unknown, array_values(array_diff($themes, $stylesheets)));
183 }
184
185 if ($files && !\current_user_can('update_plugins')) {
186 return new \WP_Error('extendify_mcp_refused', 'This user may not change how plugins update.');
187 }
188
189 if ($chosen && !\current_user_can('update_themes')) {
190 return new \WP_Error('extendify_mcp_refused', 'This user may not change how themes update.');
191 }
192
193 self::toggle('auto_update_plugins', $files, $enabled);
194 self::toggle('auto_update_themes', $chosen, $enabled);
195
196 return [
197 'plugins' => array_map(function ($file) use ($enabled) {
198 return ['slug' => preg_replace('/\.php$/', '', $file), 'auto_update' => $enabled];
199 }, $files),
200 'themes' => array_map(function ($stylesheet) use ($enabled) {
201 return ['stylesheet' => $stylesheet, 'auto_update' => $enabled];
202 }, $chosen),
203 'unknown' => $unknown,
204 'honored_by_this_site' => [
205 'plugins' => \wp_is_auto_update_enabled_for_type('plugin'),
206 'themes' => \wp_is_auto_update_enabled_for_type('theme'),
207 ],
208 ];
209 }
210
211 /**
212 * @param array|null $ids - Attachment ids asked for, or null for the whole library.
213 * @return array - The image ids to process, and the ids that are not images.
214 */
215 public static function imageIds($ids)
216 {
217 if ($ids === null) {
218 return [
219 'ids' => \get_posts([
220 'post_type' => 'attachment',
221 'post_mime_type' => 'image',
222 'post_status' => 'inherit',
223 'posts_per_page' => -1,
224 'fields' => 'ids',
225 'orderby' => 'ID',
226 'order' => 'ASC',
227 ]),
228 'skipped' => [],
229 ];
230 }
231
232 $found = [];
233 $skipped = [];
234 foreach (array_unique(array_map('intval', $ids)) as $id) {
235 \wp_attachment_is_image($id) ? $found[] = $id : $skipped[] = $id;
236 }
237
238 return ['ids' => $found, 'skipped' => $skipped];
239 }
240
241 /**
242 * @return array|\WP_Error
243 */
244 public static function health()
245 {
246 if (!\current_user_can('view_site_health_checks')) {
247 return new \WP_Error('extendify_mcp_refused', 'This user may not view the site health checks.');
248 }
249
250 self::loadAdmin();
251 require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
252 $health = \WP_Site_Health::get_instance();
253 \wp_update_plugins();
254 \wp_update_themes();
255 $core = self::coreUpdate();
256
257 return [
258 'wordpress' => ['version' => $GLOBALS['wp_version'], 'update_available' => $core ? $core['to'] : null],
259 'php' => ['version' => PHP_VERSION, 'status' => self::check($health->get_test_php_version())['status']],
260 'database' => ['version' => (string) $GLOBALS['wpdb']->db_version()],
261 'memory_limit' => WP_MEMORY_LIMIT,
262 'https' => self::check($health->get_test_https_status()),
263 'cron' => self::check($health->get_test_scheduled_events()),
264 'background_updates' => self::check($health->get_test_background_updates()),
265 'outdated' => ['plugins' => count(\get_plugin_updates()), 'themes' => count(\get_theme_updates())],
266 'auto_updates' => [
267 'plugins' => \wp_is_auto_update_enabled_for_type('plugin'),
268 'themes' => \wp_is_auto_update_enabled_for_type('theme'),
269 ],
270 'checks' => self::checks($health),
271 ];
272 }
273
274 /**
275 * A check's description and actions are dropped: those carry filesystem paths and admin links.
276 *
277 * @param \WP_Site_Health $health - The instance the named checks came from.
278 * @return array - Every check core measures without calling the site itself.
279 */
280 private static function checks($health)
281 {
282 $checks = [];
283 foreach (\WP_Site_Health::get_tests()['direct'] as $id => $test) {
284 $callback = is_string($test['test']) ? [$health, 'get_test_' . $test['test']] : $test['test'];
285
286 // A skip_cron check asks the site to call itself, which a tool call must not wait on.
287 if (!empty($test['skip_cron']) || in_array($id, self::NAMED_CHECKS, true) || !is_callable($callback)) {
288 continue;
289 }
290
291 // Core filters every result it renders, so a plugin's own amendment reaches the model too.
292 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
293 $result = self::check(\apply_filters('site_status_test_result', call_user_func($callback)));
294
295 // Core leaves the status empty when a check reached no verdict, which tells a model nothing.
296 if ($result['status'] !== '') {
297 $checks[$id] = $result;
298 }
299 }
300
301 return $checks;
302 }
303
304 /**
305 * @param string $tool - The tool the job runs for.
306 * @param array $payload - What the job was started with.
307 * @param integer $index - Which step to take.
308 * @return array - The step's result.
309 */
310 public static function step($tool, array $payload, $index)
311 {
312 switch ($tool) {
313 case 'update_plugins':
314 return self::updatePlugin((string) ($payload['plugins'][$index] ?? ''));
315 case 'update_themes':
316 return self::updateTheme((string) ($payload['themes'][$index] ?? ''));
317 case 'update_core':
318 return self::updateCore((string) ($payload['version'] ?? ''), (string) ($payload['locale'] ?? ''));
319 case 'regenerate_thumbnails':
320 return self::regenerate((int) ($payload['ids'][$index] ?? 0), !empty($payload['only_missing']));
321 }
322
323 return ['ok' => false, 'error' => 'Unknown job.'];
324 }
325
326 /**
327 * @param string $file - The plugin file, as get_plugins() keys it.
328 * @return array
329 */
330 private static function updatePlugin($file)
331 {
332 $slug = preg_replace('/\.php$/', '', $file);
333 if (!\current_user_can('update_plugins')) {
334 return ['slug' => $slug, 'ok' => false, 'error' => 'This user may not update plugins.'];
335 }
336
337 self::loadUpgrader();
338 $skin = new \Automatic_Upgrader_Skin();
339 $result = (new \Plugin_Upgrader($skin))->upgrade($file);
340 \wp_clean_plugins_cache(false);
341 if (\is_wp_error($result)) {
342 return ['slug' => $slug, 'ok' => false, 'error' => $result->get_error_message()];
343 }
344
345 if (!$result) {
346 return ['slug' => $slug, 'ok' => false, 'error' => self::unfinished($skin)];
347 }
348
349 return ['slug' => $slug, 'ok' => true, 'version' => (string) (\get_plugins()[$file]['Version'] ?? '')];
350 }
351
352 /**
353 * @param string $stylesheet - The theme directory, as wp_get_themes() keys it.
354 * @return array
355 */
356 private static function updateTheme($stylesheet)
357 {
358 if (!\current_user_can('update_themes')) {
359 return ['stylesheet' => $stylesheet, 'ok' => false, 'error' => 'This user may not update themes.'];
360 }
361
362 self::loadUpgrader();
363 $skin = new \Automatic_Upgrader_Skin();
364 $result = (new \Theme_Upgrader($skin))->upgrade($stylesheet);
365 \wp_clean_themes_cache(false);
366 if (\is_wp_error($result)) {
367 return ['stylesheet' => $stylesheet, 'ok' => false, 'error' => $result->get_error_message()];
368 }
369
370 if (!$result) {
371 return ['stylesheet' => $stylesheet, 'ok' => false, 'error' => self::unfinished($skin)];
372 }
373
374 return [
375 'stylesheet' => $stylesheet,
376 'ok' => true,
377 'version' => (string) \wp_get_theme($stylesheet)->get('Version'),
378 ];
379 }
380
381 /**
382 * @param string $version - The version the preview offered.
383 * @param string $locale - Its locale.
384 * @return array
385 */
386 private static function updateCore($version, $locale)
387 {
388 if (!\current_user_can('update_core')) {
389 return ['ok' => false, 'error' => 'This user may not update WordPress.'];
390 }
391
392 self::loadUpgrader();
393 $update = \find_core_update($version, $locale);
394 if (!$update) {
395 return ['ok' => false, 'error' => 'That update is no longer offered. Run update_core with preview again.'];
396 }
397
398 $skin = new \Automatic_Upgrader_Skin();
399 $result = (new \Core_Upgrader($skin))->upgrade($update);
400 if (\is_wp_error($result)) {
401 return ['ok' => false, 'error' => $result->get_error_message()];
402 }
403
404 if (!$result) {
405 return ['ok' => false, 'error' => self::unfinished($skin)];
406 }
407
408 return ['ok' => true, 'version' => (string) $result];
409 }
410
411 /**
412 * An upgrader answers false, not a WP_Error, when it cannot reach the files.
413 *
414 * @param \Automatic_Upgrader_Skin $skin - The skin the upgrader reported to.
415 * @return string
416 */
417 private static function unfinished(\Automatic_Upgrader_Skin $skin)
418 {
419 $messages = $skin->get_upgrade_messages();
420
421 return $messages ? \wp_strip_all_tags((string) end($messages)) : 'The update did not complete.';
422 }
423
424 /**
425 * @param integer $id - The image attachment.
426 * @param boolean $onlyMissing - Whether sizes already on disk are left alone.
427 * @return array
428 */
429 private static function regenerate($id, $onlyMissing)
430 {
431 if (!\current_user_can('edit_post', $id)) {
432 return ['id' => $id, 'ok' => false, 'error' => 'This user may not edit that image.'];
433 }
434
435 require_once ABSPATH . 'wp-admin/includes/image.php';
436 require_once ABSPATH . 'wp-admin/includes/file.php';
437 require_once ABSPATH . 'wp-admin/includes/media.php';
438 $file = \get_attached_file($id);
439 if (!$file || !file_exists($file)) {
440 return ['id' => $id, 'ok' => false, 'error' => 'The original file is missing.'];
441 }
442
443 if ($onlyMissing) {
444 self::forgetLostSizes($id, dirname($file));
445 $meta = \wp_update_image_subsizes($id);
446 } else {
447 $meta = \wp_generate_attachment_metadata($id, $file);
448 if (is_array($meta) && $meta) {
449 \wp_update_attachment_metadata($id, $meta);
450 }
451 }
452
453 if (\is_wp_error($meta)) {
454 return ['id' => $id, 'ok' => false, 'error' => $meta->get_error_message()];
455 }
456
457 return ['id' => $id, 'ok' => true, 'sizes' => count((array) ($meta['sizes'] ?? []))];
458 }
459
460 /**
461 * wp_update_image_subsizes() trusts the metadata, so a size whose file is gone has to leave it first.
462 *
463 * @param integer $id - The image attachment.
464 * @param string $dir - The directory its files live in.
465 * @return void
466 */
467 private static function forgetLostSizes($id, $dir)
468 {
469 $meta = \wp_get_attachment_metadata($id);
470 if (!is_array($meta) || empty($meta['sizes'])) {
471 return;
472 }
473
474 foreach ((array) $meta['sizes'] as $size => $info) {
475 if (!file_exists($dir . '/' . (string) (((array) $info)['file'] ?? ''))) {
476 unset($meta['sizes'][$size]);
477 }
478 }
479
480 \wp_update_attachment_metadata($id, $meta);
481 }
482
483 /**
484 * @param string $option - auto_update_plugins or auto_update_themes.
485 * @param array $names - The plugin files or stylesheets to change.
486 * @param boolean $enabled - Whether they join the list or leave it.
487 * @return void
488 */
489 private static function toggle($option, array $names, $enabled)
490 {
491 if (!$names) {
492 return;
493 }
494
495 $current = (array) \get_site_option($option, []);
496 $updated = $enabled ? array_merge($current, $names) : array_diff($current, $names);
497 \update_site_option($option, array_values(array_unique($updated)));
498 }
499
500 /**
501 * @param array $test - What a WP_Site_Health check answered.
502 * @return array
503 */
504 private static function check($test)
505 {
506 $test = (array) $test;
507
508 return [
509 'status' => (string) ($test['status'] ?? ''),
510 'label' => \wp_strip_all_tags((string) ($test['label'] ?? '')),
511 ];
512 }
513
514 /**
515 * @param string $transient - update_plugins or update_themes.
516 * @return array
517 */
518 private static function updates($transient)
519 {
520 $updates = \get_site_transient($transient);
521
522 return isset($updates->response) ? (array) $updates->response : [];
523 }
524
525 /**
526 * @return void
527 */
528 private static function loadAdmin()
529 {
530 foreach (['plugin', 'theme', 'update', 'file', 'misc'] as $include) {
531 require_once ABSPATH . 'wp-admin/includes/' . $include . '.php';
532 }
533 }
534
535 /**
536 * @return void
537 */
538 private static function loadUpgrader()
539 {
540 self::loadAdmin();
541 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
542 }
543 }
544