PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
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-premium-analytics / src / class-notices.php

class-notices.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-premium-analytics/src/class-notices.php

203 lines 7.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Computes the dashboard notices, merging WPCOM-stored dismissals with locally-derived flags.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8 namespace Automattic\Jetpack\PremiumAnalytics;
9
10 use Automattic\Jetpack\Connection\Client;
11 use Automattic\Jetpack\Stats\Options as Stats_Options;
12 use Jetpack_Options;
13 use WP_Error;
14
15 /**
16 * Ported from `Automattic\Jetpack\Stats_Admin\Notices` so premium-analytics owns the notices
17 * surface without depending on the (to-be-deprecated) stats-admin package. The WPCOM dismissal
18 * state is fetched/updated through the blog token; the opt-in/opt-out/feedback/GDPR flags are
19 * derived locally from `Stats\Options` and the environment.
20 */
21 class Notices {
22 const NOTICES_CACHE_KEY = 'jetpack_premium_analytics_notices_cache_key';
23 const OPT_OUT_NEW_STATS_NOTICE_ID = 'opt_out_new_stats';
24 const NEW_STATS_FEEDBACK_NOTICE_ID = 'new_stats_feedback';
25 const OPT_IN_NEW_STATS_NOTICE_ID = 'opt_in_new_stats';
26 const GDPR_COOKIE_CONSENT_NOTICE_ID = 'gdpr_cookie_consent';
27
28 const VIEWS_TO_SHOW_FEEDBACK = 3;
29 const POSTPONE_OPT_IN_NOTICE_DAYS = 30;
30
31 /**
32 * How long the WPCOM dismissal state stays cached. While stats-admin runs in parallel, both
33 * surfaces read the same WPCOM resource under separate cache keys, so a dismissal through one
34 * can leave the other stale for up to this window — acceptable during the migration.
35 *
36 * @var int
37 */
38 const CACHE_TTL = 5 * MINUTE_IN_SECONDS;
39
40 /**
41 * Update notice status.
42 *
43 * @param mixed $id ID of the notice.
44 * @param mixed $status Status of the notice.
45 * @param int $postponed_for Postponed for how many seconds.
46 * @return array|WP_Error
47 */
48 public function update_notice( $id, $status, $postponed_for = 0 ) {
49 delete_transient( self::NOTICES_CACHE_KEY );
50
51 return $this->request_as_blog(
52 array(
53 'timeout' => 5,
54 'method' => 'POST',
55 'headers' => array( 'Content-Type' => 'application/json' ),
56 ),
57 wp_json_encode(
58 array(
59 'id' => $id,
60 'status' => $status,
61 'postponed_for' => $postponed_for,
62 ),
63 JSON_UNESCAPED_SLASHES
64 )
65 );
66 }
67
68 /**
69 * Return an array of notice IDs as keys and a boolean flagging whether to show them.
70 *
71 * @param bool $bypass_cache Refetch the WPCOM dismissal state instead of using the cached copy.
72 * @return array
73 */
74 public function get_notices_to_show( bool $bypass_cache = false ) {
75 // Fetch the WPCOM map once and reuse it for every flag, so a force-refresh costs a single
76 // round-trip rather than one per shared-key lookup.
77 $notices_wpcom = $this->get_notices_from_wpcom( $bypass_cache );
78
79 $new_stats_enabled = Stats_Options::get_option( 'enable_odyssey_stats' );
80 $stats_views = intval( Stats_Options::get_option( 'views' ) );
81 $odyssey_stats_changed_at = intval( Stats_Options::get_option( 'odyssey_stats_changed_at' ) );
82
83 // Check if Jetpack is integrated with the Complianz plugin, which blocks the Stats.
84 $complianz_options_integrations = get_option( 'complianz_options_integrations' );
85 $is_jetpack_blocked_by_complianz = ! isset( $complianz_options_integrations['jetpack'] ) || $complianz_options_integrations['jetpack'];
86
87 return array_merge(
88 $notices_wpcom,
89 array(
90 // Show Opt-in notice 30 days after the new stats being disabled.
91 self::OPT_IN_NEW_STATS_NOTICE_ID => ! $new_stats_enabled
92 && $odyssey_stats_changed_at < time() - self::POSTPONE_OPT_IN_NOTICE_DAYS * DAY_IN_SECONDS
93 && ! $this->is_hidden( $notices_wpcom, self::OPT_IN_NEW_STATS_NOTICE_ID ),
94
95 // Show feedback notice after 3 views of the new stats.
96 self::NEW_STATS_FEEDBACK_NOTICE_ID => $new_stats_enabled
97 && $stats_views >= self::VIEWS_TO_SHOW_FEEDBACK
98 && ! $this->is_hidden( $notices_wpcom, self::NEW_STATS_FEEDBACK_NOTICE_ID ),
99
100 // Show opt-out notice before 3 views of the new stats, where 3 is included.
101 self::OPT_OUT_NEW_STATS_NOTICE_ID => $new_stats_enabled
102 && $stats_views < self::VIEWS_TO_SHOW_FEEDBACK
103 && ! $this->is_hidden( $notices_wpcom, self::OPT_OUT_NEW_STATS_NOTICE_ID ),
104
105 // GDPR cookie consent notice for Complianz users.
106 self::GDPR_COOKIE_CONSENT_NOTICE_ID => class_exists( 'COMPLIANZ' ) && $is_jetpack_blocked_by_complianz
107 && ! $this->is_hidden( $notices_wpcom, self::GDPR_COOKIE_CONSENT_NOTICE_ID ),
108 )
109 );
110 }
111
112 /**
113 * Get the array of notice dismissal flags stored on WPCOM, cached for {@see CACHE_TTL}.
114 *
115 * @param bool $bypass_cache Refetch instead of returning the cached copy.
116 * @return array
117 */
118 public function get_notices_from_wpcom( bool $bypass_cache = false ) {
119 if ( ! $bypass_cache ) {
120 $cached = get_transient( self::NOTICES_CACHE_KEY );
121 if ( false !== $cached ) {
122 $decoded = json_decode( $cached, true );
123 return is_array( $decoded ) ? $decoded : array();
124 }
125 }
126
127 $notices_wpcom = $this->request_as_blog( array( 'timeout' => 5 ) );
128 if ( is_wp_error( $notices_wpcom ) ) {
129 return array();
130 }
131
132 set_transient( self::NOTICES_CACHE_KEY, wp_json_encode( $notices_wpcom, JSON_UNESCAPED_SLASHES ), self::CACHE_TTL );
133
134 return $notices_wpcom;
135 }
136
137 /**
138 * Checks if a notice is hidden, fetching the WPCOM dismissal state.
139 *
140 * @param mixed $id ID of the notice.
141 * @return bool
142 */
143 public function is_notice_hidden( $id ) {
144 return $this->is_hidden( $this->get_notices_from_wpcom(), $id );
145 }
146
147 /**
148 * Whether a notice is hidden in an already-fetched WPCOM dismissal map.
149 *
150 * @param array $notices_wpcom The WPCOM dismissal map.
151 * @param mixed $id ID of the notice.
152 * @return bool
153 */
154 private function is_hidden( array $notices_wpcom, $id ) {
155 return array_key_exists( $id, $notices_wpcom ) && $notices_wpcom[ $id ] === false;
156 }
157
158 /**
159 * Send a blog-token request to the WPCOM notices endpoint and return its decoded body.
160 *
161 * @param array $args Request arguments passed to the connection client.
162 * @param string|null $body Request body, for writes.
163 * @return array|WP_Error Decoded response body, or a WP_Error on transport or API error.
164 */
165 private function request_as_blog( array $args, $body = null ) {
166 $response = Client::wpcom_json_api_request_as_blog(
167 sprintf( '/sites/%d/jetpack-stats-dashboard/notices', Jetpack_Options::get_option( 'id' ) ),
168 'v2',
169 $args,
170 $body,
171 'wpcom'
172 );
173
174 if ( is_wp_error( $response ) ) {
175 return $response;
176 }
177
178 $response_code = (int) wp_remote_retrieve_response_code( $response );
179 $decoded = json_decode( wp_remote_retrieve_body( $response ), true );
180
181 $error_code = null;
182 foreach ( array( 'code', 'error' ) as $key ) {
183 if ( isset( $decoded[ $key ] ) ) {
184 $error_code = $decoded[ $key ];
185 break;
186 }
187 }
188
189 if ( null !== $error_code || 200 !== $response_code ) {
190 // Fall back to a non-empty code so WP_Error keeps the status/message instead of
191 // constructing an empty error (WP_Error::__construct bails on an empty code).
192 return new WP_Error(
193 $error_code ?? 'notices_request_failed',
194 is_array( $decoded ) ? ( $decoded['message'] ?? 'unknown remote error' ) : 'unknown remote error',
195 array( 'status' => $response_code )
196 );
197 }
198
199 // The notices endpoint always returns a JSON object; coerce anything else to an empty map.
200 return is_array( $decoded ) ? $decoded : array();
201 }
202 }
203