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

wp-graphql / wp-graphql / 13316763745

13 Feb 2025 08:45PM UTC coverage: 82.712% (-0.3%) from 83.023%
13316763745

push

github

web-flow
Merge pull request #3307 from wp-graphql/release/v2.0.0

release: v2.0.0

195 of 270 new or added lines in 20 files covered. (72.22%)

180 existing lines in 42 files now uncovered.

13836 of 16728 relevant lines covered (82.71%)

299.8 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
 * @phpstan-import-type Extension from \WPGraphQL\Admin\Extensions\Registry
14
 *
15
 * @phpstan-type PopulatedExtension array{
16
 *   name: non-empty-string,
17
 *   description: non-empty-string,
18
 *   plugin_url: non-empty-string,
19
 *   support_url: non-empty-string,
20
 *   documentation_url: non-empty-string,
21
 *   repo_url?: string,
22
 *   author: array{
23
 *     name: non-empty-string,
24
 *     homepage?: string,
25
 *   },
26
 *   installed: bool,
27
 *   active: bool,
28
 *   settings_path?: string,
29
 *   settings_url?: string,
30
 * }
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 $extensions;
41

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

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

69
        /**
70
         * Render the admin page content.
71
         *
72
         * @return void
73
         */
UNCOV
74
        public function render_admin_page() {
×
75
                echo '<div class="wrap">';
×
76
                echo '<h1>' . esc_html( get_admin_page_title() ) . '</h1>';
×
77
                echo '<div style="margin-top: 20px;" id="wpgraphql-extensions"></div>';
×
78
                echo '</div>';
×
79
        }
80

81
        /**
82
         * Enqueue the necessary scripts and styles for the extensions page.
83
         *
84
         * @param string $hook_suffix The current admin page.
85
         *
86
         * @return void
87
         */
UNCOV
88
        public function enqueue_scripts( $hook_suffix ) {
×
89
                if ( 'graphql_page_wpgraphql-extensions' !== $hook_suffix ) {
×
90
                        return;
×
91
                }
92

93
                $asset_file = include WPGRAPHQL_PLUGIN_DIR . 'build/extensions.asset.php';
×
94

95
                wp_enqueue_style(
×
96
                        'wpgraphql-extensions',
×
97
                        WPGRAPHQL_PLUGIN_URL . 'build/extensions.css',
×
98
                        [ 'wp-components' ],
×
99
                        $asset_file['version']
×
100
                );
×
101

102
                wp_enqueue_script(
×
103
                        'wpgraphql-extensions',
×
104
                        WPGRAPHQL_PLUGIN_URL . 'build/extensions.js',
×
105
                        $asset_file['dependencies'],
×
106
                        $asset_file['version'],
×
107
                        true
×
108
                );
×
109

110
                wp_localize_script(
×
111
                        'wpgraphql-extensions',
×
112
                        'wpgraphqlExtensions',
×
113
                        [
×
114
                                'nonce'            => wp_create_nonce( 'wp_rest' ),
×
115
                                'graphqlEndpoint'  => trailingslashit( site_url() ) . 'index.php?' . graphql_get_endpoint(),
×
116
                                'extensions'       => $this->get_extensions(),
×
117
                                'pluginsInstalled' => $this->get_installed_plugins(),
×
118
                        ]
×
119
                );
×
120
        }
121

122
        /**
123
         * Register custom REST API routes.
124
         *
125
         * @return void
126
         */
UNCOV
127
        public function register_rest_routes() {
×
128
                register_rest_route(
×
129
                        'wp/v2',
×
130
                        '/plugins/(?P<plugin>.+)',
×
131
                        [
×
132
                                'methods'             => 'PUT',
×
133
                                'callback'            => [ $this, 'activate_plugin' ],
×
134
                                'permission_callback' => static function () {
×
135
                                        return current_user_can( 'activate_plugins' );
×
136
                                },
×
137
                                'args'                => [
×
138
                                        'plugin' => [
×
139
                                                'required'          => true,
×
140
                                                'validate_callback' => static function ( $param, $request, $key ) {
×
141
                                                        return is_string( $param );
×
142
                                                },
×
143
                                        ],
×
144
                                ],
×
145
                        ]
×
146
                );
×
147
        }
148

149
        /**
150
         * Activate a plugin.
151
         *
152
         * @param \WP_REST_Request<array<string, mixed>> $request The REST request.
153
         * @return \WP_REST_Response The REST response.
154
         */
UNCOV
155
        public function activate_plugin( WP_REST_Request $request ): WP_REST_Response {
×
156
                $plugin = $request->get_param( 'plugin' );
×
157
                $result = activate_plugin( $plugin );
×
158

159
                if ( is_wp_error( $result ) ) {
×
160
                        return new WP_REST_Response(
×
161
                                [
×
162
                                        'status'  => 'error',
×
163
                                        'message' => $result->get_error_message(),
×
164
                                ],
×
165
                                500
×
166
                        );
×
167
                }
168

169
                return new WP_REST_Response(
×
170
                        [
×
171
                                'status' => 'active',
×
172
                                'plugin' => $plugin,
×
173
                        ],
×
174
                        200
×
175
                );
×
176
        }
177

178
        /**
179
         * Get the list of installed plugins.
180
         *
181
         * @return array<string, array<string, mixed>> List of installed plugins.
182
         */
UNCOV
183
        private function get_installed_plugins() {
×
184
                if ( ! function_exists( 'get_plugins' ) ) {
×
185
                        // @phpstan-ignore requireOnce.fileNotFound
UNCOV
186
                        require_once ABSPATH . 'wp-admin/includes/plugin.php';
×
187
                }
188

189
                $plugins           = get_plugins();
×
190
                $active_plugins    = get_option( 'active_plugins' );
×
191
                $installed_plugins = [];
×
192

193
                foreach ( $plugins as $plugin_path => $plugin_info ) {
×
194
                        $slug = dirname( $plugin_path );
×
195

196
                        $installed_plugins[ $slug ] = [
×
197
                                'is_active'   => in_array( $plugin_path, $active_plugins, true ),
×
198
                                'name'        => $plugin_info['Name'],
×
199
                                'description' => $plugin_info['Description'],
×
200
                                'author'      => $plugin_info['Author'],
×
201
                        ];
×
202
                }
203

204
                return $installed_plugins;
×
205
        }
206

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

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

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

249
                // Handle the Top-Level fields.
250
                $required_fields = [
×
251
                        'description',
×
252
                        'plugin_url',
×
253
                        'support_url',
×
254
                        'documentation_url',
×
255
                ];
×
256
                foreach ( $required_fields as $property ) {
×
257
                        if ( empty( $extension[ $property ] ) ) {
×
258
                                return new \WP_Error(
×
259
                                        $error_code,
×
260
                                        sprintf( $error_message, $extension['name'], $property )
×
261
                                );
×
262
                        }
263
                }
264

265
                // Ensure Author has the required name field.
266
                if ( empty( $extension['author']['name'] ) ) {
×
267
                        return new \WP_Error(
×
268
                                $error_code,
×
269
                                sprintf( $error_message, $extension['name'], 'author.name' )
×
270
                        );
×
271
                }
272

273
                return true;
×
274
        }
275

276
        /**
277
         * Populate the extensions list with installation data.
278
         *
279
         * @param Extension[] $extensions The extensions to populate.
280
         *
281
         * @return PopulatedExtension[] The populated extensions.
282
         */
UNCOV
283
        private function populate_installation_data( $extensions ) {
×
284
                $installed_plugins = $this->get_installed_plugins();
×
285

286
                $populated_extensions = [];
×
287

288
                foreach ( $extensions as $extension ) {
×
289
                        $slug                   = basename( rtrim( $extension['plugin_url'], '/' ) );
×
290
                        $extension['installed'] = false;
×
291
                        $extension['active']    = false;
×
292

293
                        // If the plugin is installed, populate the installation data.
294
                        if ( isset( $installed_plugins[ $slug ] ) ) {
×
295
                                $extension['installed'] = true;
×
296
                                $extension['active']    = $installed_plugins[ $slug ]['is_active'];
×
297
                                $extension['author']    = $installed_plugins[ $slug ]['author'];
×
298
                        }
299

300
                        // @todo Where does this come from?
301
                        if ( isset( $extension['settings_path'] ) && true === $extension['active'] ) {
×
302
                                $extension['settings_url'] = is_multisite() && is_network_admin()
×
303
                                        ? network_admin_url( $extension['settings_path'] )
×
304
                                        : admin_url( $extension['settings_path'] );
×
305
                        }
306

307
                        $populated_extensions[] = $extension;
×
308
                }
309

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

334
                return $populated_extensions;
×
335
        }
336

337
        /**
338
         * Get the list of WPGraphQL extensions.
339
         *
340
         * @return PopulatedExtension[] The list of extensions.
341
         */
UNCOV
342
        public function get_extensions(): array {
×
343
                if ( ! isset( $this->extensions ) ) {
×
344
                        // @todo Replace with a call to the WPGraphQL server.
345
                        $extensions = Registry::get_extensions();
×
346

347
                        /**
348
                         * Filter the list of extensions, allowing other plugins to add or remove extensions.
349
                         *
350
                         * @see Admin\Extensions\Registry::get_extensions() for the correct format of the extensions.
351
                         *
352
                         * @param array<string,mixed> $extensions The list of extensions.
353
                         */
354
                        $extensions = apply_filters( 'graphql_get_extensions', $extensions );
×
355

356
                        $valid_extensions = [];
×
357
                        foreach ( $extensions as $extension ) {
×
358
                                $sanitized = $this->sanitize_extension( $extension );
×
359

360
                                if ( true === $this->is_valid_extension( $sanitized ) ) {
×
361
                                        $valid_extensions[] = $sanitized;
×
362
                                }
363
                        }
364

365
                        // If we have valid extensions, populate the installation data.
366
                        if ( ! empty( $valid_extensions ) ) {
×
367
                                $valid_extensions = $this->populate_installation_data( $valid_extensions );
×
368
                        }
369

370
                        $this->extensions = $valid_extensions;
×
371
                }
372

373
                return $this->extensions;
×
374
        }
375
}
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

© 2026 Coveralls, Inc