PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-stats-admin / src / class-notices.php

class-notices.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at jetpack_vendor/automattic/jetpack-stats-admin/src/class-notices.php

235 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A class that handles the notices for the Stats Admin dashboard.
4 *
5 * @package automattic/jetpack-stats-admin
6 */
7
8 namespace Automattic\Jetpack\Stats_Admin;
9
10 use Automattic\Jetpack\Stats\Options as Stats_Options;
11 use Jetpack_Options;
12
13 /**
14 * The Notices class handles the notices for the Stats Admin dashboard.
15 *
16 * @package Automattic\Jetpack\Stats_Admin
17 */
18 class Notices {
19 const STATS_DASHBOARD_NOTICES_CACHE_KEY = 'jetpack_stats_dashboard_notices_cache_key';
20
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';
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
32 const VIEWS_TO_SHOW_FEEDBACK = 3;
33 const POSTPONE_OPT_IN_NOTICE_DAYS = 30;
34
35 /**
36 * Update notice status.
37 *
38 * @param mixed $id ID of the notice.
39 * @param mixed $status Status of the notice.
40 * @param int $postponed_for Postponed for how many seconds.
41 * @return bool
42 */
43 public function update_notice( $id, $status, $postponed_for = 0 ) {
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'
67 );
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;
71 }
72
73 /**
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.
83 *
84 * @param bool $include_details Return a detail record per notice instead of a bare flag.
85 * @return array
86 */
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
91 $new_stats_enabled = Stats_Options::get_option( 'enable_odyssey_stats' );
92 $stats_views = intval( Stats_Options::get_option( 'views' ) );
93 $odyssey_stats_changed_at = intval( Stats_Options::get_option( 'odyssey_stats_changed_at' ) );
94
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(
100 // Show Opt-in notice 30 days after the new stats being disabled.
101 self::OPT_IN_NEW_STATS_NOTICE_ID => ! $new_stats_enabled
102 && $odyssey_stats_changed_at < time() - self::POSTPONE_OPT_IN_NOTICE_DAYS * DAY_IN_SECONDS
103 && ! $this->is_hidden( $notices_wpcom, self::OPT_IN_NEW_STATS_NOTICE_ID ),
104
105 // Show feedback notice after 3 views of the new stats.
106 self::NEW_STATS_FEEDBACK_NOTICE_ID => $new_stats_enabled
107 && $stats_views >= self::VIEWS_TO_SHOW_FEEDBACK
108 && ! $this->is_hidden( $notices_wpcom, self::NEW_STATS_FEEDBACK_NOTICE_ID ),
109
110 // Show opt-out notice before 3 views of the new stats, where 3 is included.
111 self::OPT_OUT_NEW_STATS_NOTICE_ID => $new_stats_enabled
112 && $stats_views < self::VIEWS_TO_SHOW_FEEDBACK
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 ),
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 );
125 }
126
127 /**
128 * Normalize every notice to the detail shape, whichever shape WPCOM answered in.
129 *
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
137 */
138 private function to_detail_records( array $notices_wpcom, array $local_notices ) {
139 $notices = array();
140
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' )
163 );
164
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;
187 }
188
189 /**
190 * Checks if a notice is hidden.
191 *
192 * @param mixed $id ID of the notice.
193 * @return bool
194 */
195 public function is_notice_hidden( $id ) {
196 return $this->is_hidden( $this->get_notices_from_wpcom(), $id );
197 }
198
199 /**
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
205 */
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 );
233 }
234 }
235