*/ public static function abilities(): array { $slugSchema = ['maxLength' => 200, 'minLength' => 1, 'pattern' => '^[a-z0-9-]+$', 'type' => 'string']; return [ new Ability( 'sarcio/plugins-list', __('Look at the installed plugins', 'sarcio'), Ability::READ, 'The plugins installed on this site: slug, file, name, version, whether it is active and whether an update is available.', ['properties' => [], 'type' => 'object'], static fn (array $args): bool => current_user_can('activate_plugins'), static fn (array $args): array => self::list(), ), new Ability( 'sarcio/plugin-install', __('Install a plugin from WordPress.org', 'sarcio'), Ability::WRITE, 'Install a plugin from the WordPress.org directory by its slug (the last part of its wordpress.org/plugins/ URL). Only directory plugins; refused where the host forbids installs. Does not activate it; use plugin-activate after. Only when the owner asked for a plugin.', ['properties' => ['slug' => $slugSchema], 'required' => ['slug'], 'type' => 'object'], static fn (array $args): bool => current_user_can('install_plugins') && SiteAbilities::fileModsAllowed(), static fn (array $args): array => self::install((string) $args['slug']), static fn (array $args): array => self::installPreview((string) $args['slug']), null, true, ), new Ability( 'sarcio/plugin-activate', __('Turn a plugin on', 'sarcio'), Ability::WRITE, 'Activate an installed plugin by its slug (from plugins-list or just installed).', ['properties' => ['slug' => $slugSchema], 'required' => ['slug'], 'type' => 'object'], static fn (array $args): bool => current_user_can('activate_plugins'), static fn (array $args): array => self::activate((string) $args['slug']), static fn (array $args): array => self::togglePreview((string) $args['slug'], true), static fn (array $before, array $after): array => self::deactivate((string) $before['slug'], false), true, ), new Ability( 'sarcio/plugin-deactivate', __('Turn a plugin off', 'sarcio'), Ability::WRITE, 'Deactivate an active plugin by its slug. Its files and settings stay; the site just stops using it.', ['properties' => ['slug' => $slugSchema], 'required' => ['slug'], 'type' => 'object'], static fn (array $args): bool => current_user_can('activate_plugins'), static fn (array $args): array => self::deactivate((string) $args['slug']), static fn (array $args): array => self::togglePreview((string) $args['slug'], false), static fn (array $before, array $after): array => self::activate((string) $before['slug'], false), ), ]; } private static function load(): void { if (!function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } } /** @return array{file:string,name:string,version:string}|null */ private static function find(string $slug): ?array { self::load(); foreach ((array) get_plugins() as $file => $data) { $folder = dirname((string) $file); $base = $folder === '.' ? basename((string) $file, '.php') : $folder; if ($base === $slug) { return ['file' => (string) $file, 'name' => (string) ($data['Name'] ?? $slug), 'version' => (string) ($data['Version'] ?? '')]; } } return null; } private static function list(): array { self::load(); $updates = get_site_transient('update_plugins'); $available = is_object($updates) && isset($updates->response) && is_array($updates->response) ? $updates->response : []; $out = []; foreach ((array) get_plugins() as $file => $data) { $folder = dirname((string) $file); $out[] = [ 'active' => is_plugin_active((string) $file), 'file' => (string) $file, 'name' => (string) ($data['Name'] ?? ''), 'slug' => $folder === '.' ? basename((string) $file, '.php') : $folder, 'updateAvailable' => isset($available[$file]), 'version' => (string) ($data['Version'] ?? ''), ]; } return ['plugins' => $out]; } /** @return array|array{error:string} */ public static function information(string $slug): array { if (!function_exists('plugins_api')) { require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; } $api = plugins_api('plugin_information', [ 'fields' => ['active_installs' => true, 'rating' => true, 'sections' => false, 'short_description' => true, 'versions' => false], 'slug' => $slug, ]); if (is_wp_error($api) || !is_object($api)) { return ['error' => sprintf( /* translators: %s: the plugin slug. */ __('WordPress.org has no plugin called “%s”.', 'sarcio'), $slug, )]; } return [ 'activeInstalls' => (int) ($api->active_installs ?? 0), 'author' => wp_strip_all_tags((string) ($api->author ?? '')), 'downloadLink' => (string) ($api->download_link ?? ''), 'name' => (string) ($api->name ?? $slug), 'rating' => (float) ($api->rating ?? 0) / 20, 'slug' => $slug, 'summary' => wp_strip_all_tags((string) ($api->short_description ?? '')), 'url' => 'https://wordpress.org/plugins/' . rawurlencode($slug) . '/', 'version' => (string) ($api->version ?? ''), ]; } private static function installPreview(string $slug): array { if (self::find($slug) !== null) { return ['error' => __('That plugin is already installed. Turn it on instead.', 'sarcio')]; } $info = self::information($slug); if (isset($info['error'])) { return $info; } return [ 'after' => $info['name'] . ' ' . $info['version'], 'before' => '', 'plugin' => $info, 'summary' => sprintf( /* translators: 1: the plugin name, 2: its author. */ __('Install %1$s by %2$s from WordPress.org', 'sarcio'), $info['name'], $info['author'], ), ]; } private static function install(string $slug): array { if (self::find($slug) !== null) { return ['error' => __('That plugin is already installed. Turn it on instead.', 'sarcio')]; } $info = self::information($slug); if (isset($info['error'])) { return $info; } require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/misc.php'; $upgrader = new \Plugin_Upgrader(new \WP_Ajax_Upgrader_Skin()); $result = $upgrader->install($info['downloadLink']); if (is_wp_error($result) || !$result) { return ['error' => __('WordPress could not install it. Your host may not allow plugin installs; ask them, or install it under Plugins → Add New.', 'sarcio')]; } $installed = self::find($slug); return [ 'installed' => true, 'journal' => [ 'after' => ['slug' => $slug, 'version' => $installed['version'] ?? $info['version']], 'before' => [], 'summary' => sprintf( /* translators: %s: the plugin name. */ __('Installed %s', 'sarcio'), $info['name'], ), 'target' => 'plugin:' . $slug, ], 'name' => $info['name'], 'slug' => $slug, ]; } private static function togglePreview(string $slug, bool $on): array { $plugin = self::find($slug); if ($plugin === null) { return ['error' => __('That plugin is not installed.', 'sarcio')]; } $active = is_plugin_active($plugin['file']); if ($active === $on) { return ['error' => $on ? __('That plugin is already on.', 'sarcio') : __('That plugin is already off.', 'sarcio')]; } return [ 'after' => $on ? __('On', 'sarcio') : __('Off', 'sarcio'), 'before' => $on ? __('Off', 'sarcio') : __('On', 'sarcio'), 'plugin' => ['name' => $plugin['name'], 'slug' => $slug, 'version' => $plugin['version']], 'summary' => sprintf( $on /* translators: %s: the plugin name. */ ? __('Turn on %s', 'sarcio') /* translators: %s: the plugin name. */ : __('Turn off %s', 'sarcio'), $plugin['name'], ), ]; } private static function activate(string $slug, bool $journal = true): array { $plugin = self::find($slug); if ($plugin === null) { return ['error' => __('That plugin is not installed.', 'sarcio')]; } if (is_plugin_active($plugin['file'])) { return ['error' => __('That plugin is already on.', 'sarcio')]; } $result = activate_plugin($plugin['file']); if (is_wp_error($result)) { return ['error' => sprintf( /* translators: 1: the plugin name, 2: WordPress’s reason. */ __('%1$s could not be turned on: %2$s', 'sarcio'), $plugin['name'], wp_strip_all_tags((string) $result->get_error_message()), )]; } $output = ['active' => true, 'name' => $plugin['name'], 'slug' => $slug]; if ($journal) { $output['journal'] = [ 'after' => ['active' => true, 'slug' => $slug], 'before' => ['active' => false, 'slug' => $slug], 'summary' => sprintf( /* translators: %s: the plugin name. */ __('Turned on %s', 'sarcio'), $plugin['name'], ), 'target' => 'plugin:' . $slug, ]; } return $output; } private static function deactivate(string $slug, bool $journal = true): array { $plugin = self::find($slug); if ($plugin === null) { return ['error' => __('That plugin is not installed.', 'sarcio')]; } if (!is_plugin_active($plugin['file'])) { return ['error' => __('That plugin is already off.', 'sarcio')]; } if (str_starts_with($plugin['file'], 'sarcio/')) { return ['error' => __('Sarcio cannot turn itself off from here. Use the Plugins screen.', 'sarcio')]; } deactivate_plugins($plugin['file']); $output = ['active' => false, 'name' => $plugin['name'], 'slug' => $slug]; if ($journal) { $output['journal'] = [ 'after' => ['active' => false, 'slug' => $slug], 'before' => ['active' => true, 'slug' => $slug], 'summary' => sprintf( /* translators: %s: the plugin name. */ __('Turned off %s', 'sarcio'), $plugin['name'], ), 'target' => 'plugin:' . $slug, ]; } return $output; } }