| 1 |
<?php |
| 2 |
/** |
| 3 |
* Analytics Consent Notice |
| 4 |
* |
| 5 |
* Asks admins on existing installs, who never saw the setup wizard's consent |
| 6 |
* toggle, whether they want to share usage analytics. |
| 7 |
* |
| 8 |
* @package Metasync |
| 9 |
* @subpackage Metasync/includes |
| 10 |
*/ |
| 11 |
|
| 12 |
if (!defined('ABSPATH')) { |
| 13 |
exit; |
| 14 |
} |
| 15 |
|
| 16 |
/** |
| 17 |
* Class Metasync_Analytics_Consent_Notice |
| 18 |
* |
| 19 |
* Shown on this plugin's own admin pages while analytics consent is off. |
| 20 |
* "Not now" (or the X) hides it for SNOOZE_DAYS; it comes back after that |
| 21 |
* for as long as consent stays off. |
| 22 |
*/ |
| 23 |
class Metasync_Analytics_Consent_Notice { |
| 24 |
|
| 25 |
/** |
| 26 |
* Singleton instance |
| 27 |
* |
| 28 |
* @var Metasync_Analytics_Consent_Notice|null |
| 29 |
*/ |
| 30 |
private static $instance = null; |
| 31 |
|
| 32 |
/** |
| 33 |
* Days the notice stays hidden after "Not now". |
| 34 |
*/ |
| 35 |
public const SNOOZE_DAYS = 10; |
| 36 |
|
| 37 |
/** |
| 38 |
* Unix time until which the notice is hidden. Site-wide, like the consent itself. |
| 39 |
*/ |
| 40 |
public const SNOOZE_OPTION = 'metasync_analytics_notice_snoozed_until'; |
| 41 |
|
| 42 |
private const NONCE_ACTION = 'metasync_analytics_consent_notice'; |
| 43 |
|
| 44 |
/** |
| 45 |
* Private constructor for singleton pattern |
| 46 |
*/ |
| 47 |
private function __construct() { |
| 48 |
add_action('admin_notices', [$this, 'display_notice']); |
| 49 |
add_action('wp_ajax_metasync_analytics_consent_allow', [$this, 'ajax_allow']); |
| 50 |
add_action('wp_ajax_metasync_analytics_consent_snooze', [$this, 'ajax_snooze']); |
| 51 |
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']); |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* Get singleton instance |
| 56 |
* |
| 57 |
* @return Metasync_Analytics_Consent_Notice |
| 58 |
*/ |
| 59 |
public static function get_instance() { |
| 60 |
if (self::$instance === null) { |
| 61 |
self::$instance = new self(); |
| 62 |
} |
| 63 |
return self::$instance; |
| 64 |
} |
| 65 |
|
| 66 |
/** |
| 67 |
* Whether the notice should show to the current user right now. |
| 68 |
* |
| 69 |
* @return bool |
| 70 |
*/ |
| 71 |
public function should_display() { |
| 72 |
if (!current_user_can('manage_options')) { |
| 73 |
return false; |
| 74 |
} |
| 75 |
|
| 76 |
if (get_option('metasync_analytics_opt_in', 'no') === 'yes') { |
| 77 |
return false; |
| 78 |
} |
| 79 |
|
| 80 |
return time() >= (int) get_option(self::SNOOZE_OPTION, 0); |
| 81 |
} |
| 82 |
|
| 83 |
/** |
| 84 |
* Whether the current admin screen is one of this plugin's own pages. |
| 85 |
* |
| 86 |
* The slug is whitelabellable, so match Metasync_Admin::$page_slug and its |
| 87 |
* `{slug}-...` sub-pages rather than a fixed string. |
| 88 |
* |
| 89 |
* @return bool |
| 90 |
*/ |
| 91 |
public function is_plugin_admin_page() { |
| 92 |
if (empty($_GET['page'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check for notice placement |
| 93 |
return false; |
| 94 |
} |
| 95 |
|
| 96 |
$page = sanitize_text_field(wp_unslash($_GET['page'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check for notice placement |
| 97 |
$slug = class_exists('Metasync_Admin') && !empty(Metasync_Admin::$page_slug) |
| 98 |
? Metasync_Admin::$page_slug |
| 99 |
: 'searchatlas'; |
| 100 |
|
| 101 |
return $page === $slug || strpos($page, $slug . '-') === 0; |
| 102 |
} |
| 103 |
|
| 104 |
/** |
| 105 |
* admin_notices callback. |
| 106 |
*/ |
| 107 |
public function display_notice() { |
| 108 |
if (!$this->is_plugin_admin_page() || !$this->should_display()) { |
| 109 |
return; |
| 110 |
} |
| 111 |
|
| 112 |
$plugin_name = class_exists('Metasync') ? Metasync::get_effective_plugin_name() : 'Search Atlas'; |
| 113 |
$slug = class_exists('Metasync_Admin') && !empty(Metasync_Admin::$page_slug) |
| 114 |
? Metasync_Admin::$page_slug |
| 115 |
: 'searchatlas'; |
| 116 |
$settings_url = admin_url('admin.php?page=' . $slug); |
| 117 |
?> |
| 118 |
<div id="metasync-analytics-consent-notice" class="notice notice-info is-dismissible" style="padding: 12px 16px; border-left-color: #2271b1;"> |
| 119 |
<div style="display: flex; align-items: flex-start; gap: 14px;"> |
| 120 |
<span class="dashicons dashicons-chart-bar" style="font-size: 28px; width: 28px; height: 28px; color: #2271b1;" aria-hidden="true"></span> |
| 121 |
<div style="flex: 1;"> |
| 122 |
<p style="margin: 0 0 6px 0; font-size: 14px; font-weight: 600;"> |
| 123 |
<?php |
| 124 |
/* translators: %s: Plugin name. */ |
| 125 |
echo esc_html(sprintf(__('Help improve %s', 'metasync'), $plugin_name)); |
| 126 |
?> |
| 127 |
</p> |
| 128 |
<p class="metasync-analytics-consent-message" style="margin: 0 0 6px 0; font-size: 13px;"> |
| 129 |
<?php |
| 130 |
/* translators: %s: Plugin name. */ |
| 131 |
echo esc_html(sprintf(__('Allow %s to collect limited usage analytics. This helps us understand which features are used, improve the product, and identify problems.', 'metasync'), $plugin_name)); |
| 132 |
?> |
| 133 |
</p> |
| 134 |
<p class="metasync-analytics-consent-details" style="margin: 0 0 10px 0; font-size: 12px; color: #646970;"> |
| 135 |
<?php esc_html_e('We collect limited technical information such as your site URL, plugin version, WordPress and PHP versions, user role, and feature usage. We do not collect passwords, API keys, customer content, names, or email addresses.', 'metasync'); ?> |
| 136 |
<a href="<?php echo esc_url($settings_url); ?>"><?php esc_html_e('You can change this anytime in Settings.', 'metasync'); ?></a> |
| 137 |
</p> |
| 138 |
<?php // no-loading: the dashboard script otherwise disables plugin buttons on click, and jQuery skips delegated clicks on disabled buttons. ?> |
| 139 |
<p class="metasync-analytics-consent-actions" style="margin: 0; display: flex; gap: 10px; flex-wrap: wrap;"> |
| 140 |
<button type="button" class="button button-primary no-loading metasync-analytics-consent-allow"> |
| 141 |
<?php esc_html_e('Allow', 'metasync'); ?> |
| 142 |
</button> |
| 143 |
<button type="button" class="button no-loading metasync-analytics-consent-snooze"> |
| 144 |
<?php esc_html_e('Not now', 'metasync'); ?> |
| 145 |
</button> |
| 146 |
</p> |
| 147 |
</div> |
| 148 |
</div> |
| 149 |
</div> |
| 150 |
<?php |
| 151 |
} |
| 152 |
|
| 153 |
/** |
| 154 |
* Enqueue the button handlers. |
| 155 |
* |
| 156 |
* @param string $hook Current admin page hook. |
| 157 |
*/ |
| 158 |
public function enqueue_admin_scripts($hook) { |
| 159 |
if (!$this->is_plugin_admin_page() || !$this->should_display()) { |
| 160 |
return; |
| 161 |
} |
| 162 |
|
| 163 |
wp_add_inline_script('jquery', $this->get_notice_script(), 'after'); |
| 164 |
} |
| 165 |
|
| 166 |
/** |
| 167 |
* Inline JS for the notice buttons. |
| 168 |
* |
| 169 |
* @return string JavaScript code. |
| 170 |
*/ |
| 171 |
private function get_notice_script() { |
| 172 |
$nonce = wp_create_nonce(self::NONCE_ACTION); |
| 173 |
$thanks = __('Thanks! Usage analytics are now on. You can turn them off anytime in Settings.', 'metasync'); |
| 174 |
$failed = __('Could not save your choice. Please try again, or use the toggle in Settings.', 'metasync'); |
| 175 |
|
| 176 |
return " |
| 177 |
jQuery(document).ready(function($) { |
| 178 |
var notice = $('#metasync-analytics-consent-notice'); |
| 179 |
var nonce = '" . esc_js($nonce) . "'; |
| 180 |
|
| 181 |
notice.on('click', '.metasync-analytics-consent-allow', function() { |
| 182 |
var button = $(this).prop('disabled', true); |
| 183 |
$.post(ajaxurl, { action: 'metasync_analytics_consent_allow', nonce: nonce }) |
| 184 |
.done(function(response) { |
| 185 |
if (response && response.success) { |
| 186 |
notice.removeClass('notice-info').addClass('notice-success'); |
| 187 |
notice.find('.metasync-analytics-consent-message').text('" . esc_js($thanks) . "'); |
| 188 |
notice.find('.metasync-analytics-consent-actions, .metasync-analytics-consent-details').remove(); |
| 189 |
} else { |
| 190 |
button.prop('disabled', false); |
| 191 |
notice.find('.metasync-analytics-consent-message').text('" . esc_js($failed) . "'); |
| 192 |
} |
| 193 |
}) |
| 194 |
.fail(function() { |
| 195 |
button.prop('disabled', false); |
| 196 |
notice.find('.metasync-analytics-consent-message').text('" . esc_js($failed) . "'); |
| 197 |
}); |
| 198 |
}); |
| 199 |
|
| 200 |
// 'Not now' and the X both snooze the notice. |
| 201 |
notice.on('click', '.metasync-analytics-consent-snooze, .notice-dismiss', function() { |
| 202 |
if (notice.hasClass('notice-success')) { |
| 203 |
return; |
| 204 |
} |
| 205 |
$.post(ajaxurl, { action: 'metasync_analytics_consent_snooze', nonce: nonce }); |
| 206 |
notice.fadeOut(); |
| 207 |
}); |
| 208 |
}); |
| 209 |
"; |
| 210 |
} |
| 211 |
|
| 212 |
/** |
| 213 |
* AJAX handler: turn analytics on. |
| 214 |
* |
| 215 |
* Saves the same two options as the setup wizard and the settings toggle, |
| 216 |
* so the choice counts as explicit consent. |
| 217 |
*/ |
| 218 |
public function ajax_allow() { |
| 219 |
check_ajax_referer(self::NONCE_ACTION, 'nonce'); |
| 220 |
|
| 221 |
if (!current_user_can('manage_options')) { |
| 222 |
wp_send_json_error(['message' => 'Insufficient permissions.']); |
| 223 |
// Unreachable in WordPress; keeps the handler safe under the |
| 224 |
// non-terminating stubs the unit tests run with. |
| 225 |
// @phpstan-ignore-next-line deadCode.unreachable |
| 226 |
return; |
| 227 |
} |
| 228 |
|
| 229 |
update_option('metasync_analytics_opt_in', 'yes'); |
| 230 |
update_option('metasync_analytics_consent_explicit', 'yes'); |
| 231 |
delete_option(self::SNOOZE_OPTION); |
| 232 |
|
| 233 |
wp_send_json_success(['message' => 'Analytics enabled']); |
| 234 |
} |
| 235 |
|
| 236 |
/** |
| 237 |
* AJAX handler: hide the notice for SNOOZE_DAYS. Consent stays off. |
| 238 |
*/ |
| 239 |
public function ajax_snooze() { |
| 240 |
check_ajax_referer(self::NONCE_ACTION, 'nonce'); |
| 241 |
|
| 242 |
if (!current_user_can('manage_options')) { |
| 243 |
wp_send_json_error(['message' => 'Insufficient permissions.']); |
| 244 |
// Same as above: defensive under test stubs, unreachable in WordPress. |
| 245 |
// @phpstan-ignore-next-line deadCode.unreachable |
| 246 |
return; |
| 247 |
} |
| 248 |
|
| 249 |
update_option(self::SNOOZE_OPTION, time() + (self::SNOOZE_DAYS * DAY_IN_SECONDS), false); |
| 250 |
|
| 251 |
wp_send_json_success(['message' => 'Notice snoozed']); |
| 252 |
} |
| 253 |
} |
| 254 |
|