PluginProbe
MarqueeX – Smooth Marquee Slider, News Ticker & Post Marquee for Block Editor & Elementor / 0.6.0
MarqueeX – Smooth Marquee Slider, News Ticker & Post Marquee for Block Editor & Elementor v0.6.0
0.6.0 0.5.4 0.5.3 0.5.2 0.5.1 0.5.0 0.4.0 0.3.3 0.3.2 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.1.0 0.1.1 0.1.2 0.2.0 0.2.1 0.2.2 0.2.3 0.3.0 0.3.1
marqueex / includes / Admin / ReviewRequest.php

ReviewRequest.php in MarqueeX – Smooth Marquee Slider, News Ticker & Post Marquee for Block Editor & Elementor 0.6.0, at includes/Admin/ReviewRequest.php

235 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Wpxero\Marqueex\Admin;
4
5 use Wpxero\Marqueex\Traits\Singleton;
6
7 if (!defined('ABSPATH')) {
8 exit;
9 }
10
11 /**
12 * A single, usage-triggered request for a WordPress.org review.
13 *
14 * Deliberate constraints, because review prompts are the easiest thing in a
15 * plugin to get wrong:
16 *
17 * - Triggered by USE, not by install date. The clock starts the first time a
18 * marquee is actually saved into content, so nobody is asked to vouch for
19 * something they have not used.
20 * - Shown only after a week of that, and only on MarqueeX's own screens and
21 * the plugins list — never across the whole admin.
22 * - Three honest choices, one of which ends it permanently. "Maybe later"
23 * means a month, not a day.
24 * - No second prompt, ever, once dismissed or marked done.
25 */
26 class ReviewRequest {
27 use Singleton;
28
29 const OPTION = 'marqueex_review_state';
30
31 /** Days of use before asking. */
32 const DELAY_DAYS = 7;
33
34 /** Days to wait after "Maybe later". */
35 const SNOOZE_DAYS = 30;
36
37 const REVIEW_URL = 'https://wordpress.org/support/plugin/marqueex/reviews/#new-post';
38
39 private function __construct() {
40 add_action('save_post', [$this, 'record_first_use'], 10, 2);
41 add_action('admin_notices', [$this, 'maybe_render_notice']);
42 add_action('admin_init', [$this, 'handle_action']);
43 }
44
45 /**
46 * Note the first time a marquee is saved into a post.
47 *
48 * @param int $post_id
49 * @param \WP_Post $post
50 */
51 public function record_first_use($post_id, $post) {
52 if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
53 return;
54 }
55
56 $state = $this->get_state();
57 if (!empty($state['first_use'])) {
58 return; // Already recorded — nothing to do on every later save.
59 }
60
61 if (!$this->post_uses_marqueex($post)) {
62 return;
63 }
64
65 $state['first_use'] = time();
66 $this->save_state($state);
67 }
68
69 /**
70 * Whether a post contains a MarqueeX block or Elementor widget.
71 *
72 * @param \WP_Post $post
73 * @return bool
74 */
75 private function post_uses_marqueex($post) {
76 if (!is_object($post) || empty($post->post_content)) {
77 // Elementor keeps its layout in post meta rather than post_content.
78 return $this->elementor_data_uses_marqueex($post);
79 }
80
81 if (false !== strpos($post->post_content, '<!-- wp:marqueex/')) {
82 return true;
83 }
84
85 if (false !== strpos($post->post_content, '[marqueex_')) {
86 return true;
87 }
88
89 return $this->elementor_data_uses_marqueex($post);
90 }
91
92 /**
93 * @param \WP_Post $post
94 * @return bool
95 */
96 private function elementor_data_uses_marqueex($post) {
97 if (!is_object($post) || empty($post->ID)) {
98 return false;
99 }
100
101 $data = get_post_meta($post->ID, '_elementor_data', true);
102 if (!is_string($data) || '' === $data) {
103 return false;
104 }
105
106 return false !== strpos($data, 'marqueex-');
107 }
108
109 /**
110 * Handle the notice's three buttons.
111 */
112 public function handle_action() {
113 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
114 if (!isset($_GET['marqueex_review'])) {
115 return;
116 }
117
118 if (!current_user_can('manage_options')) {
119 return;
120 }
121
122 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
123 $action = sanitize_key(wp_unslash($_GET['marqueex_review']));
124
125 check_admin_referer('marqueex_review_' . $action);
126
127 $state = $this->get_state();
128
129 if ('later' === $action) {
130 $state['snooze_until'] = time() + (self::SNOOZE_DAYS * DAY_IN_SECONDS);
131 } else {
132 // "done" and "dismiss" both end it for good.
133 $state['dismissed'] = true;
134 }
135
136 $this->save_state($state);
137
138 wp_safe_redirect(remove_query_arg(['marqueex_review', '_wpnonce']));
139 exit;
140 }
141
142 /**
143 * Render the notice when it has been earned.
144 */
145 public function maybe_render_notice() {
146 if (!current_user_can('manage_options')) {
147 return;
148 }
149
150 if (!$this->should_ask()) {
151 return;
152 }
153
154 $screen = get_current_screen();
155 if (!$screen) {
156 return;
157 }
158
159 // MarqueeX's own screens and the plugins list only.
160 $is_own_screen = false !== strpos($screen->id, 'marqueex');
161 if (!$is_own_screen && 'plugins' !== $screen->id) {
162 return;
163 }
164 ?>
165 <?php // Not `is-dismissible`: core's X only hides the notice for the current
166 // page load, which would look like a choice that did not stick. The three
167 // links below are the real, persisted choices. ?>
168 <div class="notice notice-info marqueex-review-notice">
169 <p>
170 <strong><?php esc_html_e('Enjoying MarqueeX?', 'marqueex'); ?></strong>
171 <?php esc_html_e('You have had a marquee running for a week now. A short review on WordPress.org helps other people find the plugin — and tells us what to build next.', 'marqueex'); ?>
172 </p>
173 <p>
174 <a href="<?php echo esc_url(self::REVIEW_URL); ?>" class="button button-primary" target="_blank" rel="noopener noreferrer">
175 <?php esc_html_e('Leave a review', 'marqueex'); ?>
176 </a>
177 <a href="<?php echo esc_url($this->action_url('later')); ?>" class="button">
178 <?php esc_html_e('Maybe later', 'marqueex'); ?>
179 </a>
180 <a href="<?php echo esc_url($this->action_url('done')); ?>" class="button-link" style="margin-left:8px;">
181 <?php esc_html_e('Don\'t ask again', 'marqueex'); ?>
182 </a>
183 </p>
184 </div>
185 <?php
186 }
187
188 /**
189 * @return bool
190 */
191 private function should_ask() {
192 $state = $this->get_state();
193
194 if (!empty($state['dismissed'])) {
195 return false;
196 }
197
198 if (empty($state['first_use'])) {
199 return false;
200 }
201
202 if (!empty($state['snooze_until']) && time() < $state['snooze_until']) {
203 return false;
204 }
205
206 return (time() - (int) $state['first_use']) >= (self::DELAY_DAYS * DAY_IN_SECONDS);
207 }
208
209 /**
210 * @param string $action
211 * @return string
212 */
213 private function action_url($action) {
214 return wp_nonce_url(
215 add_query_arg('marqueex_review', $action),
216 'marqueex_review_' . $action
217 );
218 }
219
220 /**
221 * @return array
222 */
223 private function get_state() {
224 $state = get_option(self::OPTION, []);
225 return is_array($state) ? $state : [];
226 }
227
228 /**
229 * @param array $state
230 */
231 private function save_state(array $state) {
232 update_option(self::OPTION, $state, false);
233 }
234 }
235