PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-stats-admin/src/class-notices.php +162 -52 12.1.3 → 16.3 View file →
@@ -7,8 +7,9 @@
7 7
8 8 namespace Automattic\Jetpack\Stats_Admin;
9 9
10 10 use Automattic\Jetpack\Stats\Options as Stats_Options;
11 +use Jetpack_Options;
11 12
12 13 /**
13 14 * The Notices class handles the notices for the Stats Admin dashboard.
14 15 *
@@ -14,21 +15,26 @@
14 15 *
15 16 * @package Automattic\Jetpack\Stats_Admin
16 17 */
17 18 class Notices {
18 - const OPT_OUT_NEW_STATS_NOTICE_ID = 'opt_out_new_stats';
19 - const NEW_STATS_FEEDBACK_NOTICE_ID = 'new_stats_feedback';
20 - const OPT_IN_NEW_STATS_NOTICE_ID = 'opt_in_new_stats';
19 + const STATS_DASHBOARD_NOTICES_CACHE_KEY = 'jetpack_stats_dashboard_notices_cache_key';
21 20
22 - const NOTICE_STATUS_DISMISSED = 'dismissed';
23 - const NOTICE_STATUS_POSTPONED = 'postponed';
21 + /**
22 + * The flat map and the detail records are different shapes of the same resource, so they
23 + * cannot share a transient.
24 + */
25 + const STATS_DASHBOARD_NOTICES_DETAILS_CACHE_KEY = self::STATS_DASHBOARD_NOTICES_CACHE_KEY . '_details';
24 26
27 + const OPT_OUT_NEW_STATS_NOTICE_ID = 'opt_out_new_stats';
28 + const NEW_STATS_FEEDBACK_NOTICE_ID = 'new_stats_feedback';
29 + const OPT_IN_NEW_STATS_NOTICE_ID = 'opt_in_new_stats';
30 + const GDPR_COOKIE_CONSENT_NOTICE_ID = 'gdpr_cookie_consent';
31 +
25 32 const VIEWS_TO_SHOW_FEEDBACK = 3;
26 - const POSTPONE_FEEDBACK_DAYS = 30;
27 33 const POSTPONE_OPT_IN_NOTICE_DAYS = 30;
28 34
29 35 /**
30 - * Update the notice status.
36 + * Update notice status.
31 37 *
32 38 * @param mixed $id ID of the notice.
33 39 * @param mixed $status Status of the notice.
34 40 * @param int $postponed_for Postponed for how many seconds.
@@ -34,76 +40,151 @@
34 40 * @param int $postponed_for Postponed for how many seconds.
35 41 * @return bool
36 42 */
37 43 public function update_notice( $id, $status, $postponed_for = 0 ) {
38 - $notices = Stats_Options::get_option( 'notices' );
39 - $notices[ $id ] = array(
40 - 'status' => $status,
41 - 'id' => $id,
42 - 'dismissed_at' => time(),
44 + $this->clear_cache();
45 + $response = WPCOM_Client::request_as_blog(
46 + sprintf(
47 + '/sites/%d/jetpack-stats-dashboard/notices',
48 + Jetpack_Options::get_option( 'id' )
49 + ),
50 + 'v2',
51 + array(
52 + 'timeout' => 5,
53 + 'method' => 'POST',
54 + 'headers' => array(
55 + 'Content-Type' => 'application/json',
56 + ),
57 + ),
58 + wp_json_encode(
59 + array(
60 + 'id' => $id,
61 + 'status' => $status,
62 + 'postponed_for' => $postponed_for,
63 + ),
64 + JSON_UNESCAPED_SLASHES
65 + ),
66 + 'wpcom'
43 67 );
44 - // Set the next show time if the notice is postponed.
45 - if ( $status === self::NOTICE_STATUS_POSTPONED ) {
46 - $notices[ $id ]['next_show_at'] = time() + ( $postponed_for > 0 ? $postponed_for : self::POSTPONE_FEEDBACK_DAYS * DAY_IN_SECONDS );
47 - }
48 - return Stats_Options::set_option( 'notices', $notices );
68 + // A notices GET running alongside this POST can cache the pre-dismissal answer while the POST is in flight.
69 + $this->clear_cache();
70 + return $response;
49 71 }
50 72
51 73 /**
52 - * Return an array of notices IDs as keys and their value to flag whther to show them.
74 + * Drop both cached shapes of the WPCOM notices response.
75 + */
76 + private function clear_cache() {
77 + delete_transient( self::STATS_DASHBOARD_NOTICES_CACHE_KEY );
78 + delete_transient( self::STATS_DASHBOARD_NOTICES_DETAILS_CACHE_KEY );
79 + }
80 +
81 + /**
82 + * Return an array of notices IDs as keys and their value to flag whether to show them.
53 83 *
84 + * @param bool $include_details Return a detail record per notice instead of a bare flag.
54 85 * @return array
55 86 */
56 - public function get_notices_to_show() {
87 + public function get_notices_to_show( bool $include_details = false ) {
88 + // Reuse the one fetch for every flag below, so details mode doesn't also pull the flat map.
89 + $notices_wpcom = $this->get_notices_from_wpcom( $include_details );
90 +
57 91 $new_stats_enabled = Stats_Options::get_option( 'enable_odyssey_stats' );
58 - $stats_views = $this->get_new_stats_views();
92 + $stats_views = intval( Stats_Options::get_option( 'views' ) );
59 93 $odyssey_stats_changed_at = intval( Stats_Options::get_option( 'odyssey_stats_changed_at' ) );
60 94
61 - return array(
95 + // Check if Jetpack is integrated with the Complianz plugin, which blocks the Stats.
96 + $complianz_options_integrations = get_option( 'complianz_options_integrations' );
97 + $is_jetpack_blocked_by_complianz = ! isset( $complianz_options_integrations['jetpack'] ) || $complianz_options_integrations['jetpack'];
98 +
99 + $local_notices = array(
62 100 // Show Opt-in notice 30 days after the new stats being disabled.
63 - self::OPT_IN_NEW_STATS_NOTICE_ID => ! $new_stats_enabled
101 + self::OPT_IN_NEW_STATS_NOTICE_ID => ! $new_stats_enabled
64 102 && $odyssey_stats_changed_at < time() - self::POSTPONE_OPT_IN_NOTICE_DAYS * DAY_IN_SECONDS
65 - && ! $this->is_notice_hidden( self::OPT_IN_NEW_STATS_NOTICE_ID ),
103 + && ! $this->is_hidden( $notices_wpcom, self::OPT_IN_NEW_STATS_NOTICE_ID ),
66 104
67 105 // Show feedback notice after 3 views of the new stats.
68 - self::NEW_STATS_FEEDBACK_NOTICE_ID => $new_stats_enabled
106 + self::NEW_STATS_FEEDBACK_NOTICE_ID => $new_stats_enabled
69 107 && $stats_views >= self::VIEWS_TO_SHOW_FEEDBACK
70 - && ! $this->is_notice_hidden( self::NEW_STATS_FEEDBACK_NOTICE_ID ),
108 + && ! $this->is_hidden( $notices_wpcom, self::NEW_STATS_FEEDBACK_NOTICE_ID ),
71 109
72 110 // Show opt-out notice before 3 views of the new stats, where 3 is included.
73 - self::OPT_OUT_NEW_STATS_NOTICE_ID => $new_stats_enabled
111 + self::OPT_OUT_NEW_STATS_NOTICE_ID => $new_stats_enabled
74 112 && $stats_views < self::VIEWS_TO_SHOW_FEEDBACK
75 - && ! $this->is_notice_hidden( self::OPT_OUT_NEW_STATS_NOTICE_ID ),
113 + && ! $this->is_hidden( $notices_wpcom, self::OPT_OUT_NEW_STATS_NOTICE_ID ),
114 +
115 + // GDPR cookie consent notice for Complianz users.
116 + self::GDPR_COOKIE_CONSENT_NOTICE_ID => class_exists( 'COMPLIANZ' ) && $is_jetpack_blocked_by_complianz
117 + && ! $this->is_hidden( $notices_wpcom, self::GDPR_COOKIE_CONSENT_NOTICE_ID ),
76 118 );
119 +
120 + if ( $include_details ) {
121 + return $this->to_detail_records( $notices_wpcom, $local_notices );
122 + }
123 +
124 + return array_merge( $notices_wpcom, $local_notices );
77 125 }
78 126
79 127 /**
80 - * Returns the array of hidden notices.
128 + * Normalize every notice to the detail shape, whichever shape WPCOM answered in.
81 129 *
82 - * @return array Array of hidden notices.
130 + * The package ships to self-hosted sites and can run for months against a WPCOM that does not
131 + * serve `include_details` yet, so a passed-through flat value would leave the caller parsing
132 + * two shapes in one response.
133 + *
134 + * @param array $notices_wpcom The WPCOM response, flat map or detail records.
135 + * @param array $local_notices The locally-computed visibility flags.
136 + * @return array
83 137 */
84 - public function get_hidden_notices() {
85 - $notices = Stats_Options::get_option( 'notices' );
138 + private function to_detail_records( array $notices_wpcom, array $local_notices ) {
139 + $notices = array();
86 140
87 - $hidden_notices = array_filter(
88 - $notices,
89 - function ( $notice ) {
90 - if ( ! isset( $notice['status'] ) ) {
91 - return false;
92 - }
93 - switch ( $notice['status'] ) {
94 - case self::NOTICE_STATUS_DISMISSED:
95 - return true;
96 - case self::NOTICE_STATUS_POSTPONED:
97 - return empty( $notice['next_show_at'] ) || $notice['next_show_at'] > time();
98 - default:
99 - return false;
100 - }
101 - },
102 - ARRAY_FILTER_USE_BOTH
141 + foreach ( array_keys( array_merge( $notices_wpcom, $local_notices ) ) as $id ) {
142 + // A local notice keeps its own visibility; its escalation fields still come from WPCOM.
143 + $show = array_key_exists( $id, $local_notices )
144 + ? (bool) $local_notices[ $id ]
145 + : ! $this->is_hidden( $notices_wpcom, $id );
146 +
147 + $notices[ $id ] = $this->to_detail_record( $notices_wpcom[ $id ] ?? null, $show );
148 + }
149 +
150 + return $notices;
151 + }
152 +
153 + /**
154 + * Get the array of hidden notices from WPCOM.
155 + *
156 + * @param bool $include_details Ask WPCOM for detail records instead of a flat map.
157 + * @return array
158 + */
159 + public function get_notices_from_wpcom( bool $include_details = false ) {
160 + $path = sprintf(
161 + '/sites/%d/jetpack-stats-dashboard/notices',
162 + Jetpack_Options::get_option( 'id' )
103 163 );
104 164
105 - return $hidden_notices;
165 + if ( $include_details ) {
166 + $path .= '?include_details=true';
167 + }
168 +
169 + $notices_wpcom = WPCOM_Client::request_as_blog_cached(
170 + $path,
171 + 'v2',
172 + array(
173 + 'timeout' => 5,
174 + ),
175 + null,
176 + 'wpcom',
177 + true,
178 + $include_details
179 + ? static::STATS_DASHBOARD_NOTICES_DETAILS_CACHE_KEY
180 + : static::STATS_DASHBOARD_NOTICES_CACHE_KEY
181 + );
182 +
183 + if ( is_wp_error( $notices_wpcom ) ) {
184 + return array();
185 + }
186 + return $notices_wpcom;
106 187 }
107 188
108 189 /**
109 190 * Checks if a notice is hidden.
@@ -111,14 +192,43 @@
111 192 * @param mixed $id ID of the notice.
112 193 * @return bool
113 194 */
114 195 public function is_notice_hidden( $id ) {
115 - return array_key_exists( $id, $this->get_hidden_notices() );
196 + return $this->is_hidden( $this->get_notices_from_wpcom(), $id );
116 197 }
117 198
118 199 /**
119 - * Returns the number of views of the new stats dashboard.
200 + * Whether a notice is hidden in an already-fetched WPCOM response, in either shape.
201 + *
202 + * @param array $notices_wpcom The WPCOM response, flat map or detail records.
203 + * @param mixed $id ID of the notice.
204 + * @return bool
120 205 */
121 - public function get_new_stats_views() {
122 - return Stats_Options::get_option( 'views' );
206 + private function is_hidden( array $notices_wpcom, $id ) {
207 + if ( ! array_key_exists( $id, $notices_wpcom ) ) {
208 + return false;
209 + }
210 +
211 + $record = $notices_wpcom[ $id ];
212 +
213 + return is_array( $record ) ? empty( $record['show'] ) : $record === false;
214 + }
215 +
216 + /**
217 + * Wrap a locally-computed flag in the detail shape, filling the escalation fields from WPCOM.
218 + *
219 + * @param mixed $wpcom_record The WPCOM detail record for this notice, or null when it has none.
220 + * @param bool $show The locally-computed visibility flag.
221 + * @return array
222 + */
223 + private function to_detail_record( $wpcom_record, bool $show ) {
224 + $record = is_array( $wpcom_record ) ? $wpcom_record : array();
225 +
226 + return array(
227 + 'show' => $show,
228 + 'status' => $record['status'] ?? null,
229 + 'postponed_count' => (int) ( $record['postponed_count'] ?? 0 ),
230 + // A non-numeric value would cast to 0, which reads as "due now" rather than "not scheduled".
231 + 'next_show_at' => is_numeric( $record['next_show_at'] ?? null ) ? (int) $record['next_show_at'] : null,
232 + );
123 233 }
124 234 }