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 |