• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wp-graphql / wp-graphql / 18790791685

24 Oct 2025 08:03PM UTC coverage: 83.207% (-1.4%) from 84.575%
18790791685

push

github

actions-user
release: merge develop into master for v2.5.0

2 of 4 new or added lines in 2 files covered. (50.0%)

189 existing lines in 10 files now uncovered.

16143 of 19401 relevant lines covered (83.21%)

257.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Admin/Extensions/Extensions.php
1
<?php
2

3
namespace WPGraphQL\Admin\Extensions;
4

5
use WP_REST_Request;
6
use WP_REST_Response;
7

8
/**
9
 * Class Extensions
10
 *
11
 * @package WPGraphQL\Admin\Extensions
12
 *
13
 * phpcs:disable -- For phpstan type hinting
14
 * @phpstan-import-type ExtensionAuthor from \WPGraphQL\Admin\Extensions\Registry
15
 * @phpstan-import-type Extension from \WPGraphQL\Admin\Extensions\Registry
16
 *
17
 * @phpstan-type PopulatedExtension array{
18
 *   name: non-empty-string,
19
 *   description: non-empty-string,
20
 *   plugin_url: non-empty-string,
21
 *   support_url: non-empty-string,
22
 *   documentation_url: non-empty-string,
23
 *   repo_url?: string,
24
 *   author: ExtensionAuthor,
25
 *   installed: bool,
26
 *   active: bool,
27
 *   settings_path?: string,
28
 *   settings_url?: string,
29
 * }
30
 * phpcs:enable
31
 */
32
final class Extensions {
33
        /**
34
         * The list of extensions.
35
         *
36
         * Filtered by `graphql_get_extensions`.
37
         *
38
         * @var PopulatedExtension[]
39
         */
40
        private array $extensions;
41

42
        /**
43
         * Initialize Extensions functionality for WPGraphQL.
44
         */
45
        public function init(): void {
×
46
                add_action( 'admin_menu', [ $this, 'register_admin_page' ] );
×
47
                add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
×
48
                add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
×
49
        }
50

51
        /**
52
         * Register the admin page for extensions.
53
         */
54
        public function register_admin_page(): void {
×
55
                add_submenu_page(
×
56
                        'graphiql-ide',
×
57
                        __( 'WPGraphQL Extensions', 'wp-graphql' ),
×
58
                        __( 'Extensions', 'wp-graphql' ),
×
59
                        'manage_options',
×
60
                        'wpgraphql-extensions',
×
61
                        [ $this, 'render_admin_page' ]
×
62
                );
×
63
        }
64

65
        /**
66
         * Render the admin page content.
67
         */
68
        public function render_admin_page(): void {
×
69
                echo '<div class="wrap">';
×
70
                echo '<h1>' . esc_html( get_admin_page_title() ) . '</h1>';
×
71
                echo '<div style="margin-top: 20px;" id="wpgraphql-extensions"></div>';
×
72
                echo '</div>';
×
73
        }
74

75
        /**
76
         * Enqueue the necessary scripts and styles for the extensions page.
77
         *
78
         * @param string $hook_suffix The current admin page.
79
         */
80
        public function enqueue_scripts( $hook_suffix ): void {
×
81
                if ( 'graphql_page_wpgraphql-extensions' !== $hook_suffix ) {
×
82
                        return;
×
83
                }
84

85
                $asset_file = include WPGRAPHQL_PLUGIN_DIR . 'build/extensions.asset.php';
×
86

87
                wp_enqueue_style(
×
88
                        'wpgraphql-extensions',
×
89
                        WPGRAPHQL_PLUGIN_URL . 'build/extensions.css',
×
90
                        [ 'wp-components' ],
×
91
                        $asset_file['version']
×
92
                );
×
93

94
                wp_enqueue_script(
×
95
                        'wpgraphql-extensions',
×
96
                        WPGRAPHQL_PLUGIN_URL . 'build/extensions.js',
×
97
                        $asset_file['dependencies'],
×
98
                        $asset_file['version'],
×
99
                        true
×
100
                );
×
101

102
                wp_localize_script(
×
103
                        'wpgraphql-extensions',
×
104
                        'wpgraphqlExtensions',
×
105
                        [
×
106
                                'nonce'            => wp_create_nonce( 'wp_rest' ),
×
107
                                'graphqlEndpoint'  => trailingslashit( site_url() ) . 'index.php?' . graphql_get_endpoint(),
×
108
                                'extensions'       => $this->get_extensions(),
×
109
                                'pluginsInstalled' => $this->get_installed_plugins(),
×
110
                        ]
×
111
                );
×
112
        }
113

114
        /**
115
         * Register custom REST API routes.
116
         */
117
        public function register_rest_routes(): void {
×
118
                register_rest_route(
×
119
                        'wp/v2',
×
120
                        '/plugins/(?P<plugin>.+)',
×
121
                        [
×
122
                                'methods'             => 'PUT',
×
123
                                'callback'            => [ $this, 'activate_plugin' ],
×
124
                                'permission_callback' => static function () {
×
125
                                        return current_user_can( 'activate_plugins' );
×
126
                                },
×
127
                                'args'                => [
×
128
                                        'plugin' => [
×
129
                                                'required'          => true,
×
130
                                                'validate_callback' => static function ( $param, $request, $key ) {
×
131
                                                        return is_string( $param );
×
132
                                                },
×
133
                                        ],
×
134
                                ],
×
135
                        ]
×
136
                );
×
137
        }
138

139
        /**
140
         * Activate a plugin.
141
         *
142
         * @param \WP_REST_Request<array{plugin:string}> $request The REST request.
143
         *
144
         * @return \WP_REST_Response The REST response.
145
         */
146
        public function activate_plugin( WP_REST_Request $request ): WP_REST_Response {
×
147
                $plugin = (string) $request->get_param( 'plugin' );
×
148
                $result = activate_plugin( $plugin );
×
149

150
                if ( is_wp_error( $result ) ) {
×
151
                        return new WP_REST_Response(
×
152
                                [
×
153
                                        'status'  => 'error',
×
154
                                        'message' => $result->get_error_message(),
×
155
                                ],
×
156
                                500
×
157
                        );
×
158
                }
159

160
                return new WP_REST_Response(
×
161
                        [
×
162
                                'status' => 'active',
×
163
                                'plugin' => $plugin,
×
164
                        ],
×
165
                        200
×
166
                );
×
167
        }
168

169
        /**
170
         * Get the list of installed plugins
171
         *
172
         * @return array<string,array{
173
         *  is_active: bool,
174
         *  name: string,
175
         *  description: string,
176
         *  author: string,
177
         * }> List of installed plugins, keyed by the plugin slug.
178
         */
179
        private function get_installed_plugins(): array {
×
180
                if ( ! function_exists( 'get_plugins' ) ) {
×
UNCOV
181
                        require_once ABSPATH . 'wp-admin/includes/plugin.php';
×
182
                }
183

UNCOV
184
                $plugins           = get_plugins();
×
185
                $active_plugins    = get_option( 'active_plugins' );
×
186
                $installed_plugins = [];
×
187

UNCOV
188
                foreach ( $plugins as $plugin_path => $plugin_info ) {
×
189
                        $slug = dirname( $plugin_path );
×
190

UNCOV
191
                        $installed_plugins[ $slug ] = [
×
192
                                'is_active'   => in_array( $plugin_path, $active_plugins, true ),
×
193
                                'name'        => $plugin_info['Name'],
×
194
                                'description' => $plugin_info['Description'],
×
195
                                'author'      => $plugin_info['Author'],
×
196
                        ];
×
197
                }
198

UNCOV
199
                return $installed_plugins;
×
200
        }
201

202
        /**
203
         * Sanitizes extension values before they are used.
204
         *
205
         * @param array<string,mixed> $extension The extension to sanitize.
206
         * @return array{
207
         *  name: string|null,
208
         *  description: string|null,
209
         *  plugin_url: string|null,
210
         *  support_url: string|null,
211
         *  documentation_url: string|null,
212
         *  repo_url: string|null,
213
         *  author: array{
214
         *    name: string|null,
215
         *    homepage: string|null,
216
         *  },
217
         * }
218
         */
UNCOV
219
        private function sanitize_extension( array $extension ): array {
×
220
                return [
×
221
                        'name'              => ! empty( $extension['name'] ) ? sanitize_text_field( $extension['name'] ) : null,
×
222
                        'description'       => ! empty( $extension['description'] ) ? sanitize_text_field( $extension['description'] ) : null,
×
223
                        'plugin_url'        => ! empty( $extension['plugin_url'] ) ? esc_url_raw( $extension['plugin_url'] ) : null,
×
224
                        'support_url'       => ! empty( $extension['support_url'] ) ? esc_url_raw( $extension['support_url'] ) : null,
×
225
                        'documentation_url' => ! empty( $extension['documentation_url'] ) ? esc_url_raw( $extension['documentation_url'] ) : null,
×
226
                        'repo_url'          => ! empty( $extension['repo_url'] ) ? esc_url_raw( $extension['repo_url'] ) : null,
×
227
                        'author'            => [
×
228
                                'name'     => ! empty( $extension['author']['name'] ) ? sanitize_text_field( $extension['author']['name'] ) : null,
×
229
                                'homepage' => ! empty( $extension['author']['homepage'] ) ? esc_url_raw( $extension['author']['homepage'] ) : null,
×
230
                        ],
×
231
                ];
×
232
        }
233

234
        /**
235
         * Validate an extension.
236
         *
237
         * Sanitization ensures that the values are correctly types, so we just need to check if the required fields are present.
238
         *
239
         * @param array<string,mixed> $extension The extension to validate.
240
         *
241
         * @return true|\WP_Error True if the extension is valid, otherwise an error.
242
         *
243
         * @phpstan-assert-if-true Extension $extension
244
         */
UNCOV
245
        public function is_valid_extension( array $extension ) {
×
246
                $error_code = 'invalid_extension';
×
247
                // translators: First placeholder is the extension name. Second placeholder is the property that is missing from the extension.
UNCOV
248
                $error_message = __( 'Invalid extension %1$s is missing a valid value for %2$s.', 'wp-graphql' );
×
249

250
                // First handle the name field, since we'll use it in other error messages.
UNCOV
251
                if ( empty( $extension['name'] ) ) {
×
252
                        return new \WP_Error( $error_code, esc_html__( 'Invalid extension. All extensions must have a `name`.', 'wp-graphql' ) );
×
253
                }
254

255
                // Handle the Top-Level fields.
UNCOV
256
                $required_fields = [
×
257
                        'description',
×
258
                        'plugin_url',
×
259
                        'support_url',
×
260
                        'documentation_url',
×
261
                ];
×
262
                foreach ( $required_fields as $property ) {
×
263
                        if ( empty( $extension[ $property ] ) ) {
×
264
                                return new \WP_Error(
×
265
                                        $error_code,
×
266
                                        sprintf( $error_message, $extension['name'], $property )
×
267
                                );
×
268
                        }
269
                }
270

271
                // Ensure Author has the required name field.
UNCOV
272
                if ( empty( $extension['author']['name'] ) ) {
×
273
                        return new \WP_Error(
×
274
                                $error_code,
×
275
                                sprintf( $error_message, $extension['name'], 'author.name' )
×
276
                        );
×
277
                }
278

UNCOV
279
                return true;
×
280
        }
281

282
        /**
283
         * Populate the extensions list with installation data.
284
         *
285
         * @param Extension[] $extensions The extensions to populate.
286
         *
287
         * @return PopulatedExtension[] The populated extensions.
288
         */
UNCOV
289
        private function populate_installation_data( $extensions ): array {
×
290
                $installed_plugins = $this->get_installed_plugins();
×
291

UNCOV
292
                $populated_extensions = [];
×
293

UNCOV
294
                foreach ( $extensions as $extension ) {
×
295
                        $slug                   = basename( rtrim( $extension['plugin_url'], '/' ) );
×
296
                        $extension['installed'] = false;
×
297
                        $extension['active']    = false;
×
298

299
                        // If the plugin is installed, populate the installation data.
UNCOV
300
                        if ( isset( $installed_plugins[ $slug ] ) ) {
×
301
                                $extension['installed'] = true;
×
302
                                $extension['active']    = $installed_plugins[ $slug ]['is_active'];
×
303

UNCOV
304
                                if ( ! empty( $installed_plugins[ $slug ]['author'] ) ) {
×
305
                                        $extension['author']['name'] = $installed_plugins[ $slug ]['author'];
×
306
                                }
307
                        }
308

309
                        // @todo Where does this come from?
UNCOV
310
                        if ( isset( $extension['settings_path'] ) && true === $extension['active'] ) {
×
311
                                $extension['settings_url'] = is_multisite() && is_network_admin()
×
312
                                        ? network_admin_url( $extension['settings_path'] )
×
313
                                        : admin_url( $extension['settings_path'] );
×
314
                        }
315

UNCOV
316
                        $populated_extensions[] = $extension;
×
317
                }
318

319
                /**
320
                 * Sort the extensions by the following criteria:
321
                 * 1. Plugins grouped by WordPress.org plugins first, non WordPress.org plugins after
322
                 * 2. Sort by plugin name in alphabetical order within the above groups, prioritizing "WPGraphQL" authored plugins
323
                 */
UNCOV
324
                usort(
×
325
                        $populated_extensions,
×
326
                        static function ( $a, $b ) {
×
327
                                if ( false !== strpos( $a['plugin_url'], 'wordpress.org' ) && false === strpos( $b['plugin_url'], 'wordpress.org' ) ) {
×
328
                                        return -1;
×
329
                                }
UNCOV
330
                                if ( false === strpos( $a['plugin_url'], 'wordpress.org' ) && false !== strpos( $b['plugin_url'], 'wordpress.org' ) ) {
×
331
                                        return 1;
×
332
                                }
UNCOV
333
                                if ( ! empty( $a['author']['name'] ) && ( 'WPGraphQL' === $a['author']['name'] && ( ! empty( $b['author']['name'] ) && 'WPGraphQL' !== $b['author']['name'] ) ) ) {
×
334
                                        return -1;
×
335
                                }
UNCOV
336
                                if ( ! empty( $a['author']['name'] ) && 'WPGraphQL' !== $a['author']['name'] && ( ! empty( $b['author']['name'] ) && 'WPGraphQL' === $b['author']['name'] ) ) {
×
337
                                        return 1;
×
338
                                }
UNCOV
339
                                return strcasecmp( $a['name'], $b['name'] );
×
340
                        }
×
341
                );
×
342

UNCOV
343
                return $populated_extensions;
×
344
        }
345

346
        /**
347
         * Get the list of WPGraphQL extensions.
348
         *
349
         * @return PopulatedExtension[] The list of extensions.
350
         */
UNCOV
351
        public function get_extensions(): array {
×
352
                if ( ! isset( $this->extensions ) ) {
×
353
                        // @todo Replace with a call to the WPGraphQL server.
UNCOV
354
                        $extensions = Registry::get_extensions();
×
355

356
                        /**
357
                         * Filter the list of extensions, allowing other plugins to add or remove extensions.
358
                         *
359
                         * @see Admin\Extensions\Registry::get_extensions() for the correct format of the extensions.
360
                         *
361
                         * @param array<string,Extension> $extensions The list of extensions.
362
                         */
UNCOV
363
                        $extensions = apply_filters( 'graphql_get_extensions', $extensions );
×
364

UNCOV
365
                        $valid_extensions = [];
×
366
                        foreach ( $extensions as $extension ) {
×
367
                                $sanitized = $this->sanitize_extension( $extension );
×
368

UNCOV
369
                                if ( true === $this->is_valid_extension( $sanitized ) ) {
×
370
                                        $valid_extensions[] = $sanitized;
×
371
                                }
372
                        }
373

374
                        // If we have valid extensions, populate the installation data.
UNCOV
375
                        if ( ! empty( $valid_extensions ) ) {
×
376
                                $valid_extensions = $this->populate_installation_data( $valid_extensions );
×
377
                        }
378

UNCOV
379
                        $this->extensions = $valid_extensions;
×
380
                }
381

UNCOV
382
                return $this->extensions;
×
383
        }
384
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc