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

wp-graphql / wp-graphql / 13659882478

04 Mar 2025 05:56PM UTC coverage: 82.702% (-0.01%) from 82.712%
13659882478

push

github

jasonbahl
- fix changelog and readme.txt

13822 of 16713 relevant lines covered (82.7%)

299.99 hits per line

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

93.89
/src/Admin/Updates/UpdateChecker.php
1
<?php
2
/**
3
 * Handle the plugin update checks and notifications.
4
 *
5
 * @internal This class is for internal use only. It may change in the future without warning.
6
 *
7
 * Code is inspired by and adapted from WooCommerce's WC_Plugin_Updates class.
8
 * @see https://github.com/woocommerce/woocommerce/blob/5f04212f8188e0f7b09f6375d1a6c610fac8a631/plugins/woocommerce/includes/admin/plugin-updates/class-wc-plugin-updates.php
9
 *
10
 * @package WPGraphQL\Admin\Updates
11
 */
12

13
namespace WPGraphQL\Admin\Updates;
14

15
/**
16
 * Class UpdateChecker
17
 *
18
 * @internal This class is for internal use only. It may change in the future without warning.
19
 */
20
class UpdateChecker {
21
        /**
22
         * The version header to check for in the plugin file.
23
         */
24
        public const VERSION_HEADER = 'Requires WPGraphQL';
25

26
        /**
27
         * The tested up to header to check for in the plugin file.
28
         */
29
        public const TESTED_UP_TO_HEADER = 'WPGraphQL tested up to';
30

31
        /**
32
         * The current version of the plugin.
33
         *
34
         * @var string
35
         */
36
        public $current_version = WPGRAPHQL_VERSION;
37

38
        /**
39
         * The new version of the available.
40
         *
41
         * @var string
42
         */
43
        public $new_version;
44

45
        /**
46
         * The local cache of _all_ plugins.
47
         *
48
         * @var ?array<string,array<string,mixed>>
49
         */
50
        private $all_plugins;
51

52
        /**
53
         * The array of plugins that use WPGraphQL as a dependency.
54
         *
55
         * @var ?array<string,array<string,mixed>>
56
         */
57
        private $dependents;
58

59
        /**
60
         * The array of plugins that *maybe* use WPGraphQL as a dependency.
61
         *
62
         * @var ?array<string,array<string,mixed>>
63
         */
64
        private $possible_dependents;
65

66
        /**
67
         * The WPGraphQL plugin data object.
68
         *
69
         * @var object
70
         */
71
        private $plugin_data;
72

73

74
        /**
75
         * The release type of the new version of WPGraphQL.
76
         *
77
         * @var 'major'|'minor'|'patch'|'prerelease'|'unknown'
78
         */
79
        private $release_type;
80

81
        /**
82
         * UpdateChecker constructor.
83
         *
84
         * @param object $plugin_data The plugin data object from the update check.
85
         */
86
        public function __construct( $plugin_data ) {
8✔
87
                $this->plugin_data  = $plugin_data;
8✔
88
                $this->new_version  = property_exists( $plugin_data, 'new_version' ) ? $plugin_data->new_version : '';
8✔
89
                $this->release_type = SemVer::get_release_type( $this->current_version, $this->new_version );
8✔
90
        }
91

92
        /**
93
         * Checks whether any untested or incompatible WPGraphQL extensions should prevent an autoupdate.
94
         *
95
         * @param bool $default_value Whether to allow the update by default.
96
         */
97
        public function should_autoupdate( bool $default_value ): bool {
7✔
98
                // If this is a major release, and we have those disabled, don't allow the autoupdate.
99
                if ( 'major' === $this->release_type && ! $this->should_allow_major_autoupdates() ) {
7✔
100
                        return false;
3✔
101
                }
102

103
                // If there are active incompatible plugins, don't allow the update.
104
                $incompatible_plugins = $this->get_incompatible_plugins( $this->new_version, true );
5✔
105

106
                if ( ! empty( $incompatible_plugins ) ) {
5✔
107
                        return false;
1✔
108
                }
109

110
                // If allow untested autoupdates enabled, allow the update.
111
                if ( $this->should_allow_untested_autoupdates() ) {
5✔
112
                        return $default_value;
2✔
113
                }
114

115
                $untested_release_type = $this->get_untested_release_type();
5✔
116
                $untested_plugins      = $this->get_untested_plugins( $untested_release_type );
5✔
117

118
                if ( ! empty( $untested_plugins ) ) {
5✔
119
                        return false;
3✔
120
                }
121

122
                return $default_value;
2✔
123
        }
124
        /**
125
         * Gets a list of plugins that use WPGraphQL as a dependency and are not tested with the current version of WPGraphQL.
126
         *
127
         * @param string $release_type The release type of the current version of WPGraphQL.
128
         *
129
         * @return array<string,array<string,mixed>> The array of untested plugin data.
130
         * @throws \InvalidArgumentException If the WPGraphQL version is invalid.
131
         */
132
        public function get_untested_plugins( string $release_type ): array {
7✔
133
                $version = SemVer::parse( $this->new_version );
7✔
134

135
                if ( null === $version ) {
7✔
136
                        throw new \InvalidArgumentException( esc_html__( 'Invalid WPGraphQL version', 'wp-graphql' ) );
×
137
                }
138

139
                $dependents = array_merge(
7✔
140
                        $this->get_dependents(),
7✔
141
                        $this->get_possible_dependents()
7✔
142
                );
7✔
143

144
                $untested_plugins = [];
7✔
145
                foreach ( $dependents as $file => $plugin ) {
7✔
146
                        // If the plugin doesn't have a version header, it's compatibility is unknown.
147
                        if ( empty( $plugin[ self::TESTED_UP_TO_HEADER ] ) ) {
6✔
148
                                $plugin[ self::TESTED_UP_TO_HEADER ] = __( 'Unknown', 'wp-graphql' );
2✔
149

150
                                $untested_plugins[ $file ] = $plugin;
2✔
151
                                continue;
2✔
152
                        }
153

154
                        // Parse the tested version.
155
                        $tested_version = SemVer::parse( $plugin[ self::TESTED_UP_TO_HEADER ] );
4✔
156
                        if ( null === $tested_version ) {
4✔
157
                                continue;
×
158
                        }
159

160
                        // If the major version is greater, the plugin is untested.
161
                        if ( $version['major'] > $tested_version['major'] ) {
4✔
162
                                $untested_plugins[ $file ] = $plugin;
3✔
163
                                continue;
3✔
164
                        }
165

166
                        // If the minor version is greater, the plugin is untested.
167
                        if ( 'major' !== $release_type && $version['minor'] > $tested_version['minor'] ) {
1✔
168
                                $untested_plugins[ $file ] = $plugin;
×
169
                                continue;
×
170
                        }
171

172
                        // If the patch version is greater, the plugin is untested.
173
                        if ( 'major' !== $release_type && 'minor' !== $release_type && $version['patch'] > $tested_version['patch'] ) {
1✔
174
                                $untested_plugins[ $file ] = $plugin;
×
175
                                continue;
×
176
                        }
177
                }
178

179
                return $untested_plugins;
7✔
180
        }
181

182
        /**
183
         * Get incompatible plugins.
184
         *
185
         * @param string $version The current plugin version.
186
         * @param bool   $active_only Whether to only return active plugins. Default false.
187
         *
188
         * @return array<string,array<string,mixed>> The array of incompatible plugins.
189
         */
190
        public function get_incompatible_plugins( string $version = WPGRAPHQL_VERSION, bool $active_only = false ): array {
6✔
191
                $dependents = $this->get_dependents();
6✔
192
                $plugins    = [];
6✔
193

194
                foreach ( $dependents as $file => $plugin ) {
6✔
195
                        // Skip if the plugin is not active or is not incompatible.
196
                        if ( ! $this->is_incompatible_dependent( $file, $version ) ) {
4✔
197
                                continue;
3✔
198
                        }
199

200
                        // If we only want active plugins, skip if the plugin is not active.
201
                        if ( $active_only && ! is_plugin_active( $file ) ) {
2✔
202
                                continue;
2✔
203
                        }
204

205
                        $plugins[ $file ] = $plugin;
2✔
206
                }
207

208
                return $plugins;
6✔
209
        }
210

211
        /**
212
         * Get the shared modal HTML for the update checkers.
213
         *
214
         * @param array<string,array<string,mixed>> $untested_plugins The untested plugins.
215
         */
216
        public function get_untested_plugins_modal( array $untested_plugins ): string {
2✔
217
                $plugins = array_map(
2✔
218
                        static function ( $plugin ) {
2✔
219
                                return $plugin['Name'];
2✔
220
                        },
2✔
221
                        $untested_plugins
2✔
222
                );
2✔
223

224
                if ( empty( $plugins ) ) {
2✔
225
                        return '';
×
226
                }
227

228
                ob_start();
2✔
229
                ?>
230

231
                <div id="wp-graphql-update-modal">
2✔
232
                        <div class="wp-graphql-update-modal__content">
2✔
233

234
                                <h1><?php esc_html_e( 'Are you sure you\'re ready to update?', 'wp-graphql' ); ?></h1>
2✔
235

236
                                <div class="wp-graphql-update-notice">
237
                                        <?php echo wp_kses_post( $this->get_compatibility_warning_message( $untested_plugins ) ); ?>
2✔
238

239
                                        <?php if ( current_user_can( 'update_plugins' ) ) : ?>
2✔
240
                                                <div class="actions">
×
241
                                                        <a href="#" class="button button-secondary cancel"><?php esc_html_e( 'Cancel', 'wp-graphql' ); ?></a>
×
242
                                                        <a class="button button-primary accept" href="#"><?php esc_html_e( 'Update now', 'wp-graphql' ); ?></a>
×
243
                                                </div>
244
                                        <?php endif ?>
245
                                </div>
2✔
246
                        </div>
2✔
247
                </div>
2✔
248

249
                <?php
2✔
250
                return (string) ob_get_clean();
2✔
251
        }
252

253
        /**
254
         * Outputs the shared modal JS for the update checkers.
255
         *
256
         * @todo WIP.
257
         */
258
        public function modal_js(): void {
2✔
259
                ?>
260
                <script>
2✔
261
                        ( function( $ ) {
2✔
262
                                // Initialize thickbox.
2✔
263
                                tb_init( '.wp-graphql-thickbox' );
2✔
264

265
                                var old_tb_position = false;
2✔
266

267
                                // Make the WC thickboxes look good when opened.
2✔
268
                                $( '.wp-graphql-thickbox' ).on( 'click', function( evt ) {
2✔
269
                                        var $overlay = $( '#TB_overlay' );
2✔
270
                                        if ( ! $overlay.length ) {
2✔
271
                                                $( 'body' ).append( '<div id="TB_overlay"></div><div id="TB_window" class="wp-graphql-update-modal__container"></div>' );
2✔
272
                                        } else {
2✔
273
                                                $( '#TB_window' ).removeClass( 'thickbox-loading' ).addClass( 'wp-graphql-update-modal__container' );
2✔
274
                                        }
2✔
275

276
                                        // WP overrides the tb_position function. We need to use a different tb_position function than that one.
2✔
277
                                        // This is based on the original tb_position.
2✔
278
                                        if ( ! old_tb_position ) {
2✔
279
                                                old_tb_position = tb_position;
2✔
280
                                        }
2✔
281
                                        tb_position = function() {
2✔
282
                                                $( '#TB_window' ).css( { marginLeft: '-' + parseInt( ( TB_WIDTH / 2 ), 10 ) + 'px', width: TB_WIDTH + 'px' } );
2✔
283
                                                $( '#TB_window' ).css( { marginTop: '-' + parseInt( ( TB_HEIGHT / 2 ), 10 ) + 'px' } );
2✔
284
                                        };
2✔
285
                                });
2✔
286

287
                                // Reset tb_position to WP default when modal is closed.
2✔
288
                                $( 'body' ).on( 'thickbox:removed', function() {
2✔
289
                                        if ( old_tb_position ) {
2✔
290
                                                tb_position = old_tb_position;
2✔
291
                                        }
2✔
292
                                });
2✔
293
                        }
2✔
294
                ) ( jQuery );
2✔
295
                </script>
2✔
296
                <?php
2✔
297
        }
298

299
        /**
300
         * Returns whether to allow major plugin autoupdates.
301
         *
302
         * Defaults to false.
303
         *
304
         * @uses 'wpgraphql_enable_major_autoupdates' filter.
305
         */
306
        protected function should_allow_major_autoupdates(): bool {
7✔
307
                /**
308
                 * Filter whether to allow major autoupdates.
309
                 *
310
                 * @param bool   $should_allow    Whether to allow major autoupdates. Defaults to false.
311
                 * @param string $new_version     The new WPGraphQL version number.
312
                 * @param string $current_version The current WPGraphQL version number.
313
                 * @param object $plugin_data     The plugin data object.
314
                 */
315
                return apply_filters( 'wpgraphql_enable_major_autoupdates', false, $this->new_version, $this->current_version, $this->plugin_data );
7✔
316
        }
317

318
        /**
319
         * Returns whether to allow plugin autoupdates when plugin dependencies are untested and might be incompatible.
320
         *
321
         * @uses `wpgraphql_untested_release_type` filter to determine the release type to use when checking for untested plugins.
322
         *
323
         * @uses 'wpgraphql_enable_untested_autoupdates' filter.
324
         */
325
        protected function should_allow_untested_autoupdates(): bool {
5✔
326
                $should_allow = $this->get_untested_release_type() !== $this->release_type;
5✔
327

328
                /**
329
                 * Filter whether to allow autoupdates with untested plugins.
330
                 *
331
                 * @param bool   $should_allow    Whether to allow autoupdates with untested plugins.
332
                 * @param string $release_type    The release type of the current version of WPGraphQL. Either 'major', 'minor', 'patch', or 'prerelease'.
333
                 * @param string $new_version     The new WPGraphQL version number.
334
                 * @param string $current_version The current WPGraphQL version number.
335
                 * @param object $plugin_data     The plugin data object.
336
                 */
337
                return apply_filters( 'wpgraphql_enable_untested_autoupdates', $should_allow, $this->release_type, $this->new_version, $this->current_version, $this->plugin_data );
5✔
338
        }
339

340
        /**
341
         * Gets the release type to use when checking for untested plugins.
342
         *
343
         * @return 'major'|'minor'|'patch'|'prerelease' The release type to use when checking for untested plugins.
344
         */
345
        protected function get_untested_release_type(): string {
5✔
346
                /**
347
                 * Filter the release type to use when checking for untested plugins.
348
                 * This is used to prevent autoupdates when a plugin is untested with the specified channel. I.e. major > minor > patch > prerelease.
349
                 *
350
                 * @param 'major'|'minor'|'patch'|'prerelease' $release_type The release type to use when checking for untested plugins. Defaults to 'major'.
351
                 */
352
                $release_type = (string) apply_filters( 'wpgraphql_untested_release_type', 'major' );
5✔
353

354
                if ( ! in_array( $release_type, [ 'major', 'minor', 'patch', 'prerelease' ], true ) ) {
5✔
355
                        $release_type = 'major';
×
356
                }
357

358
                return $release_type;
5✔
359
        }
360
        /**
361
         * Gets the plugins that use WPGraphQL as a dependency.
362
         *
363
         * @return array<string,array<string,mixed>> The array of plugins that use WPGraphQL as a dependency, keyed by plugin path.
364
         */
365
        protected function get_dependents(): array {
8✔
366
                if ( isset( $this->dependents ) ) {
8✔
367
                        return $this->dependents;
5✔
368
                }
369

370
                $all_plugins = $this->get_all_plugins();
8✔
371
                $plugins     = [];
8✔
372

373
                foreach ( $all_plugins as $plugin_path => $plugin ) {
8✔
374
                        // If they're explicitly using a header, it's a dependent.
375
                        if ( ! $this->is_versioned_dependent( $plugin_path ) && ! $this->is_wpapi_dependent( $plugin_path ) ) {
8✔
376
                                continue;
8✔
377
                        }
378

379
                        $plugins[ $plugin_path ] = $plugin;
6✔
380
                }
381

382
                /**
383
                 * Filters the list of plugins that use WPGraphQL as a dependency.
384
                 *
385
                 * @param array<string,array<string,mixed>> $plugins The array of plugins that use WPGraphQL as a dependency.
386
                 * @param array<string,array<string,mixed>> $all_plugins The array of all plugins.
387
                 */
388
                $this->dependents = apply_filters( 'graphql_get_dependents', $plugins, $all_plugins );
8✔
389

390
                return $this->dependents;
8✔
391
        }
392

393
        /**
394
         * Gets the plugins that *maybe* use WPGraphQL as a dependency.
395
         *
396
         * @return array<string,array<string,mixed>> The array of plugins that maybe use WPGraphQL as a dependency, keyed by plugin path.
397
         */
398
        protected function get_possible_dependents(): array {
7✔
399
                // Bail early if we've already fetched the possible plugins.
400
                if ( isset( $this->possible_dependents ) ) {
7✔
401
                        return $this->possible_dependents;
×
402
                }
403

404
                $all_plugins = $this->get_all_plugins();
7✔
405
                $plugins     = [];
7✔
406

407
                foreach ( $all_plugins as $plugin_path => $plugin ) {
7✔
408
                        // Skip the WPGraphQL plugin.
409
                        if ( 'WPGraphQL' === $plugin['Name'] ) {
7✔
410
                                continue;
7✔
411
                        }
412

413
                        if ( ! $this->is_possible_dependent( $plugin_path ) ) {
7✔
414
                                continue;
7✔
415
                        }
416

417
                        $plugins[ $plugin_path ] = $plugin;
1✔
418
                }
419

420
                /**
421
                 * Filters the list of plugins that use WPGraphQL as a dependency.
422
                 *
423
                 * Can be used to hide false positives or to add additional plugins that may use WPGraphQL as a dependency.
424
                 *
425
                 * @param array<string,array<string,mixed>> $plugins The array of plugins that maybe use WPGraphQL as a dependency.
426
                 * @param array<string,array<string,mixed>> $all_plugins The array of all plugins.
427
                 */
428
                $this->possible_dependents = apply_filters( 'graphql_get_possible_dependents', $plugins, $all_plugins );
7✔
429

430
                return $this->possible_dependents;
7✔
431
        }
432

433
        /**
434
         * Gets all plugins, priming the cache if necessary.
435
         *
436
         * @return array<string,array<string,mixed>> The array of all plugins, keyed by plugin path.
437
         */
438
        private function get_all_plugins(): array {
8✔
439
                if ( ! isset( $this->all_plugins ) ) {
8✔
440
                        $this->all_plugins = get_plugins();
8✔
441
                }
442

443
                return $this->all_plugins;
8✔
444
        }
445

446
        /**
447
         * Checks whether a dependency is incompatible with a specific version of WPGraphQL.
448
         *
449
         * @param string $plugin_path The plugin path used as the key in the plugins array.
450
         * @param string $version     The current version to check against.
451
         */
452
        private function is_incompatible_dependent( string $plugin_path, string $version = WPGRAPHQL_VERSION ): bool {
4✔
453
                $all_plugins = $this->get_all_plugins();
4✔
454
                $plugin_data = $all_plugins[ $plugin_path ] ?? null;
4✔
455

456
                // If the plugin doesn't have a version header, it's compatibility is unknown.
457
                if ( empty( $plugin_data[ self::VERSION_HEADER ] ) ) {
4✔
458
                        return false;
1✔
459
                }
460

461
                // The version is incompatible if the current version is less than the required version.
462
                return version_compare( $version, $plugin_data[ self::VERSION_HEADER ], '<' );
3✔
463
        }
464

465
        /**
466
         * Checks whether the plugin is "possibly" using WPGraphQL as a dependency.
467
         *
468
         * I.e if it's in the plugin name or description.
469
         *
470
         * @param string $plugin_path The plugin path used as the key in the plugins array.
471
         */
472
        private function is_possible_dependent( string $plugin_path ): bool {
7✔
473
                $all_plugins = $this->get_all_plugins();
7✔
474
                $plugin_data = $all_plugins[ $plugin_path ] ?? null;
7✔
475

476
                // Bail early if the plugin doesn't exist.
477
                if ( empty( $plugin_data ) ) {
7✔
478
                        return false;
×
479
                }
480

481
                return stristr( $plugin_data['Name'], 'WPGraphQL' ) || stristr( $plugin_data['Description'], 'WPGraphQL' );
7✔
482
        }
483

484
        /**
485
         * Checks whether the plugin uses our version headers.
486
         *
487
         * @param string $plugin_path The plugin path used as the key in the plugins array.
488
         */
489
        private function is_versioned_dependent( string $plugin_path ): bool {
8✔
490
                $all_plugins = $this->get_all_plugins();
8✔
491
                $plugin_data = $all_plugins[ $plugin_path ] ?? null;
8✔
492

493
                // Bail early if the plugin doesn't exist.
494
                if ( empty( $plugin_data ) ) {
8✔
495
                        return false;
×
496
                }
497

498
                return ! empty( $plugin_data[ self::VERSION_HEADER ] ) || ! empty( $plugin_data[ self::TESTED_UP_TO_HEADER ] );
8✔
499
        }
500

501
        /**
502
         * Whether the plugin lists WPGraphQL in its `Requires Plugins` header.
503
         *
504
         * @param string $plugin_path The plugin path used as the key in the plugins array.
505
         */
506
        private function is_wpapi_dependent( string $plugin_path ): bool {
8✔
507
                $all_plugins = $this->get_all_plugins();
8✔
508

509
                $plugin_data = $all_plugins[ $plugin_path ] ?? null;
8✔
510

511
                // Bail early if the plugin doesn't exist.
512
                if ( empty( $plugin_data ) || empty( $plugin_data['RequiresPlugins'] ) ) {
8✔
513
                        return false;
8✔
514
                }
515

516
                // Normalize the required plugins.
517
                $required_plugins = array_map(
1✔
518
                        static function ( $slug ) {
1✔
519
                                return strtolower( trim( $slug ) );
1✔
520
                        },
1✔
521
                        explode( ',', $plugin_data['RequiresPlugins'] ) ?: []
1✔
522
                );
1✔
523

524
                return in_array( 'wp-graphql', $required_plugins, true );
1✔
525
        }
526

527
        /**
528
         * Gets the complete compatibility warning message including the plugins table and follow-up text.
529
         *
530
         * @param array<string,array<string,mixed>> $untested_plugins The untested plugins.
531
         * @return string The formatted HTML message.
532
         */
533
        public function get_compatibility_warning_message( array $untested_plugins ): string {
2✔
534
                ob_start();
2✔
535
                ?>
536
                <p>
2✔
537
                <?php
2✔
538
                echo wp_kses_post(
2✔
539
                        sprintf(
2✔
540
                        // translators: %s: The WPGraphQL version wrapped in a strong tag.
541
                                __(
2✔
542
                                        'The following active plugin(s) require WPGraphQL to function but have not yet declared compatibility with %s. Before updating WPGraphQL, please:',
2✔
543
                                        'wp-graphql'
2✔
544
                                ),
2✔
545
                                // translators: %s: The WPGraphQL version.
546
                                sprintf( '<strong>WPGraphQL v%s</strong>', $this->new_version )
2✔
547
                        )
2✔
548
                );
2✔
549
                ?>
550
                </p>
2✔
551

552
                <ol>
2✔
553
                        <li>
2✔
554
                        <?php
2✔
555
                        echo wp_kses_post(
2✔
556
                                sprintf(
2✔
557
                                // translators: %s: The WPGraphQL version wrapped in a strong tag.
558
                                        __( 'Update these plugins to their latest versions that declare compatibility with %s, OR', 'wp-graphql' ),
2✔
559
                                        sprintf( '<strong>WPGraphQL v%s</strong>', $this->new_version )
2✔
560
                                )
2✔
561
                        );
2✔
562
                        ?>
563
                        </li>
2✔
564
                        <li>
2✔
565
                        <?php
2✔
566
                        echo wp_kses_post(
2✔
567
                                sprintf(
2✔
568
                                // translators: %s: The WPGraphQL version wrapped in a strong tag.
569
                                        __( 'Confirm their compatibility with %s on your staging environment.', 'wp-graphql' ),
2✔
570
                                        sprintf( '<strong>WPGraphQL v%s</strong>', $this->new_version )
2✔
571
                                )
2✔
572
                        );
2✔
573
                        ?>
574
                        </li>
2✔
575
                </ol>
2✔
576

577
                <div class="plugin-details-table-container">
2✔
578
                        <table class="plugin-details-table" cellspacing="0">
2✔
579
                                <thead>
2✔
580
                                        <tr>
2✔
581
                                                <th><?php esc_html_e( 'Plugin', 'wp-graphql' ); ?></th>
2✔
582
                                                <th><?php esc_html_e( 'WPGraphQL Tested Up To', 'wp-graphql' ); ?></th>
2✔
583
                                        </tr>
584
                                </thead>
585
                                <tbody>
586
                                        <?php foreach ( $untested_plugins as $plugin ) : ?>
2✔
587
                                                <tr>
2✔
588
                                                        <td><?php echo esc_html( $plugin['Name'] ); ?></td>
2✔
589
                                                        <td><?php echo esc_html( $plugin[ self::TESTED_UP_TO_HEADER ] ); ?></td>
2✔
590
                                                </tr>
591
                                        <?php endforeach; ?>
592
                                </tbody>
2✔
593
                        </table>
2✔
594
                </div>
2✔
595

596
                <p><?php esc_html_e( 'For more information, review each plugin\'s changelogs or contact the plugin\'s developers.', 'wp-graphql' ); ?></p>
2✔
597

598
                <p><strong><?php esc_html_e( 'We strongly recommend creating a backup of your site before updating.', 'wp-graphql' ); ?></strong></p>
2✔
599
                <?php
600
                return (string) ob_get_clean();
2✔
601
        }
602
}
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