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

Yoast / wordpress-seo / dd6e866a9e6d253114633104d9e3858d807178ba

19 Jun 2024 10:03AM UTC coverage: 48.628% (-4.3%) from 52.936%
dd6e866a9e6d253114633104d9e3858d807178ba

push

github

web-flow
Merge pull request #21431 from Yoast/21429-update-copy-in-the-introduction-and-consent-modals

Updates the copy for the introduction and consent modals

7441 of 13454 branches covered (55.31%)

Branch coverage included in aggregate %.

0 of 3 new or added lines in 2 files covered. (0.0%)

3718 existing lines in 107 files now uncovered.

25100 of 53464 relevant lines covered (46.95%)

62392.47 hits per line

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

0.0
/admin/class-yoast-notification-center.php
1
<?php
2
/**
3
 * WPSEO plugin file.
4
 *
5
 * @package WPSEO\Admin\Notifications
6
 */
7

8
use Yoast\WP\SEO\Presenters\Abstract_Presenter;
9

10
/**
11
 * Handles notifications storage and display.
12
 */
13
class Yoast_Notification_Center {
14

15
        /**
16
         * Option name to store notifications on.
17
         *
18
         * @var string
19
         */
20
        public const STORAGE_KEY = 'yoast_notifications';
21

22
        /**
23
         * The singleton instance of this object.
24
         *
25
         * @var Yoast_Notification_Center
26
         */
27
        private static $instance = null;
28

29
        /**
30
         * Holds the notifications.
31
         *
32
         * @var Yoast_Notification[][]
33
         */
34
        private $notifications = [];
35

36
        /**
37
         * Notifications there are newly added.
38
         *
39
         * @var array
40
         */
41
        private $new = [];
42

43
        /**
44
         * Notifications that were resolved this execution.
45
         *
46
         * @var int
47
         */
48
        private $resolved = 0;
49

50
        /**
51
         * Internal storage for transaction before notifications have been retrieved from storage.
52
         *
53
         * @var array
54
         */
55
        private $queued_transactions = [];
56

57
        /**
58
         * Internal flag for whether notifications have been retrieved from storage.
59
         *
60
         * @var bool
61
         */
62
        private $notifications_retrieved = false;
63

64
        /**
65
         * Internal flag for whether notifications need to be updated in storage.
66
         *
67
         * @var bool
68
         */
69
        private $notifications_need_storage = false;
70

71
        /**
72
         * Construct.
73
         */
74
        private function __construct() {
×
75

76
                add_action( 'init', [ $this, 'setup_current_notifications' ], 1 );
×
77

78
                add_action( 'all_admin_notices', [ $this, 'display_notifications' ] );
×
79

80
                add_action( 'wp_ajax_yoast_get_notifications', [ $this, 'ajax_get_notifications' ] );
×
81

82
                add_action( 'wpseo_deactivate', [ $this, 'deactivate_hook' ] );
×
83
                add_action( 'shutdown', [ $this, 'update_storage' ] );
×
84
        }
85

86
        /**
87
         * Singleton getter.
88
         *
89
         * @return Yoast_Notification_Center
90
         */
UNCOV
91
        public static function get() {
×
92

UNCOV
93
                if ( self::$instance === null ) {
×
94
                        self::$instance = new self();
×
95
                }
96

UNCOV
97
                return self::$instance;
×
98
        }
99

100
        /**
101
         * Dismiss a notification.
102
         *
103
         * @return void
104
         */
105
        public static function ajax_dismiss_notification() {
×
106
                $notification_center = self::get();
×
107

108
                if ( ! isset( $_POST['notification'] ) || ! is_string( $_POST['notification'] ) ) {
×
109
                        die( '-1' );
×
110
                }
111

112
                $notification_id = sanitize_text_field( wp_unslash( $_POST['notification'] ) );
×
113

114
                if ( empty( $notification_id ) ) {
×
115
                        die( '-1' );
×
116
                }
117

118
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are using the variable as a nonce.
119
                if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['nonce'] ), $notification_id ) ) {
×
120
                        die( '-1' );
×
121
                }
122

123
                $notification = $notification_center->get_notification_by_id( $notification_id );
×
124
                if ( ( $notification instanceof Yoast_Notification ) === false ) {
×
125

126
                        // Permit legacy.
127
                        $options      = [
128
                                'id'            => $notification_id,
×
129
                                'dismissal_key' => $notification_id,
×
130
                        ];
131
                        $notification = new Yoast_Notification( '', $options );
×
132
                }
133

134
                if ( self::maybe_dismiss_notification( $notification ) ) {
×
135
                        die( '1' );
×
136
                }
137

138
                die( '-1' );
×
139
        }
140

141
        /**
142
         * Check if the user has dismissed a notification.
143
         *
144
         * @param Yoast_Notification $notification The notification to check for dismissal.
145
         * @param int|null           $user_id      User ID to check on.
146
         *
147
         * @return bool
148
         */
UNCOV
149
        public static function is_notification_dismissed( Yoast_Notification $notification, $user_id = null ) {
×
150

UNCOV
151
                $user_id       = self::get_user_id( $user_id );
×
UNCOV
152
                $dismissal_key = $notification->get_dismissal_key();
×
153

154
                // This checks both the site-specific user option and the meta value.
UNCOV
155
                $current_value = get_user_option( $dismissal_key, $user_id );
×
156

157
                // Migrate old user meta to user option on-the-fly.
UNCOV
158
                if ( ! empty( $current_value )
×
UNCOV
159
                        && metadata_exists( 'user', $user_id, $dismissal_key )
×
UNCOV
160
                        && update_user_option( $user_id, $dismissal_key, $current_value ) ) {
×
UNCOV
161
                        delete_user_meta( $user_id, $dismissal_key );
×
162
                }
163

UNCOV
164
                return ! empty( $current_value );
×
165
        }
166

167
        /**
168
         * Checks if the notification is being dismissed.
169
         *
170
         * @param Yoast_Notification $notification Notification to check dismissal of.
171
         * @param string             $meta_value   Value to set the meta value to if dismissed.
172
         *
173
         * @return bool True if dismissed.
174
         */
UNCOV
175
        public static function maybe_dismiss_notification( Yoast_Notification $notification, $meta_value = 'seen' ) {
×
176

177
                // Only persistent notifications are dismissible.
UNCOV
178
                if ( ! $notification->is_persistent() ) {
×
UNCOV
179
                        return false;
×
180
                }
181

182
                // If notification is already dismissed, we're done.
UNCOV
183
                if ( self::is_notification_dismissed( $notification ) ) {
×
184
                        return true;
×
185
                }
186

UNCOV
187
                $dismissal_key   = $notification->get_dismissal_key();
×
UNCOV
188
                $notification_id = $notification->get_id();
×
189

UNCOV
190
                $is_dismissing = ( $dismissal_key === self::get_user_input( 'notification' ) );
×
UNCOV
191
                if ( ! $is_dismissing ) {
×
UNCOV
192
                        $is_dismissing = ( $notification_id === self::get_user_input( 'notification' ) );
×
193
                }
194

195
                // Fallback to ?dismissal_key=1&nonce=bla when JavaScript fails.
UNCOV
196
                if ( ! $is_dismissing ) {
×
UNCOV
197
                        $is_dismissing = ( self::get_user_input( $dismissal_key ) === '1' );
×
198
                }
199

UNCOV
200
                if ( ! $is_dismissing ) {
×
UNCOV
201
                        return false;
×
202
                }
203

204
                $user_nonce = self::get_user_input( 'nonce' );
×
205
                if ( wp_verify_nonce( $user_nonce, $notification_id ) === false ) {
×
206
                        return false;
×
207
                }
208

209
                return self::dismiss_notification( $notification, $meta_value );
×
210
        }
211

212
        /**
213
         * Dismisses a notification.
214
         *
215
         * @param Yoast_Notification $notification Notification to dismiss.
216
         * @param string             $meta_value   Value to save in the dismissal.
217
         *
218
         * @return bool True if dismissed, false otherwise.
219
         */
UNCOV
220
        public static function dismiss_notification( Yoast_Notification $notification, $meta_value = 'seen' ) {
×
221
                // Dismiss notification.
UNCOV
222
                return update_user_option( get_current_user_id(), $notification->get_dismissal_key(), $meta_value ) !== false;
×
223
        }
224

225
        /**
226
         * Restores a notification.
227
         *
228
         * @param Yoast_Notification $notification Notification to restore.
229
         *
230
         * @return bool True if restored, false otherwise.
231
         */
UNCOV
232
        public static function restore_notification( Yoast_Notification $notification ) {
×
233

UNCOV
234
                $user_id       = get_current_user_id();
×
UNCOV
235
                $dismissal_key = $notification->get_dismissal_key();
×
236

237
                // Restore notification.
UNCOV
238
                $restored = delete_user_option( $user_id, $dismissal_key );
×
239

240
                // Delete unprefixed user meta too for backward-compatibility.
UNCOV
241
                if ( metadata_exists( 'user', $user_id, $dismissal_key ) ) {
×
UNCOV
242
                        $restored = delete_user_meta( $user_id, $dismissal_key ) && $restored;
×
243
                }
244

UNCOV
245
                return $restored;
×
246
        }
247

248
        /**
249
         * Clear dismissal information for the specified Notification.
250
         *
251
         * When a cause is resolved, the next time it is present we want to show
252
         * the message again.
253
         *
254
         * @param string|Yoast_Notification $notification Notification to clear the dismissal of.
255
         *
256
         * @return bool
257
         */
UNCOV
258
        public function clear_dismissal( $notification ) {
×
259

UNCOV
260
                global $wpdb;
×
261

UNCOV
262
                if ( $notification instanceof Yoast_Notification ) {
×
UNCOV
263
                        $dismissal_key = $notification->get_dismissal_key();
×
264
                }
265

UNCOV
266
                if ( is_string( $notification ) ) {
×
UNCOV
267
                        $dismissal_key = $notification;
×
268
                }
269

UNCOV
270
                if ( empty( $dismissal_key ) ) {
×
UNCOV
271
                        return false;
×
272
                }
273

274
                // Remove notification dismissal for all users.
UNCOV
275
                $deleted = delete_metadata( 'user', 0, $wpdb->get_blog_prefix() . $dismissal_key, '', true );
×
276

277
                // Delete unprefixed user meta too for backward-compatibility.
UNCOV
278
                $deleted = delete_metadata( 'user', 0, $dismissal_key, '', true ) || $deleted;
×
279

UNCOV
280
                return $deleted;
×
281
        }
282

283
        /**
284
         * Retrieves notifications from the storage and merges in previous notification changes.
285
         *
286
         * The current user in WordPress is not loaded shortly before the 'init' hook, but the plugin
287
         * sometimes needs to add or remove notifications before that. In such cases, the transactions
288
         * are not actually executed, but added to a queue. That queue is then handled in this method,
289
         * after notifications for the current user have been set up.
290
         *
291
         * @return void
292
         */
293
        public function setup_current_notifications() {
×
294
                $this->retrieve_notifications_from_storage( get_current_user_id() );
×
295

296
                foreach ( $this->queued_transactions as $transaction ) {
×
297
                        list( $callback, $args ) = $transaction;
×
298

299
                        call_user_func_array( $callback, $args );
×
300
                }
301

302
                $this->queued_transactions = [];
×
303
        }
304

305
        /**
306
         * Add notification to the cookie.
307
         *
308
         * @param Yoast_Notification $notification Notification object instance.
309
         *
310
         * @return void
311
         */
UNCOV
312
        public function add_notification( Yoast_Notification $notification ) {
×
313

UNCOV
314
                $callback = [ $this, __FUNCTION__ ];
×
UNCOV
315
                $args     = func_get_args();
×
UNCOV
316
                if ( $this->queue_transaction( $callback, $args ) ) {
×
317
                        return;
×
318
                }
319

320
                // Don't add if the user can't see it.
UNCOV
321
                if ( ! $notification->display_for_current_user() ) {
×
UNCOV
322
                        return;
×
323
                }
324

UNCOV
325
                $notification_id = $notification->get_id();
×
UNCOV
326
                $user_id         = $notification->get_user_id();
×
327

328
                // Empty notifications are always added.
UNCOV
329
                if ( $notification_id !== '' ) {
×
330

331
                        // If notification ID exists in notifications, don't add again.
UNCOV
332
                        $present_notification = $this->get_notification_by_id( $notification_id, $user_id );
×
UNCOV
333
                        if ( ! is_null( $present_notification ) ) {
×
UNCOV
334
                                $this->remove_notification( $present_notification, false );
×
335
                        }
336

UNCOV
337
                        if ( is_null( $present_notification ) ) {
×
UNCOV
338
                                $this->new[] = $notification_id;
×
339
                        }
340
                }
341

342
                // Add to list.
UNCOV
343
                $this->notifications[ $user_id ][] = $notification;
×
344

UNCOV
345
                $this->notifications_need_storage = true;
×
346
        }
347

348
        /**
349
         * Get the notification by ID and user ID.
350
         *
351
         * @param string   $notification_id The ID of the notification to search for.
352
         * @param int|null $user_id         The ID of the user.
353
         *
354
         * @return Yoast_Notification|null
355
         */
356
        public function get_notification_by_id( $notification_id, $user_id = null ) {
×
357
                $user_id = self::get_user_id( $user_id );
×
358

359
                $notifications = $this->get_notifications_for_user( $user_id );
×
360

361
                foreach ( $notifications as $notification ) {
×
362
                        if ( $notification_id === $notification->get_id() ) {
×
363
                                return $notification;
×
364
                        }
365
                }
366

367
                return null;
×
368
        }
369

370
        /**
371
         * Display the notifications.
372
         *
373
         * @param bool $echo_as_json True when notifications should be printed directly.
374
         *
375
         * @return void
376
         */
UNCOV
377
        public function display_notifications( $echo_as_json = false ) {
×
378

379
                // Never display notifications for network admin.
UNCOV
380
                if ( is_network_admin() ) {
×
381
                        return;
×
382
                }
383

UNCOV
384
                $sorted_notifications = $this->get_sorted_notifications();
×
UNCOV
385
                $notifications        = array_filter( $sorted_notifications, [ $this, 'is_notification_persistent' ] );
×
386

UNCOV
387
                if ( empty( $notifications ) ) {
×
UNCOV
388
                        return;
×
389
                }
390

UNCOV
391
                array_walk( $notifications, [ $this, 'remove_notification' ] );
×
392

UNCOV
393
                $notifications = array_unique( $notifications );
×
UNCOV
394
                if ( $echo_as_json ) {
×
395
                        $notification_json = [];
×
396

397
                        foreach ( $notifications as $notification ) {
×
398
                                $notification_json[] = $notification->render();
×
399
                        }
400

401
                        // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: WPSEO_Utils::format_json_encode is safe.
402
                        echo WPSEO_Utils::format_json_encode( $notification_json );
×
403

404
                        return;
×
405
                }
406

UNCOV
407
                foreach ( $notifications as $notification ) {
×
408
                        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Reason: Temporarily disabled, see: https://github.com/Yoast/wordpress-seo-premium/issues/2510 and https://github.com/Yoast/wordpress-seo-premium/issues/2511.
UNCOV
409
                        echo $notification;
×
410
                }
411
        }
412

413
        /**
414
         * Remove notification after it has been displayed.
415
         *
416
         * @param Yoast_Notification $notification Notification to remove.
417
         * @param bool               $resolve      Resolve as fixed.
418
         *
419
         * @return void
420
         */
421
        public function remove_notification( Yoast_Notification $notification, $resolve = true ) {
×
422

423
                $callback = [ $this, __FUNCTION__ ];
×
424
                $args     = func_get_args();
×
425
                if ( $this->queue_transaction( $callback, $args ) ) {
×
426
                        return;
×
427
                }
428

429
                $index = false;
×
430

431
                // ID of the user to show the notification for, defaults to current user id.
432
                $user_id       = $notification->get_user_id();
×
433
                $notifications = $this->get_notifications_for_user( $user_id );
×
434

435
                // Match persistent Notifications by ID, non persistent by item in the array.
436
                if ( $notification->is_persistent() ) {
×
437
                        foreach ( $notifications as $current_index => $present_notification ) {
×
438
                                if ( $present_notification->get_id() === $notification->get_id() ) {
×
439
                                        $index = $current_index;
×
440
                                        break;
×
441
                                }
442
                        }
443
                }
444
                else {
445
                        $index = array_search( $notification, $notifications, true );
×
446
                }
447

448
                if ( $index === false ) {
×
449
                        return;
×
450
                }
451

452
                if ( $notification->is_persistent() && $resolve ) {
×
453
                        ++$this->resolved;
×
454
                        $this->clear_dismissal( $notification );
×
455
                }
456

457
                unset( $notifications[ $index ] );
×
458
                $this->notifications[ $user_id ] = array_values( $notifications );
×
459

460
                $this->notifications_need_storage = true;
×
461
        }
462

463
        /**
464
         * Removes a notification by its ID.
465
         *
466
         * @param string $notification_id The notification id.
467
         * @param bool   $resolve         Resolve as fixed.
468
         *
469
         * @return void
470
         */
UNCOV
471
        public function remove_notification_by_id( $notification_id, $resolve = true ) {
×
UNCOV
472
                $notification = $this->get_notification_by_id( $notification_id );
×
473

UNCOV
474
                if ( $notification === null ) {
×
UNCOV
475
                        return;
×
476
                }
477

UNCOV
478
                $this->remove_notification( $notification, $resolve );
×
UNCOV
479
                $this->notifications_need_storage = true;
×
480
        }
481

482
        /**
483
         * Get the notification count.
484
         *
485
         * @param bool $dismissed Count dismissed notifications.
486
         *
487
         * @return int Number of notifications
488
         */
UNCOV
489
        public function get_notification_count( $dismissed = false ) {
×
490

UNCOV
491
                $notifications = $this->get_notifications_for_user( get_current_user_id() );
×
UNCOV
492
                $notifications = array_filter( $notifications, [ $this, 'filter_persistent_notifications' ] );
×
493

UNCOV
494
                if ( ! $dismissed ) {
×
UNCOV
495
                        $notifications = array_filter( $notifications, [ $this, 'filter_dismissed_notifications' ] );
×
496
                }
497

UNCOV
498
                return count( $notifications );
×
499
        }
500

501
        /**
502
         * Get the number of notifications resolved this execution.
503
         *
504
         * These notifications have been resolved and should be counted when active again.
505
         *
506
         * @return int
507
         */
UNCOV
508
        public function get_resolved_notification_count() {
×
509

UNCOV
510
                return $this->resolved;
×
511
        }
512

513
        /**
514
         * Return the notifications sorted on type and priority.
515
         *
516
         * @return array|Yoast_Notification[] Sorted Notifications
517
         */
UNCOV
518
        public function get_sorted_notifications() {
×
UNCOV
519
                $notifications = $this->get_notifications_for_user( get_current_user_id() );
×
UNCOV
520
                if ( empty( $notifications ) ) {
×
UNCOV
521
                        return [];
×
522
                }
523

524
                // Sort by severity, error first.
UNCOV
525
                usort( $notifications, [ $this, 'sort_notifications' ] );
×
526

UNCOV
527
                return $notifications;
×
528
        }
529

530
        /**
531
         * AJAX display notifications.
532
         *
533
         * @return void
534
         */
535
        public function ajax_get_notifications() {
×
536
                $echo = false;
×
537
                // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form data.
538
                if ( isset( $_POST['version'] ) && is_string( $_POST['version'] ) ) {
×
539
                        // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are only comparing the variable in a condition.
540
                        $echo = wp_unslash( $_POST['version'] ) === '2';
×
541
                }
542

543
                // Display the notices.
544
                $this->display_notifications( $echo );
×
545

546
                // AJAX die.
547
                exit;
×
548
        }
549

550
        /**
551
         * Remove storage when the plugin is deactivated.
552
         *
553
         * @return void
554
         */
555
        public function deactivate_hook() {
×
556

557
                $this->clear_notifications();
×
558
        }
559

560
        /**
561
         * Returns the given user ID if it exists.
562
         * Otherwise, this function returns the ID of the current user.
563
         *
564
         * @param int $user_id The user ID to check.
565
         *
566
         * @return int The user ID to use.
567
         */
568
        private static function get_user_id( $user_id ) {
×
569
                if ( $user_id ) {
×
570
                        return $user_id;
×
571
                }
572
                return get_current_user_id();
×
573
        }
574

575
        /**
576
         * Splits the notifications on user ID.
577
         *
578
         * In other terms, it returns an associative array,
579
         * mapping user ID to a list of notifications for this user.
580
         *
581
         * @param array|Yoast_Notification[] $notifications The notifications to split.
582
         *
583
         * @return array The notifications, split on user ID.
584
         */
585
        private function split_on_user_id( $notifications ) {
×
586
                $split_notifications = [];
×
587
                foreach ( $notifications as $notification ) {
×
588
                        $split_notifications[ $notification->get_user_id() ][] = $notification;
×
589
                }
590
                return $split_notifications;
×
591
        }
592

593
        /**
594
         * Save persistent notifications to storage.
595
         *
596
         * We need to be able to retrieve these so they can be dismissed at any time during the execution.
597
         *
598
         * @since 3.2
599
         *
600
         * @return void
601
         */
UNCOV
602
        public function update_storage() {
×
603

UNCOV
604
                $notifications = $this->notifications;
×
605

606
                /**
607
                 * One array of Yoast_Notifications, merged from multiple arrays.
608
                 *
609
                 * @var Yoast_Notification[] $merged_notifications
610
                 */
UNCOV
611
                $merged_notifications = [];
×
UNCOV
612
                if ( ! empty( $notifications ) ) {
×
UNCOV
613
                        $merged_notifications = array_merge( ...$notifications );
×
614
                }
615

616
                /**
617
                 * Filter: 'yoast_notifications_before_storage' - Allows developer to filter notifications before saving them.
618
                 *
619
                 * @param Yoast_Notification[] $notifications
620
                 */
UNCOV
621
                $filtered_merged_notifications = apply_filters( 'yoast_notifications_before_storage', $merged_notifications );
×
622

623
                // The notifications were filtered and therefore need to be stored.
UNCOV
624
                if ( $merged_notifications !== $filtered_merged_notifications ) {
×
UNCOV
625
                        $merged_notifications             = $filtered_merged_notifications;
×
UNCOV
626
                        $this->notifications_need_storage = true;
×
627
                }
628

UNCOV
629
                $notifications = $this->split_on_user_id( $merged_notifications );
×
630

631
                // No notifications to store, clear storage if it was previously present.
UNCOV
632
                if ( empty( $notifications ) ) {
×
633
                        $this->remove_storage();
×
634

635
                        return;
×
636
                }
637

638
                // Only store notifications if changes are made.
UNCOV
639
                if ( $this->notifications_need_storage ) {
×
UNCOV
640
                        array_walk( $notifications, [ $this, 'store_notifications_for_user' ] );
×
641
                }
642
        }
643

644
        /**
645
         * Stores the notifications to its respective user's storage.
646
         *
647
         * @param array|Yoast_Notification[] $notifications The notifications to store.
648
         * @param int                        $user_id       The ID of the user for which to store the notifications.
649
         *
650
         * @return void
651
         */
652
        private function store_notifications_for_user( $notifications, $user_id ) {
×
653
                $notifications_as_arrays = array_map( [ $this, 'notification_to_array' ], $notifications );
×
654
                update_user_option( $user_id, self::STORAGE_KEY, $notifications_as_arrays );
×
655
        }
656

657
        /**
658
         * Provide a way to verify present notifications.
659
         *
660
         * @return array|Yoast_Notification[] Registered notifications.
661
         */
662
        public function get_notifications() {
×
663
                if ( ! $this->notifications ) {
×
664
                        return [];
×
665
                }
666
                return array_merge( ...$this->notifications );
×
667
        }
668

669
        /**
670
         * Returns the notifications for the given user.
671
         *
672
         * @param int $user_id The id of the user to check.
673
         *
674
         * @return Yoast_Notification[] The notifications for the user with the given ID.
675
         */
676
        public function get_notifications_for_user( $user_id ) {
×
677
                if ( array_key_exists( $user_id, $this->notifications ) ) {
×
678
                        return $this->notifications[ $user_id ];
×
679
                }
680
                return [];
×
681
        }
682

683
        /**
684
         * Get newly added notifications.
685
         *
686
         * @return array
687
         */
UNCOV
688
        public function get_new_notifications() {
×
689

UNCOV
690
                return array_map( [ $this, 'get_notification_by_id' ], $this->new );
×
691
        }
692

693
        /**
694
         * Get information from the User input.
695
         *
696
         * Note that this function does not handle nonce verification.
697
         *
698
         * @param string $key Key to retrieve.
699
         *
700
         * @return string non-sanitized value of key if set, an empty string otherwise.
701
         */
702
        private static function get_user_input( $key ) {
×
703
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing -- Reason: We are not processing form information and only using this variable in a comparison.
704
                $request_method = isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
×
705
                // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: This function does not sanitize variables.
706
                // phpcs:disable WordPress.Security.NonceVerification.Recommended,WordPress.Security.NonceVerification.Missing -- Reason: This function does not verify a nonce.
707
                if ( $request_method === 'POST' ) {
×
708
                        if ( isset( $_POST[ $key ] ) && is_string( $_POST[ $key ] ) ) {
×
709
                                return wp_unslash( $_POST[ $key ] );
×
710
                        }
711
                }
712
                elseif ( isset( $_GET[ $key ] ) && is_string( $_GET[ $key ] ) ) {
×
713
                        return wp_unslash( $_GET[ $key ] );
×
714
                }
715
                // phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
716
                return '';
×
717
        }
718

719
        /**
720
         * Retrieve the notifications from storage and fill the relevant property.
721
         *
722
         * @param int $user_id The ID of the user to retrieve notifications for.
723
         *
724
         * @return void
725
         */
UNCOV
726
        private function retrieve_notifications_from_storage( $user_id ) {
×
UNCOV
727
                if ( $this->notifications_retrieved ) {
×
728
                        return;
×
729
                }
730

UNCOV
731
                $this->notifications_retrieved = true;
×
732

UNCOV
733
                $stored_notifications = get_user_option( self::STORAGE_KEY, $user_id );
×
734

735
                // Check if notifications are stored.
UNCOV
736
                if ( empty( $stored_notifications ) ) {
×
737
                        return;
×
738
                }
739

UNCOV
740
                if ( is_array( $stored_notifications ) ) {
×
UNCOV
741
                        $notifications = array_map( [ $this, 'array_to_notification' ], $stored_notifications );
×
742

743
                        // Apply array_values to ensure we get a 0-indexed array.
UNCOV
744
                        $notifications = array_values( array_filter( $notifications, [ $this, 'filter_notification_current_user' ] ) );
×
745

UNCOV
746
                        $this->notifications[ $user_id ] = $notifications;
×
747
                }
748
        }
749

750
        /**
751
         * Sort on type then priority.
752
         *
753
         * @param Yoast_Notification $a Compare with B.
754
         * @param Yoast_Notification $b Compare with A.
755
         *
756
         * @return int 1, 0 or -1 for sorting offset.
757
         */
758
        private function sort_notifications( Yoast_Notification $a, Yoast_Notification $b ) {
×
759

760
                $a_type = $a->get_type();
×
761
                $b_type = $b->get_type();
×
762

763
                if ( $a_type === $b_type ) {
×
764
                        return WPSEO_Utils::calc( $b->get_priority(), 'compare', $a->get_priority() );
×
765
                }
766

767
                if ( $a_type === 'error' ) {
×
768
                        return -1;
×
769
                }
770

771
                if ( $b_type === 'error' ) {
×
772
                        return 1;
×
773
                }
774

775
                return 0;
×
776
        }
777

778
        /**
779
         * Clear local stored notifications.
780
         *
781
         * @return void
782
         */
783
        private function clear_notifications() {
×
784

785
                $this->notifications           = [];
×
786
                $this->notifications_retrieved = false;
×
787
        }
788

789
        /**
790
         * Filter out non-persistent notifications.
791
         *
792
         * @since 3.2
793
         *
794
         * @param Yoast_Notification $notification Notification to test for persistent.
795
         *
796
         * @return bool
797
         */
798
        private function filter_persistent_notifications( Yoast_Notification $notification ) {
×
799

800
                return $notification->is_persistent();
×
801
        }
802

803
        /**
804
         * Filter out dismissed notifications.
805
         *
806
         * @param Yoast_Notification $notification Notification to check.
807
         *
808
         * @return bool
809
         */
810
        private function filter_dismissed_notifications( Yoast_Notification $notification ) {
×
811

812
                return ! self::maybe_dismiss_notification( $notification );
×
813
        }
814

815
        /**
816
         * Convert Notification to array representation.
817
         *
818
         * @since 3.2
819
         *
820
         * @param Yoast_Notification $notification Notification to convert.
821
         *
822
         * @return array
823
         */
824
        private function notification_to_array( Yoast_Notification $notification ) {
×
825

826
                $notification_data = $notification->to_array();
×
827

828
                if ( isset( $notification_data['nonce'] ) ) {
×
829
                        unset( $notification_data['nonce'] );
×
830
                }
831

832
                return $notification_data;
×
833
        }
834

835
        /**
836
         * Convert stored array to Notification.
837
         *
838
         * @param array $notification_data Array to convert to Notification.
839
         *
840
         * @return Yoast_Notification
841
         */
842
        private function array_to_notification( $notification_data ) {
×
843

844
                if ( isset( $notification_data['options']['nonce'] ) ) {
×
845
                        unset( $notification_data['options']['nonce'] );
×
846
                }
847

848
                if ( isset( $notification_data['message'] )
×
849
                        && is_subclass_of( $notification_data['message'], Abstract_Presenter::class, false )
×
850
                ) {
851
                        $notification_data['message'] = $notification_data['message']->present();
×
852
                }
853

854
                if ( isset( $notification_data['options']['user'] ) ) {
×
855
                        $notification_data['options']['user_id'] = $notification_data['options']['user']->ID;
×
856
                        unset( $notification_data['options']['user'] );
×
857

858
                        $this->notifications_need_storage = true;
×
859
                }
860

861
                return new Yoast_Notification(
×
862
                        $notification_data['message'],
×
863
                        $notification_data['options']
×
864
                );
865
        }
866

867
        /**
868
         * Filter notifications that should not be displayed for the current user.
869
         *
870
         * @param Yoast_Notification $notification Notification to test.
871
         *
872
         * @return bool
873
         */
874
        private function filter_notification_current_user( Yoast_Notification $notification ) {
×
875
                return $notification->display_for_current_user();
×
876
        }
877

878
        /**
879
         * Checks if given notification is persistent.
880
         *
881
         * @param Yoast_Notification $notification The notification to check.
882
         *
883
         * @return bool True when notification is not persistent.
884
         */
885
        private function is_notification_persistent( Yoast_Notification $notification ) {
×
886
                return ! $notification->is_persistent();
×
887
        }
888

889
        /**
890
         * Queues a notification transaction for later execution if notifications are not yet set up.
891
         *
892
         * @param callable $callback Callback that performs the transaction.
893
         * @param array    $args     Arguments to pass to the callback.
894
         *
895
         * @return bool True if transaction was queued, false if it can be performed immediately.
896
         */
897
        private function queue_transaction( $callback, $args ) {
×
898
                if ( $this->notifications_retrieved ) {
×
899
                        return false;
×
900
                }
901

902
                $this->add_transaction_to_queue( $callback, $args );
×
903

904
                return true;
×
905
        }
906

907
        /**
908
         * Adds a notification transaction to the queue for later execution.
909
         *
910
         * @param callable $callback Callback that performs the transaction.
911
         * @param array    $args     Arguments to pass to the callback.
912
         *
913
         * @return void
914
         */
915
        private function add_transaction_to_queue( $callback, $args ) {
×
916
                $this->queued_transactions[] = [ $callback, $args ];
×
917
        }
918

919
        /**
920
         * Removes all notifications from storage.
921
         *
922
         * @return bool True when notifications got removed.
923
         */
UNCOV
924
        protected function remove_storage() {
×
UNCOV
925
                if ( ! $this->has_stored_notifications() ) {
×
UNCOV
926
                        return false;
×
927
                }
928

UNCOV
929
                delete_user_option( get_current_user_id(), self::STORAGE_KEY );
×
UNCOV
930
                return true;
×
931
        }
932

933
        /**
934
         * Checks if there are stored notifications.
935
         *
936
         * @return bool True when there are stored notifications.
937
         */
UNCOV
938
        protected function has_stored_notifications() {
×
UNCOV
939
                $stored_notifications = $this->get_stored_notifications();
×
940

UNCOV
941
                return ! empty( $stored_notifications );
×
942
        }
943

944
        /**
945
         * Retrieves the stored notifications.
946
         *
947
         * @codeCoverageIgnore
948
         *
949
         * @return array|false Array with notifications or false when not set.
950
         */
951
        protected function get_stored_notifications() {
952
                return get_user_option( self::STORAGE_KEY, get_current_user_id() );
953
        }
954
}
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