PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.9.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.9.0
2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.7 2.6.8 2.6.5 2.6.4 2.6.3 2.6.2 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 trunk 1.0 1.0.1 1.0.2 1.0.3 1.1 1.1.1 1.1.2 1.2.0 2.0 2.1.0 2.1.1 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1
reviews-feed / class / Common / Services / MigrationReactivationNotice.php
reviews-feed / class / Common / Services Last commit date
Upgrade 1 month ago CLIService.php 1 month ago FeedCacheUpdateService.php 1 month ago MigrationReactivationNotice.php 1 month ago SBR_Upgrader.php 1 month ago SettingsManagerService.php 1 month ago ShortcodeService.php 1 month ago SiteUrlWatcher.php 1 month ago UsageTrackingService.php 1 month ago
MigrationReactivationNotice.php
204 lines
1 <?php
2
3 namespace SmashBalloon\Reviews\Common\Services;
4
5 use Smashballoon\Stubs\Services\ServiceProvider;
6
7 /**
8 * Admin notice surfaced after a successful silent license re-activation on
9 * a migrated site (SMASH-1281). Companion to
10 * `SBRelay::attempt_silent_reactivation()`.
11 *
12 * **Why the notice matters.**
13 * The silent path re-registers + re-activates the license on the NEW site
14 * URL, but EDD has no "transfer" operation — the OLD site's activation
15 * slot stays consumed until the user explicitly frees it. If they did a
16 * real one-way move (prod domain rename, backup restore) they almost
17 * certainly want to free that slot. The notice points them at the
18 * SmashBalloon account page where they can do so.
19 *
20 * **Lifecycle.**
21 * - Set: `SBRelay::attempt_silent_reactivation()` writes a transient
22 * `sbr_silent_reactivation_notice` on successful re-activation.
23 * - Read: this service renders on `admin_notices` if the transient
24 * is present.
25 * - Dismiss: a per-user option captures dismissal so it doesn't
26 * reappear after a cache flush wipes the transient.
27 * - Auto-expire: transient carries a `WEEK_IN_SECONDS` TTL as a
28 * safety net if the user never sees / dismisses the notice.
29 *
30 * @see SBRelay::attempt_silent_reactivation()
31 * @since SMASH-1281
32 */
33 class MigrationReactivationNotice extends ServiceProvider
34 {
35 public const TRANSIENT_KEY = 'sbr_silent_reactivation_notice';
36 public const DISMISS_META = 'sbr_silent_reactivation_notice_dismissed';
37 public const DISMISS_ACTION = 'sbr_dismiss_silent_reactivation_notice';
38
39 public function register()
40 {
41 if (!function_exists('add_action')) {
42 return;
43 }
44 add_action('admin_notices', [$this, 'maybe_render']);
45 add_action('wp_ajax_' . self::DISMISS_ACTION, [$this, 'ajax_dismiss']);
46 }
47
48 /**
49 * Render the notice if (a) transient is set, (b) current user hasn't
50 * already dismissed it, (c) current user has the capability to care.
51 *
52 * Restricts to administrators to avoid surfacing the account-page
53 * pointer to editors/authors who couldn't act on it anyway.
54 *
55 * @return void
56 */
57 public function maybe_render()
58 {
59 if (!function_exists('current_user_can') || !current_user_can('manage_options')) {
60 return;
61 }
62
63 $payload = function_exists('get_transient') ? get_transient(self::TRANSIENT_KEY) : false;
64 if (!is_array($payload)) {
65 return;
66 }
67
68 if (function_exists('get_user_meta') && function_exists('get_current_user_id')) {
69 $user_id = get_current_user_id();
70 if ($user_id > 0) {
71 $dismissed_ts = (int) get_user_meta($user_id, self::DISMISS_META, true);
72 // If the user dismissed this specific reactivation event
73 // (timestamps match), stay silent. A fresh migration + fresh
74 // reactivation writes a new timestamp and re-surfaces the
75 // notice — that's desired behavior.
76 if ($dismissed_ts > 0 && isset($payload['timestamp']) && (int) $payload['timestamp'] === $dismissed_ts) {
77 return;
78 }
79 }
80 }
81
82 $new_url = isset($payload['new_url']) && is_string($payload['new_url']) ? $payload['new_url'] : '';
83 $old_url = isset($payload['old_url']) && is_string($payload['old_url']) ? $payload['old_url'] : '';
84 $account_url = 'https://smashballoon.com/account/';
85 $nonce = function_exists('wp_create_nonce') ? wp_create_nonce(self::DISMISS_ACTION) : '';
86 $ajax_url = function_exists('admin_url') ? admin_url('admin-ajax.php') : '';
87 $timestamp = isset($payload['timestamp']) ? (int) $payload['timestamp'] : 0;
88
89 // `esc_*` might not be loaded in unusual bootstrap contexts (CLI,
90 // test harness). Defensive wrappers — skip rendering if they don't
91 // exist, rather than echoing raw data.
92 if (
93 !function_exists('esc_url')
94 || !function_exists('esc_html')
95 || !function_exists('esc_attr')
96 || !function_exists('esc_html__')
97 || !function_exists('esc_html_e')
98 ) {
99 return;
100 }
101
102 $detail_line = '';
103 if ($old_url !== '' && $new_url !== '') {
104 $detail_line = sprintf(
105 /* translators: 1: previous site URL, 2: current site URL */
106 esc_html__('Detected change from %1$s to %2$s.', 'reviews-feed'),
107 '<code>' . esc_html($old_url) . '</code>',
108 '<code>' . esc_html($new_url) . '</code>'
109 );
110 }
111
112 ?>
113 <div class="notice notice-success is-dismissible sbr-silent-reactivation-notice"
114 data-sbr-dismiss-action="<?php echo esc_attr(self::DISMISS_ACTION); ?>"
115 data-sbr-dismiss-nonce="<?php echo esc_attr($nonce); ?>"
116 data-sbr-dismiss-ts="<?php echo esc_attr((string) $timestamp); ?>"
117 data-sbr-dismiss-url="<?php echo esc_attr($ajax_url); ?>">
118 <p>
119 <strong><?php esc_html_e('Reviews Feed Pro', 'reviews-feed'); ?>:</strong>
120 <?php esc_html_e('Your site URL changed and we automatically reactivated your license on the new URL.', 'reviews-feed'); ?>
121 <?php if ($detail_line !== '') : ?>
122 <br><?php echo $detail_line; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above ?>
123 <?php endif; ?>
124 </p>
125 <p>
126 <?php esc_html_e('Your old site is still using one of your license activation slots. If you have fully migrated and no longer need it, you can free that slot from your SmashBalloon account.', 'reviews-feed'); ?>
127 <a href="<?php echo esc_url($account_url); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e('Manage activations →', 'reviews-feed'); ?></a>
128 </p>
129 </div>
130 <script>
131 (function () {
132 var notice = document.querySelector('.sbr-silent-reactivation-notice');
133 if (!notice) { return; }
134 notice.addEventListener('click', function (e) {
135 var btn = e.target && e.target.closest('.notice-dismiss');
136 if (!btn) { return; }
137 var action = notice.getAttribute('data-sbr-dismiss-action');
138 var nonce = notice.getAttribute('data-sbr-dismiss-nonce');
139 var ts = notice.getAttribute('data-sbr-dismiss-ts');
140 var url = notice.getAttribute('data-sbr-dismiss-url');
141 if (!action || !nonce || !url) { return; }
142 var data = new FormData();
143 data.append('action', action);
144 data.append('nonce', nonce);
145 data.append('timestamp', ts);
146 // Fire and forget — a failed dismissal just means the notice
147 // re-appears on the next admin page load, which is the
148 // pre-existing fallback for users on unusual caching setups.
149 if (window.fetch) {
150 window.fetch(url, { method: 'POST', credentials: 'same-origin', body: data });
151 }
152 });
153 })();
154 </script>
155 <?php
156 }
157
158 /**
159 * Persist per-user dismissal so a fresh transient load or server cache
160 * flush doesn't re-surface the same notice event. A subsequent fresh
161 * migration + fresh silent-reactivation writes a new timestamp and
162 * re-surfaces the notice — intended.
163 *
164 * @return void
165 */
166 public function ajax_dismiss()
167 {
168 if (
169 !function_exists('check_ajax_referer')
170 || !function_exists('current_user_can')
171 || !function_exists('get_current_user_id')
172 || !function_exists('update_user_meta')
173 || !function_exists('wp_send_json_success')
174 || !function_exists('wp_send_json_error')
175 ) {
176 return;
177 }
178 check_ajax_referer(self::DISMISS_ACTION, 'nonce');
179
180 if (!current_user_can('manage_options')) {
181 // wp_send_json_error exits internally — no explicit return needed.
182 wp_send_json_error(null, 403);
183 }
184
185 $timestamp = isset($_POST['timestamp']) ? (int) $_POST['timestamp'] : 0;
186 if ($timestamp <= 0) {
187 wp_send_json_error(null, 400);
188 }
189
190 $user_id = get_current_user_id();
191 if ($user_id > 0) {
192 update_user_meta($user_id, self::DISMISS_META, $timestamp);
193 }
194
195 // Opportunistically clear the transient too — other admins on the
196 // same site don't need to see the same notice again.
197 if (function_exists('delete_transient')) {
198 delete_transient(self::TRANSIENT_KEY);
199 }
200
201 wp_send_json_success();
202 }
203 }
204