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

wp-graphql / wp-graphql / 14716683875

28 Apr 2025 07:58PM UTC coverage: 84.287% (+1.6%) from 82.648%
14716683875

push

github

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

15905 of 18870 relevant lines covered (84.29%)

257.23 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
 * @phpstan-type AdminNoticeConfig array{
15
 *   message: string,
16
 *   type?: 'error'|'warning'|'success'|'info',
17
 *   is_dismissable?: bool,
18
 *   conditions?: callable():bool,
19
 * }
20
 *
21
 * @internal
22
 */
23
class AdminNotices {
24

25
        /**
26
         * Stores the singleton instance of the class
27
         *
28
         * @var self|null
29
         */
30
        private static $instance = null;
31

32
        /**
33
         * Stores the admin notices to display
34
         *
35
         * @var array<string,AdminNoticeConfig>
36
         */
37
        protected $admin_notices = [];
38

39
        /**
40
         * @var array<string>
41
         */
42
        protected $dismissed_notices = [];
43

44
        /**
45
         * Private constructor to prevent direct instantiation
46
         */
47
        private function __construct() {
×
48
                // Initialize the class (can move code from init() here if desired)
49
        }
×
50

51
        /**
52
         * Prevent cloning the instance
53
         */
54
        public function __clone() {}
55

56
        /**
57
         * Prevent unserializing the instance
58
         */
59
        public function __wakeup() {}
60

61
        /**
62
         * Get the singleton instance of the class
63
         */
64
        public static function get_instance(): self {
4✔
65
                if ( null === self::$instance ) {
4✔
66
                        self::$instance = new self();
×
67
                        self::$instance->init();
×
68
                }
69
                return self::$instance;
4✔
70
        }
71

72
        /**
73
         * Initialize the Admin Notices class
74
         */
75
        public function init(): void {
×
76

77
                register_graphql_admin_notice(
×
78
                        'wpgraphql-acf-announcement',
×
79
                        [
×
80
                                'type'           => 'info',
×
81
                                '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' ),
×
82
                                'is_dismissable' => true,
×
83
                                'conditions'     => static function () {
×
84
                                        if ( ! class_exists( 'ACF' ) ) {
×
85
                                                return false;
×
86
                                        }
87

88
                                        // Bail if new version of WPGraphQL for ACF is active.
89
                                        if ( class_exists( 'WPGraphQLAcf' ) ) {
×
90
                                                return false;
×
91
                                        }
92

93
                                        return true;
×
94
                                },
×
95
                        ]
×
96
                );
×
97

98
                // Initialize Admin Notices. This is where register_graphql_admin_notice hooks in
99
                do_action( 'graphql_admin_notices_init', $this );
×
100

101
                $current_user_id         = get_current_user_id();
×
102
                $this->dismissed_notices = get_user_meta( $current_user_id, 'wpgraphql_dismissed_admin_notices', true ) ?: [];
×
103

104
                // Filter the notices to remove any dismissed notices
105
                $this->pre_filter_dismissed_notices();
×
106

107
                add_action( 'admin_notices', [ $this, 'maybe_display_notices' ] );
×
108
                add_action( 'network_admin_notices', [ $this, 'maybe_display_notices' ] );
×
109
                add_action( 'admin_init', [ $this, 'handle_dismissal_of_notice' ] );
×
110
                add_action( 'admin_menu', [ $this, 'add_notification_bubble' ], 100 );
×
111
        }
112

113
        /**
114
         * Pre-filters dismissed notices from the admin notices array.
115
         */
116
        protected function pre_filter_dismissed_notices(): void {
×
117

118
                // remove any notice that's been dismissed
119
                foreach ( $this->dismissed_notices as $dismissed_notice ) {
×
120
                        $this->remove_admin_notice( $dismissed_notice );
×
121
                }
122

123
                // For all remaining notices, run the callback to see if it's actually relevant
124
                foreach ( $this->admin_notices as $notice_slug => $notice ) {
×
125
                        if ( ! isset( $notice['conditions'] ) ) {
×
126
                                continue;
×
127
                        }
128

129
                        if ( ! is_callable( $notice['conditions'] ) ) {
×
130
                                continue;
×
131
                        }
132

133
                        if ( false === $notice['conditions']() && ! is_network_admin() ) {
×
134
                                $this->remove_admin_notice( $notice_slug );
×
135
                        }
136
                }
137
        }
138

139
        /**
140
         * Return all admin notices
141
         *
142
         * @return array<string,AdminNoticeConfig>
143
         */
144
        public function get_admin_notices(): array {
3✔
145
                return $this->admin_notices;
3✔
146
        }
147

148
        /**
149
         * @param string            $slug   The slug identifying the admin notice
150
         * @param AdminNoticeConfig $config The config of the admin notice
151
         *
152
         * @return AdminNoticeConfig|array{}
153
         */
154
        public function add_admin_notice( string $slug, array $config ): array {
3✔
155
                /**
156
                 * Pass the notice through a filter before registering it
157
                 *
158
                 * @param AdminNoticeConfig $config The config of the admin notice
159
                 * @param string            $slug   The slug identifying the admin notice
160
                 */
161
                $filtered_notice = apply_filters( 'graphql_add_admin_notice', $config, $slug );
3✔
162

163
                // If not a valid config, bail early.
164
                if ( ! $this->is_valid_config( $filtered_notice ) ) {
3✔
165
                        return [];
×
166
                }
167

168
                $this->admin_notices[ $slug ] = $filtered_notice;
3✔
169
                return $this->admin_notices[ $slug ];
3✔
170
        }
171

172
        /**
173
         * Throw an error if the config is not valid.
174
         *
175
         * @since v1.21.0
176
         *
177
         * @param array<string,mixed> $config The config of the admin notice
178
         *
179
         * @phpstan-assert-if-true array{
180
         *  message: string,
181
         *  type?: 'error'|'warning'|'success'|'info',
182
         *  is_dismissable?: bool,
183
         *  conditions?: callable,
184
         * } $config
185
         */
186
        public function is_valid_config( array $config ): bool {
3✔
187
                if ( empty( $config['message'] ) ) {
3✔
188
                        _doing_it_wrong( 'register_graphql_admin_notice', esc_html__( 'Config message is required', 'wp-graphql' ), '1.21.0' );
×
189
                        return false;
×
190
                }
191

192
                if ( isset( $config['conditions'] ) && ! is_callable( $config['conditions'] ) ) {
3✔
193
                        _doing_it_wrong( 'register_graphql_admin_notice', esc_html__( 'Config conditions should be callable', 'wp-graphql' ), '1.21.0' );
×
194
                        return false;
×
195
                }
196

197
                if ( isset( $config['type'] ) && ! in_array( $config['type'], [ 'error', 'warning', 'success', 'info' ], true ) ) {
3✔
198
                        _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' );
×
199
                        return false;
×
200
                }
201

202
                if ( isset( $config['is_dismissable'] ) && ! is_bool( $config['is_dismissable'] ) ) {
3✔
203
                        _doing_it_wrong( 'register_graphql_admin_notice', esc_html__( 'is_dismissable should be a boolean', 'wp-graphql' ), '1.21.0' );
×
204
                        return false;
×
205
                }
206

207
                return true;
3✔
208
        }
209

210
        /**
211
         * Given the slug of an admin notice, remove it from the notices
212
         *
213
         * @param string $slug The slug identifying the admin notice to remove
214
         *
215
         * @return array<string,AdminNoticeConfig>
216
         */
217
        public function remove_admin_notice( string $slug ): array {
1✔
218
                unset( $this->admin_notices[ $slug ] );
1✔
219
                return $this->admin_notices;
1✔
220
        }
221

222
        /**
223
         * Determine whether a notice is dismissable or not
224
         *
225
         * @param AdminNoticeConfig|array{} $notice The notice to check whether its dismissable or not
226
         */
227
        public function is_notice_dismissable( array $notice = [] ): bool {
×
228
                return ( ! isset( $notice['is_dismissable'] ) || false !== (bool) $notice['is_dismissable'] );
×
229
        }
230

231
        /**
232
         * Display notices if they are displayable
233
         */
234
        public function maybe_display_notices(): void {
1✔
235
                if ( ! $this->is_plugin_scoped_page() ) {
1✔
236
                        return;
1✔
237
                }
238

239
                $this->render_notices();
×
240
        }
241

242
        /**
243
         * Adds the notification count to the menu item.
244
         */
245
        public function add_notification_bubble(): void {
×
246
                global $menu;
×
247

248
                $admin_notices = $this->get_admin_notices();
×
249

250
                $notice_count = count( $admin_notices );
×
251

252
                if ( 0 === $notice_count ) {
×
253
                        return;
×
254
                }
255

256
                foreach ( $menu as $key => $item ) {
×
257
                        if ( 'graphiql-ide' === $item[2] ) {
×
258
                // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
259
                                $menu[ $key ][0] .= ' <span class="update-plugins count-' . absint( $notice_count ) . '"><span class="plugin-count">' . absint( $notice_count ) . '</span></span>';
×
260
                                break;
×
261
                        }
262
                }
263
        }
264

265
        /**
266
         * Render the notices.
267
         */
268
        protected function render_notices(): void {
×
269

270
                $notices = $this->get_admin_notices();
×
271

272
                if ( empty( $notices ) ) {
×
273
                        return;
×
274
                }
275
                ?>
276
                <style>
×
277
                        /* Only display the ACF notice */
×
278
                        body.toplevel_page_graphiql-ide #wpbody .wpgraphql-admin-notice {
×
279
                                display: block;
×
280
                                position: absolute;
×
281
                                top: 0;
×
282
                                right: 0;
×
283
                                z-index: 1;
×
284
                                min-width: 40%;
×
285
                        }
×
286
                        body.toplevel_page_graphiql-ide #wpbody #wp-graphiql-wrapper {
×
287
                                margin-top: <?php echo count( $notices ) * 45; ?>px;
×
288
                        }
289
                        .wpgraphql-admin-notice {
290
                                position: relative;
291
                                text-decoration: none;
292
                                padding: 1px 40px 1px 12px;
293
                        }
294
                        .wpgraphql-admin-notice .notice-dismiss {
295
                                text-decoration: none;
296
                        }
297

298
                </style>
299
                <?php
300
                $count = 0;
×
301

302
                /**
303
                 * Fires before the admin notices are rendered.
304
                 *
305
                 * @param array<string,AdminNoticeConfig> $notices The notices to be rendered
306
                 *
307
                 * @since v1.23.0
308
                 */
309
                do_action( 'graphql_admin_notices_render_notices', $notices );
×
310

311
                foreach ( $notices as $notice_slug => $notice ) {
×
312
                        $type = $notice['type'] ?? 'info';
×
313
                        ?>
314
                        <style>
×
315
                                body.toplevel_page_graphiql-ide #wpbody #wpgraphql-admin-notice-<?php echo esc_attr( $notice_slug ); ?> {
×
316
                                        top: <?php echo esc_attr( ( $count * 45 ) . 'px' ); ?>
×
317
                                }
×
318
                        </style>
×
319
                        <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' : ''; ?>">
×
320
                                <p><?php echo ! empty( $notice['message'] ) ? wp_kses_post( $notice['message'] ) : ''; ?></p>
×
321
                                <?php
322
                                $is_dismissable = $this->is_notice_dismissable( $notice );
×
323
                                if ( $is_dismissable ) {
×
324
                                        $dismiss_acf_nonce = wp_create_nonce( 'wpgraphql_disable_notice_nonce' );
×
325
                                        $dismiss_url       = add_query_arg(
×
326
                                                [
×
327
                                                        'wpgraphql_disable_notice_nonce' => $dismiss_acf_nonce,
×
328
                                                        'wpgraphql_disable_notice' => $notice_slug,
×
329
                                                ]
×
330
                                        );
×
331
                                        ?>
332
                                        <a href="<?php echo esc_url( $dismiss_url ); ?>" class="notice-dismiss">
×
333
                                                <span class="screen-reader-text"><?php esc_html_e( 'Dismiss', 'wp-graphql' ); ?></span>
×
334
                                        </a>
335
                                <?php } ?>
336
                        </div>
×
337
                        <?php
×
338
                        /**
339
                         * Fires for each admin notice that is rendered.
340
                         *
341
                         * @param string $notice_slug The slug of the notice
342
                         * @param AdminNoticeConfig $notice The notice to be rendered
343
                         * @param bool $is_dismissable Whether the notice is dismissable or not
344
                         * @param int $count The count of the notice
345
                         *
346
                         * @since v1.23.0
347
                         */
348
                        do_action( 'graphql_admin_notices_render_notice', $notice_slug, $notice, $is_dismissable, $count );
×
349
                        ++$count;
×
350
                }
351
        }
352

353
        /**
354
         * Checks if the current admin page is within the scope of the plugin's own pages.
355
         *
356
         * @return bool True if the current page is within scope of the plugin's pages.
357
         */
358
        protected function is_plugin_scoped_page(): bool {
1✔
359
                $screen = get_current_screen();
1✔
360

361
                // Guard clause for invalid screen.
362
                if ( ! $screen ) {
1✔
363
                        return false;
1✔
364
                }
365

366
                $allowed_pages = [
×
367
                        'plugins',
×
368
                        'plugins-network',
×
369
                        'toplevel_page_graphiql-ide',
×
370
                        'graphql_page_graphql-settings',
×
371
                ];
×
372

373
                $current_page_id = $screen->id;
×
374

375
                $is_allowed_admin_page = in_array( $current_page_id, $allowed_pages, true );
×
376

377
                /**
378
                 * Filter to determine if the current admin page is within the scope of the plugin's own pages.
379
                 * This filter can be used to add additional pages to the list of allowed pages.
380
                 *
381
                 * The filter receives the following arguments:
382
                 *
383
                 * @param bool $is_plugin_scoped_page True if the current page is within scope of the plugin's pages.
384
                 * @param string $current_page_id The ID of the current admin page.
385
                 * @param array<string> $allowed_pages The list of allowed pages.
386
                 */
387
                return apply_filters( 'graphql_admin_notices_is_allowed_admin_page', $is_allowed_admin_page, $current_page_id, $allowed_pages );
×
388
        }
389

390
        /**
391
         * Handles the dismissal of the ACF notice.
392
         * set_transient reference: https://developer.wordpress.org/reference/functions/set_transient/
393
         * This function sets a transient to remember the dismissal status of the notice.
394
         */
395
        public function handle_dismissal_of_notice(): void {
×
396
                if ( ! isset( $_GET['wpgraphql_disable_notice_nonce'], $_GET['wpgraphql_disable_notice'] ) ) {
×
397
                        return;
×
398
                }
399

400
                $nonce       = sanitize_text_field( wp_unslash( $_GET['wpgraphql_disable_notice_nonce'] ) );
×
401
                $notice_slug = sanitize_text_field( wp_unslash( $_GET['wpgraphql_disable_notice'] ) );
×
402

403
                if ( empty( $notice_slug ) || ! wp_verify_nonce( $nonce, 'wpgraphql_disable_notice_nonce' ) ) {
×
404
                        return;
×
405
                }
406

407
                $current_user_id = get_current_user_id();
×
408

409
                $disabled   = get_user_meta( $current_user_id, 'wpgraphql_dismissed_admin_notices', true ) ?: [];
×
410
                $disabled[] = $notice_slug;
×
411

412
                update_user_meta( $current_user_id, 'wpgraphql_dismissed_admin_notices', array_unique( $disabled ) );
×
413

414
                // Redirect to clear URL parameters
415
                wp_safe_redirect( remove_query_arg( [ 'wpgraphql_disable_notice_nonce', 'wpgraphql_disable_notice' ] ) );
×
416
                exit();
×
417
        }
418
}
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