PluginProbe
Filter Everything — WordPress & WooCommerce Filters / 1.9.5
Filter Everything — WordPress & WooCommerce Filters v1.9.5
1.9.6 1.9.5 1.9.4 1.9.3 1.9.2.2 1.9.2.1 trunk 1.2.1 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.1 1.4.4 1.4.5 1.4.8 1.4.9 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 All 51 releases
filter-everything / src / Admin / ReviewRequest.php

ReviewRequest.php in Filter Everything — WordPress & WooCommerce Filters 1.9.5, at src/Admin/ReviewRequest.php

213 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FilterEverything\Filter;
4
5 if ( ! defined('ABSPATH') ) {
6 exit;
7 }
8
9 /**
10 * Review request popup (free build only).
11 *
12 * Asks a long-term user to rate the plugin on wordpress.org. Shown on the
13 * plugin's own admin pages to users with manage_options, at most once per page
14 * load, when ALL of the following hold:
15 * - at least self::FIRST_DELAY has passed since the install timestamp
16 * (stamped on the first admin_init after install or after updating to the
17 * version that introduced it);
18 * - the site has at least one published Filter Set (the user actually uses
19 * the plugin — we only ask people the plugin has worked for);
20 * - the current admin has not dismissed it, has seen it fewer than
21 * self::MAX_SHOWS times, and is not within a snooze window.
22 *
23 * Every render auto-snoozes the popup for self::SNOOZE, so ignoring it equals
24 * "Maybe later". "Don't show this again" and clicking through to the review
25 * dismiss it permanently. State is kept per admin user (the ask is personal),
26 * the install timestamp per site.
27 */
28 class ReviewRequest
29 {
30 const INSTALL_OPTION = 'flrt_free_installed_at';
31 const USER_META = 'flrt_review_request';
32 const NONCE_ACTION = 'flrt_review_popup';
33 const INSTALLS_TRANSIENT = 'flrt_free_active_installs';
34
35 const REVIEW_URL = 'https://wordpress.org/support/plugin/filter-everything/reviews/#new-post';
36 const SUPPORT_URL = 'https://wordpress.org/support/plugin/filter-everything/';
37
38 const FIRST_DELAY = 14 * DAY_IN_SECONDS;
39 const SNOOZE = 21 * DAY_IN_SECONDS;
40 const MAX_SHOWS = 3;
41
42 // Used when the wordpress.org API is unreachable; matches the listing at
43 // the time this feature shipped.
44 const INSTALLS_FALLBACK = 50000;
45
46 public function __construct()
47 {
48 // The review ask targets the wordpress.org listing — free build only.
49 if ( defined('FLRT_FILTERS_PRO') ) {
50 return;
51 }
52
53 if ( ! is_admin() ) {
54 return;
55 }
56
57 add_action( 'admin_init', [ $this, 'stampInstallTime' ] );
58 add_action( 'admin_footer', [ $this, 'maybeRender' ] );
59 add_action( 'wp_ajax_flrt_review_popup', [ $this, 'ajaxDismiss' ] );
60 }
61
62 /**
63 * Existing installs never recorded an install time — the first admin_init
64 * after the update becomes their reference point, so everybody waits the
65 * same FIRST_DELAY from a known moment.
66 */
67 public function stampInstallTime()
68 {
69 if ( ! get_option( self::INSTALL_OPTION ) ) {
70 add_option( self::INSTALL_OPTION, time() );
71 }
72 }
73
74 public function maybeRender()
75 {
76 if ( ! $this->shouldShow() ) {
77 return;
78 }
79
80 // Count the show and auto-snooze right away: a user who ignores the
81 // popup (navigates on without clicking anything) is treated exactly
82 // like "Maybe later", and reloading pages cannot burn through the
83 // MAX_SHOWS budget within one session.
84 $state = $this->getState();
85 $state['shows'] = (int) $state['shows'] + 1;
86 $state['snoozed_until'] = time() + self::SNOOZE;
87 $this->saveState( $state );
88
89 flrt_include_admin_view( 'review-popup', [
90 'installs_label' => $this->getActiveInstallsLabel(),
91 'review_nonce' => wp_create_nonce( self::NONCE_ACTION ),
92 ] );
93 }
94
95 public function ajaxDismiss()
96 {
97 check_ajax_referer( self::NONCE_ACTION, 'nonce' );
98
99 if ( ! current_user_can( flrt_plugin_user_caps() ) ) {
100 wp_send_json_error();
101 }
102
103 $mode = isset( $_POST['mode'] ) ? sanitize_key( $_POST['mode'] ) : '';
104 $state = $this->getState();
105
106 if ( $mode === 'later' ) {
107 $state['snoozed_until'] = time() + self::SNOOZE;
108 } elseif ( $mode === 'never' || $mode === 'rated' ) {
109 $state['dismissed'] = true;
110 } else {
111 wp_send_json_error();
112 }
113
114 $this->saveState( $state );
115 wp_send_json_success();
116 }
117
118 private function shouldShow()
119 {
120 if ( ! current_user_can( flrt_plugin_user_caps() ) ) {
121 return false;
122 }
123
124 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
125
126 // The plugin's own admin pages only — same gate as the PRO modal.
127 if ( ! $screen || $screen->post_type !== FLRT_FILTERS_SET_POST_TYPE ) {
128 return false;
129 }
130
131 $installed_at = (int) get_option( self::INSTALL_OPTION );
132
133 if ( ! $installed_at || ( time() - $installed_at ) < self::FIRST_DELAY ) {
134 return false;
135 }
136
137 $state = $this->getState();
138
139 if ( $state['dismissed']
140 || (int) $state['shows'] >= self::MAX_SHOWS
141 || (int) $state['snoozed_until'] > time()
142 ) {
143 return false;
144 }
145
146 // Only ask users the plugin has actually worked for: at least one
147 // published Filter Set.
148 $sets = get_posts( [
149 'post_type' => FLRT_FILTERS_SET_POST_TYPE,
150 'post_status' => 'publish',
151 'numberposts' => 1,
152 'fields' => 'ids',
153 ] );
154
155 return ! empty( $sets );
156 }
157
158 private function getState()
159 {
160 $state = get_user_meta( get_current_user_id(), self::USER_META, true );
161
162 if ( ! is_array( $state ) ) {
163 $state = [];
164 }
165
166 return array_merge(
167 [ 'shows' => 0, 'snoozed_until' => 0, 'dismissed' => false ],
168 $state
169 );
170 }
171
172 private function saveState( array $state )
173 {
174 update_user_meta( get_current_user_id(), self::USER_META, $state );
175 }
176
177 /**
178 * "50,000+" — the real active-installs count from the wordpress.org
179 * listing, refreshed weekly. On API failure the last known/fallback count
180 * is cached for a day so the admin never waits on a dead request twice.
181 *
182 * @return string
183 */
184 private function getActiveInstallsLabel()
185 {
186 $installs = get_transient( self::INSTALLS_TRANSIENT );
187
188 if ( false === $installs ) {
189 $installs = 0;
190 $response = wp_remote_get(
191 'https://api.wordpress.org/plugins/info/1.0/filter-everything.json?fields=active_installs',
192 [ 'timeout' => 5 ]
193 );
194
195 if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
196 $data = json_decode( wp_remote_retrieve_body( $response ), true );
197 $installs = isset( $data['active_installs'] ) ? (int) $data['active_installs'] : 0;
198 }
199
200 if ( $installs > 0 ) {
201 set_transient( self::INSTALLS_TRANSIENT, $installs, WEEK_IN_SECONDS );
202 } else {
203 $installs = self::INSTALLS_FALLBACK;
204 set_transient( self::INSTALLS_TRANSIENT, $installs, DAY_IN_SECONDS );
205 }
206 }
207
208 return number_format_i18n( (int) $installs ) . '+';
209 }
210 }
211
212 new ReviewRequest();
213