• 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

17.33
/src/Admin/AdminNotices.php
1
<?php
2

3
namespace WPGraphQL\Admin;
4

5
/**
6
 * This class isn't intended for direct extending or customizing.
7
 *
8
 * This class is responsible for handling the management and display of admin notices
9
 * related directly to WPGraphQL.
10
 *
11
 * Breaking changes to this class will not be considered a semver breaking change as there's no
12
 * expectation that users will be calling these functions directly or extending this class.
13
 *
14
 * @internal
15
 */
16
class AdminNotices {
17

18
        /**
19
         * Stores the singleton instance of the class
20
         *
21
         * @var self|null
22
         */
23
        private static $instance = null;
24

25
        /**
26
         * Stores the admin notices to display
27
         *
28
         * @var array<string,array<string,mixed>>
29
         */
30
        protected $admin_notices = [];
31

32
        /**
33
         * @var array<string>
34
         */
35
        protected $dismissed_notices = [];
36

37
        /**
38
         * Private constructor to prevent direct instantiation
39
         */
UNCOV
40
        private function __construct() {
×
41
                // Initialize the class (can move code from init() here if desired)
42
        }
×
43

44
        /**
45
         * Prevent cloning the instance
46
         */
47
        public function __clone() {}
48

49
        /**
50
         * Prevent unserializing the instance
51
         */
52
        public function __wakeup() {}
53

54
        /**
55
         * Get the singleton instance of the class
56
         */
57
        public static function get_instance(): self {
4✔
58
                if ( null === self::$instance ) {
4✔
59
                        self::$instance = new self();
×
60
                        self::$instance->init();
×
61
                }
62
                return self::$instance;
4✔
63
        }
64

65
        /**
66
         * Initialize the Admin Notices class
67
         */
UNCOV
68
        public function init(): void {
×
69

70
                register_graphql_admin_notice(
×
71
                        'wpgraphql-acf-announcement',
×
72
                        [
×
73
                                'type'           => 'info',
×
74
                                'message'        => __( 'You are using WPGraphQL and Advanced Custom Fields. Have you seen the new <a href="https://acf.wpgraphql.com/" target="_blank" rel="nofollow">WPGraphQL for ACF</a>?', 'wp-graphql' ),
×
75
                                'is_dismissable' => true,
×
76
                                'conditions'     => static function () {
×
77
                                        if ( ! class_exists( 'ACF' ) ) {
×
78
                                                return false;
×
79
                                        }
80

81
                                        // Bail if new version of WPGraphQL for ACF is active.
82
                                        if ( class_exists( 'WPGraphQLAcf' ) ) {
×
83
                                                return false;
×
84
                                        }
85

86
                                        return true;
×
87
                                },
×
88
                        ]
×
89
                );
×
90

91
                // Initialize Admin Notices. This is where register_graphql_admin_notice hooks in
92
                do_action( 'graphql_admin_notices_init', $this );
×
93

94
                $current_user_id         = get_current_user_id();
×
95
                $this->dismissed_notices = get_user_meta( $current_user_id, 'wpgraphql_dismissed_admin_notices', true ) ?: [];
×
96

97
                // Filter the notices to remove any dismissed notices
98
                $this->pre_filter_dismissed_notices();
×
99

100
                add_action( 'admin_notices', [ $this, 'maybe_display_notices' ] );
×
101
                add_action( 'network_admin_notices', [ $this, 'maybe_display_notices' ] );
×
102
                add_action( 'admin_init', [ $this, 'handle_dismissal_of_notice' ] );
×
103
                add_action( 'admin_menu', [ $this, 'add_notification_bubble' ], 100 );
×
104
        }
105

106
        /**
107
         * Pre-filters dismissed notices from the admin notices array.
108
         */
UNCOV
109
        protected function pre_filter_dismissed_notices(): void {
×
110

111
                // remove any notice that's been dismissed
112
                foreach ( $this->dismissed_notices as $dismissed_notice ) {
×
113
                        $this->remove_admin_notice( $dismissed_notice );
×
114
                }
115

116
                // For all remaining notices, run the callback to see if it's actually relevant
117
                foreach ( $this->admin_notices as $notice_slug => $notice ) {
×
118
                        if ( ! isset( $notice['conditions'] ) ) {
×
119
                                continue;
×
120
                        }
121

122
                        if ( ! is_callable( $notice['conditions'] ) ) {
×
123
                                continue;
×
124
                        }
125

126
                        if ( false === $notice['conditions']() && ! is_network_admin() ) {
×
127
                                $this->remove_admin_notice( $notice_slug );
×
128
                        }
129
                }
130
        }
131

132
        /**
133
         * Return all admin notices
134
         *
135
         * @return array<string,array<string,mixed>>
136
         */
137
        public function get_admin_notices(): array {
3✔
138
                return $this->admin_notices;
3✔
139
        }
140

141
        /**
142
         * @param string              $slug The slug identifying the admin notice
143
         * @param array<string,mixed> $config The config of the admin notice
144
         *
145
         * @return array<string,mixed>
146
         */
147
        public function add_admin_notice( string $slug, array $config ): array {
3✔
148
                /**
149
                 * Pass the notice through a filter before registering it
150
                 *
151
                 * @param array<string,mixed> $config The config of the admin notice
152
                 * @param string              $slug   The slug identifying the admin notice
153
                 */
154
                $filtered_notice = apply_filters( 'graphql_add_admin_notice', $config, $slug );
3✔
155

156
                // If not a valid config, bail early.
157
                if ( ! $this->is_valid_config( $config ) ) {
3✔
158
                        return [];
×
159
                }
160

161
                $this->admin_notices[ $slug ] = $filtered_notice;
3✔
162
                return $this->admin_notices[ $slug ];
3✔
163
        }
164

165
        /**
166
         * Throw an error if the config is not valid.
167
         *
168
         * @since v1.21.0
169
         *
170
         * @param array<string,mixed> $config The config of the admin notice
171
         */
172
        public function is_valid_config( array $config ): bool {
3✔
173
                if ( empty( $config['message'] ) ) {
3✔
174
                        _doing_it_wrong( 'register_graphql_admin_notice', esc_html__( 'Config message is required', 'wp-graphql' ), '1.21.0' );
×
175
                        return false;
×
176
                }
177

178
                if ( isset( $config['conditions'] ) && ! is_callable( $config['conditions'] ) ) {
3✔
179
                        _doing_it_wrong( 'register_graphql_admin_notice', esc_html__( 'Config conditions should be callable', 'wp-graphql' ), '1.21.0' );
×
180
                        return false;
×
181
                }
182

183
                if ( isset( $config['type'] ) && ! in_array( $config['type'], [ 'error', 'warning', 'success', 'info' ], true ) ) {
3✔
184
                        _doing_it_wrong( 'register_graphql_admin_notice', esc_html__( 'Config type should be one of the following: error | warning | success | info', 'wp-graphql' ), '1.21.0' );
×
185
                        return false;
×
186
                }
187

188
                if ( isset( $config['is_dismissable'] ) && ! is_bool( $config['is_dismissable'] ) ) {
3✔
189
                        _doing_it_wrong( 'register_graphql_admin_notice', esc_html__( 'is_dismissable should be a boolean', 'wp-graphql' ), '1.21.0' );
×
190
                        return false;
×
191
                }
192

193
                return true;
3✔
194
        }
195

196
        /**
197
         * Given the slug of an admin notice, remove it from the notices
198
         *
199
         * @param string $slug The slug identifying the admin notice to remove
200
         *
201
         * @return array<mixed>
202
         */
203
        public function remove_admin_notice( string $slug ): array {
1✔
204
                unset( $this->admin_notices[ $slug ] );
1✔
205
                return $this->admin_notices;
1✔
206
        }
207

208
        /**
209
         * Determine whether a notice is dismissable or not
210
         *
211
         * @param array<mixed> $notice The notice to check whether its dismissable or not
212
         */
UNCOV
213
        public function is_notice_dismissable( array $notice = [] ): bool {
×
214
                return ( ! isset( $notice['is_dismissable'] ) || false !== (bool) $notice['is_dismissable'] );
×
215
        }
216

217
        /**
218
         * Display notices if they are displayable
219
         */
220
        public function maybe_display_notices(): void {
1✔
221
                if ( ! $this->is_plugin_scoped_page() ) {
1✔
222
                        return;
1✔
223
                }
224

225
                $this->render_notices();
×
226
        }
227

228
        /**
229
         * Adds the notification count to the menu item.
230
         */
UNCOV
231
        public function add_notification_bubble(): void {
×
232
                global $menu;
×
233

234
                $admin_notices = $this->get_admin_notices();
×
235

236
                $notice_count = count( $admin_notices );
×
237

238
                if ( 0 === $notice_count ) {
×
239
                        return;
×
240
                }
241

242
                foreach ( $menu as $key => $item ) {
×
243
                        if ( 'graphiql-ide' === $item[2] ) {
×
244
                // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
245
                                $menu[ $key ][0] .= ' <span class="update-plugins count-' . absint( $notice_count ) . '"><span class="plugin-count">' . absint( $notice_count ) . '</span></span>';
×
246
                                break;
×
247
                        }
248
                }
249
        }
250

251
        /**
252
         * Render the notices.
253
         */
UNCOV
254
        protected function render_notices(): void {
×
255

256
                $notices = $this->get_admin_notices();
×
257

258
                if ( empty( $notices ) ) {
×
259
                        return;
×
260
                }
261
                ?>
262
                <style>
×
263
                        /* Only display the ACF notice */
×
264
                        body.toplevel_page_graphiql-ide #wpbody .wpgraphql-admin-notice {
×
265
                                display: block;
×
266
                                position: absolute;
×
267
                                top: 0;
×
268
                                right: 0;
×
269
                                z-index: 1;
×
270
                                min-width: 40%;
×
271
                        }
×
272
                        body.toplevel_page_graphiql-ide #wpbody #wp-graphiql-wrapper {
×
273
                                margin-top: <?php echo count( $notices ) * 45; ?>px;
×
274
                        }
275
                        .wpgraphql-admin-notice {
276
                                position: relative;
277
                                text-decoration: none;
278
                                padding: 1px 40px 1px 12px;
279
                        }
280
                        .wpgraphql-admin-notice .notice-dismiss {
281
                                text-decoration: none;
282
                        }
283

284
                </style>
285
                <?php
286
                $count = 0;
×
287

288
                /**
289
                 * Fires before the admin notices are rendered.
290
                 *
291
                 * @param array<string,mixed> $notices The notices to be rendered
292
                 *
293
                 * @since v1.23.0
294
                 */
295
                do_action( 'graphql_admin_notices_render_notices', $notices );
×
296

297
                foreach ( $notices as $notice_slug => $notice ) {
×
298
                        $type = $notice['type'] ?? 'info';
×
299
                        ?>
300
                        <style>
×
301
                                body.toplevel_page_graphiql-ide #wpbody #wpgraphql-admin-notice-<?php echo esc_attr( $notice_slug ); ?> {
×
302
                                        top: <?php echo esc_attr( ( $count * 45 ) . 'px' ); ?>
×
303
                                }
×
304
                        </style>
×
305
                        <div id="wpgraphql-admin-notice-<?php echo esc_attr( $notice_slug ); ?>" class="wpgraphql-admin-notice notice notice-<?php echo esc_attr( $type ); ?> <?php echo $this->is_notice_dismissable( $notice ) ? 'is-dismissable' : ''; ?>">
×
306
                                <p><?php echo ! empty( $notice['message'] ) ? wp_kses_post( $notice['message'] ) : ''; ?></p>
×
307
                                <?php
308
                                $is_dismissable = $this->is_notice_dismissable( $notice );
×
309
                                if ( $is_dismissable ) {
×
310
                                        $dismiss_acf_nonce = wp_create_nonce( 'wpgraphql_disable_notice_nonce' );
×
311
                                        $dismiss_url       = add_query_arg(
×
312
                                                [
×
313
                                                        'wpgraphql_disable_notice_nonce' => $dismiss_acf_nonce,
×
314
                                                        'wpgraphql_disable_notice' => $notice_slug,
×
315
                                                ]
×
316
                                        );
×
317
                                        ?>
318
                                        <a href="<?php echo esc_url( $dismiss_url ); ?>" class="notice-dismiss">
×
319
                                                <span class="screen-reader-text"><?php esc_html_e( 'Dismiss', 'wp-graphql' ); ?></span>
×
320
                                        </a>
321
                                <?php } ?>
322
                        </div>
×
323
                        <?php
×
324
                        /**
325
                         * Fires for each admin notice that is rendered.
326
                         *
327
                         * @param string $notice_slug The slug of the notice
328
                         * @param array<mixed> $notice The notice to be rendered
329
                         * @param bool $is_dismissable Whether the notice is dismissable or not
330
                         * @param int $count The count of the notice
331
                         *
332
                         * @since v1.23.0
333
                         */
334
                        do_action( 'graphql_admin_notices_render_notice', $notice_slug, $notice, $is_dismissable, $count );
×
335
                        ++$count;
×
336
                }
337
        }
338

339
        /**
340
         * Checks if the current admin page is within the scope of the plugin's own pages.
341
         *
342
         * @return bool True if the current page is within scope of the plugin's pages.
343
         */
344
        protected function is_plugin_scoped_page(): bool {
1✔
345
                $screen = get_current_screen();
1✔
346

347
                // Guard clause for invalid screen.
348
                if ( ! $screen ) {
1✔
349
                        return false;
1✔
350
                }
351

352
                $allowed_pages = [
×
353
                        'plugins',
×
354
                        'plugins-network',
×
355
                        'toplevel_page_graphiql-ide',
×
356
                        'graphql_page_graphql-settings',
×
357
                ];
×
358

359
                $current_page_id = $screen->id;
×
360

361
                $is_allowed_admin_page = in_array( $current_page_id, $allowed_pages, true );
×
362

363
                /**
364
                 * Filter to determine if the current admin page is within the scope of the plugin's own pages.
365
                 * This filter can be used to add additional pages to the list of allowed pages.
366
                 *
367
                 * The filter receives the following arguments:
368
                 *
369
                 * @param bool $is_plugin_scoped_page True if the current page is within scope of the plugin's pages.
370
                 * @param string $current_page_id The ID of the current admin page.
371
                 * @param array<string> $allowed_pages The list of allowed pages.
372
                 */
373
                return apply_filters( 'graphql_admin_notices_is_allowed_admin_page', $is_allowed_admin_page, $current_page_id, $allowed_pages );
×
374
        }
375

376
        /**
377
         * Handles the dismissal of the ACF notice.
378
         * set_transient reference: https://developer.wordpress.org/reference/functions/set_transient/
379
         * This function sets a transient to remember the dismissal status of the notice.
380
         */
UNCOV
381
        public function handle_dismissal_of_notice(): void {
×
382
                if ( ! isset( $_GET['wpgraphql_disable_notice_nonce'], $_GET['wpgraphql_disable_notice'] ) ) {
×
383
                        return;
×
384
                }
385

386
                $nonce       = sanitize_text_field( wp_unslash( $_GET['wpgraphql_disable_notice_nonce'] ) );
×
387
                $notice_slug = sanitize_text_field( wp_unslash( $_GET['wpgraphql_disable_notice'] ) );
×
388

389
                if ( empty( $notice_slug ) || ! wp_verify_nonce( $nonce, 'wpgraphql_disable_notice_nonce' ) ) {
×
390
                        return;
×
391
                }
392

393
                $current_user_id = get_current_user_id();
×
394

395
                $disabled   = get_user_meta( $current_user_id, 'wpgraphql_dismissed_admin_notices', true ) ?: [];
×
396
                $disabled[] = $notice_slug;
×
397

398
                update_user_meta( $current_user_id, 'wpgraphql_dismissed_admin_notices', array_unique( $disabled ) );
×
399

400
                // Redirect to clear URL parameters
401
                wp_safe_redirect( remove_query_arg( [ 'wpgraphql_disable_notice_nonce', 'wpgraphql_disable_notice' ] ) );
×
402
                exit();
×
403
        }
404
}
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