PluginProbe
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More / trunk
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More vtrunk
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 47 releases
content-control / classes / Controllers / Admin / Reviews.php

Reviews.php in Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More trunk, at classes/Controllers/Admin/Reviews.php

589 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Review Controller Class.
4 *
5 * @package ContentControl
6 */
7
8 namespace ContentControl\Controllers\Admin;
9
10 use ContentControl\Base\Controller;
11
12 use function ContentControl\plugin;
13
14 defined( 'ABSPATH' ) || exit;
15
16 /**
17 * Class ContentControl\Admin\Reviews
18 *
19 * This class adds a review request system for your plugin or theme to the WP dashboard.
20 *
21 * @since 1.1.0
22 */
23 class Reviews extends Controller {
24
25 /**
26 * Enable debug mode.
27 *
28 * @var boolean
29 */
30 private $debug = false;
31
32 /**
33 * Debug trigger.
34 *
35 * @var array{group:string,code:string}
36 */
37 private $debug_trigger = [
38 'group' => 'time_installed',
39 'code' => 'one_week',
40 ];
41
42 /**
43 * Tracking API Endpoint.
44 *
45 * @var string
46 */
47 public $api_url;
48
49 /**
50 * Initialize review requests.
51 */
52 public function init() {
53 add_action( 'init', [ $this, 'hooks' ] );
54 add_action( 'wp_ajax_content_control_review_action', [ $this, 'ajax_handler' ] );
55 }
56
57 /**
58 * Hook into relevant WP actions.
59 *
60 * @return void
61 */
62 public function hooks() {
63 if ( is_admin() && current_user_can( 'manage_options' ) ) {
64 $this->installed_on();
65 add_action( 'admin_notices', [ $this, 'admin_notices' ] );
66 add_action( 'network_admin_notices', [ $this, 'admin_notices' ] );
67 add_action( 'user_admin_notices', [ $this, 'admin_notices' ] );
68 }
69 }
70
71 /**
72 * Get the install date for comparisons. Sets the date to now if none is found.
73 *
74 * @return false|string
75 */
76 public function installed_on() {
77 $installed_on = \get_option( 'content_control_installed_on', false );
78
79 if ( ! $installed_on ) {
80 $installed_on = current_time( 'mysql' );
81 \update_option( 'content_control_installed_on', $installed_on );
82 }
83
84 return $installed_on;
85 }
86
87 /**
88 * AJAX Handler
89 *
90 * @return void
91 */
92 public function ajax_handler() {
93 if ( ! isset( $_REQUEST['nonce'] ) || ! is_string( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['nonce'] ) ), 'content_control_review_action' ) ) {
94 wp_send_json_error();
95 }
96
97 $args = [
98 'group' => isset( $_REQUEST['group'] ) && is_string( $_REQUEST['group'] ) ? sanitize_key( wp_unslash( $_REQUEST['group'] ) ) : $this->get_trigger_group(),
99 'code' => isset( $_REQUEST['code'] ) && is_string( $_REQUEST['code'] ) ? sanitize_key( wp_unslash( $_REQUEST['code'] ) ) : $this->get_trigger_code(),
100 'pri' => isset( $_REQUEST['pri'] ) && is_numeric( $_REQUEST['pri'] ) ? absint( $_REQUEST['pri'] ) : $this->get_current_trigger( 'pri' ),
101 'reason' => isset( $_REQUEST['reason'] ) && is_string( $_REQUEST['reason'] ) ? sanitize_key( wp_unslash( $_REQUEST['reason'] ) ) : 'maybe_later',
102 ];
103
104 try {
105 $user_id = get_current_user_id();
106
107 $dismissed_triggers = $this->dismissed_triggers();
108 $dismissed_triggers[ $args['group'] ] = $args['pri'];
109 update_user_meta( $user_id, 'content_control_reviews_dismissed_triggers', $dismissed_triggers );
110 update_user_meta( $user_id, 'content_control_reviews_last_dismissed', current_time( 'mysql' ) );
111
112 switch ( $args['reason'] ) {
113 case 'maybe_later':
114 update_user_meta( $user_id, 'content_control_reviews_last_dismissed', current_time( 'mysql' ) );
115 break;
116 case 'am_now':
117 case 'already_did':
118 $this->already_did( true );
119 break;
120 default:
121 // Do nothing if the reason value does not match one of ours.
122 }
123
124 wp_send_json_success();
125 } catch ( \Exception $e ) {
126 wp_send_json_error( $e );
127 }
128 }
129
130 /**
131 * Get the current trigger group and code.
132 *
133 * @return int|string
134 */
135 public function get_trigger_group() {
136 static $selected;
137
138 if ( $this->debug ) {
139 return ! empty( $this->debug_trigger['group'] ) ? $this->debug_trigger['group'] : 'time_installed';
140 }
141
142 if ( ! isset( $selected ) ) {
143 $dismissed_triggers = $this->dismissed_triggers();
144
145 $triggers = $this->triggers();
146
147 foreach ( $triggers as $g => $group ) {
148 foreach ( $group['triggers'] as $t => $trigger ) {
149 if ( ! in_array( false, $trigger['conditions'], true ) && ( empty( $dismissed_triggers[ $g ] ) || $dismissed_triggers[ $g ] < $trigger['pri'] ) ) {
150 $selected = $g;
151 break;
152 }
153 }
154
155 if ( isset( $selected ) ) {
156 break;
157 }
158 }
159 }
160
161 return $selected;
162 }
163
164 /**
165 * Get the current trigger group and code.
166 *
167 * @return int|string
168 */
169 public function get_trigger_code() {
170 static $selected;
171
172 if ( $this->debug ) {
173 return ! empty( $this->debug_trigger['code'] ) ? $this->debug_trigger['code'] : 'one_week';
174 }
175
176 if ( ! isset( $selected ) ) {
177 $dismissed_triggers = $this->dismissed_triggers();
178
179 foreach ( $this->triggers() as $g => $group ) {
180 foreach ( $group['triggers'] as $t => $trigger ) {
181 if ( ! in_array( false, $trigger['conditions'], true ) && ( empty( $dismissed_triggers[ $g ] ) || $dismissed_triggers[ $g ] < $trigger['pri'] ) ) {
182 $selected = $t;
183 break;
184 }
185 }
186
187 if ( isset( $selected ) ) {
188 break;
189 }
190 }
191 }
192
193 return $selected;
194 }
195
196 /**
197 * Get the current trigger.
198 *
199 * @param string $key Optional. Key to return from the trigger array.
200 *
201 * @return bool|mixed
202 */
203 public function get_current_trigger( $key = null ) {
204 $group = $this->get_trigger_group();
205 $code = $this->get_trigger_code();
206
207 if ( ! $group || ! $code ) {
208 return false;
209 }
210
211 $trigger = $this->triggers( $group, $code );
212
213 if ( empty( $key ) ) {
214 return $trigger;
215 } else {
216 return isset( $trigger[ $key ] ) ? $trigger[ $key ] : false;
217 }
218 }
219
220 /**
221 * Returns an array of dismissed trigger groups.
222 *
223 * Array contains the group key and highest priority trigger that has been shown previously for each group.
224 *
225 * $return = array(
226 * 'group1' => 20
227 * );
228 *
229 * @return array|mixed
230 */
231 public function dismissed_triggers() {
232 $user_id = get_current_user_id();
233
234 $dismissed_triggers = get_user_meta( $user_id, 'content_control_reviews_dismissed_triggers', true );
235
236 if ( ! $dismissed_triggers ) {
237 $dismissed_triggers = [];
238 }
239
240 return $dismissed_triggers;
241 }
242
243 /**
244 * Returns true if the user has opted to never see this again. Or sets the option.
245 *
246 * @param bool $set If set this will mark the user as having opted to never see this again.
247 *
248 * @return bool
249 */
250 public function already_did( $set = false ) {
251 $user_id = get_current_user_id();
252
253 if ( $set ) {
254 update_user_meta( $user_id, '_content_control_reviews_already_did', true );
255
256 return true;
257 }
258
259 return (bool) get_user_meta( $user_id, '_content_control_reviews_already_did', true );
260 }
261
262 /**
263 * Gets a list of triggers.
264 *
265 * @param string $group Trigger group.
266 * @param string $code Trigger code.
267 *
268 * @return bool|mixed
269 */
270 public function triggers( $group = null, $code = null ) {
271 static $triggers;
272
273 if ( ! isset( $triggers ) ) {
274 $link = $this->get_review_link();
275
276 // Translators: 1. emoji, 2. html tag, 3. html tag, 4. the number of days, 5. html tag, 6. html tag.
277 $time_message = __( 'Hi there! %1$s You\'ve been using the %2$sContent Control%3$s plugin on your site for %4$s now - We hope it\'s been helpful. If you\'re enjoying the plugin, would you mind rating it %5$s5-stars%6$s to help spread the word?', 'content-control' );
278 $triggers = [
279 'time_installed' => [
280 'triggers' => [
281 'one_week' => [
282 'message' => sprintf(
283 $time_message,
284 '👋',
285 '<strong>',
286 '</strong>',
287 __( '1 week', 'content-control' ),
288 '<span class="five-stars" title="',
289 '"></span>'
290 ),
291 'conditions' => [
292 strtotime( $this->installed_on() . ' +1 week' ) < time(),
293 ],
294 'link' => $link,
295 'pri' => 10,
296 ],
297 'one_month' => [
298 'message' => sprintf(
299 $time_message,
300 '👋',
301 '<strong>',
302 '</strong>',
303 __( '1 month', 'content-control' ),
304 '<span class="five-stars" title="',
305 '"></span>'
306 ),
307 'conditions' => [
308 strtotime( $this->installed_on() . ' +1 month' ) < time(),
309 ],
310 'link' => $link,
311 'pri' => 20,
312 ],
313 'three_months' => [
314 'message' => sprintf(
315 $time_message,
316 '👋',
317 '<strong>',
318 '</strong>',
319 __( '3 months', 'content-control' ),
320 '<span class="five-stars" title="',
321 '"></span>'
322 ),
323 'conditions' => [
324 strtotime( $this->installed_on() . ' +3 months' ) < time(),
325 ],
326 'link' => $link,
327 'pri' => 30,
328 ],
329
330 ],
331 'pri' => 10,
332 ],
333 ];
334
335 $triggers = apply_filters( 'content_control_reviews_triggers', $triggers );
336
337 // Sort Groups.
338 uasort( $triggers, [ $this, 'rsort_by_priority' ] );
339
340 // Sort each groups triggers.
341 foreach ( $triggers as $k => $v ) {
342 uasort( $triggers[ $k ]['triggers'], [ $this, 'rsort_by_priority' ] );
343 }
344 }
345
346 if ( isset( $group ) ) {
347 if ( ! isset( $triggers[ $group ] ) ) {
348 return false;
349 }
350
351 if ( ! isset( $code ) ) {
352 return $triggers[ $group ];
353 } else {
354 return isset( $triggers[ $group ]['triggers'][ $code ] ) ? $triggers[ $group ]['triggers'][ $code ] : false;
355 }
356 }
357
358 return $triggers;
359 }
360
361 /**
362 * Get the review destination for the installed product.
363 *
364 * Free users go directly to WordPress.org's unselected review form. Sites
365 * with Pro installed use the first-party landing page, which routes them to
366 * the verified-purchaser review form for the commercial product.
367 *
368 * @since 2.7.3
369 *
370 * @return string
371 */
372 public function get_review_link() {
373 return $this->container->is_pro_installed()
374 ? 'https://contentcontrolplugin.com/leave-a-review/?product=pro'
375 : 'https://wordpress.org/support/plugin/content-control/reviews/#rate-response';
376 }
377
378 /**
379 * Render admin notices if available.
380 *
381 * @return void
382 */
383 public function admin_notices() {
384 if ( $this->hide_notices() ) {
385 return;
386 }
387
388 $group = $this->get_trigger_group();
389 $code = $this->get_trigger_code();
390 $pri = $this->get_current_trigger( 'pri' );
391 $trigger = $this->get_current_trigger();
392
393 // Used to anonymously distinguish unique site+user combinations in terms of effectiveness of each trigger.
394 $uuid = wp_hash( home_url() . '-' . get_current_user_id() );
395
396 ?>
397
398 <script type="text/javascript">
399 (function ($) {
400 var trigger = {
401 group: '<?php echo esc_js( $group ); ?>',
402 code: '<?php echo esc_js( $code ); ?>',
403 pri: '<?php echo esc_js( $pri ); ?>'
404 };
405
406 function dismiss(reason) {
407 $.ajax({
408 method: "POST",
409 dataType: "json",
410 url: ajaxurl,
411 data: {
412 action: 'content_control_review_action',
413 nonce: '<?php echo esc_js( wp_create_nonce( 'content_control_review_action' ) ); ?>',
414 group: trigger.group,
415 code: trigger.code,
416 pri: trigger.pri,
417 reason: reason
418 }
419 });
420
421 <?php if ( ! empty( $this->api_url ) ) : ?>
422 $.ajax({
423 method: "POST",
424 dataType: "json",
425 url: '<?php echo esc_js( $this->api_url ); ?>',
426 data: {
427 trigger_group: trigger.group,
428 trigger_code: trigger.code,
429 reason: reason,
430 uuid: '<?php echo esc_js( $uuid ); ?>'
431 }
432 });
433 <?php endif; ?>
434 }
435
436 $(document)
437 .on('click', '.content-control-notice .content-control-dismiss', function (event) {
438 var $this = $(this),
439 reason = $this.data('reason'),
440 notice = $this.parents('.content-control-notice');
441
442 notice.fadeTo(100, 0, function () {
443 notice.slideUp(100, function () {
444 notice.remove();
445 });
446 });
447
448 dismiss(reason);
449 })
450 .ready(function () {
451 setTimeout(function () {
452 $('.content-control-notice button.notice-dismiss').click(function (event) {
453 dismiss('maybe_later');
454 });
455 }, 1000);
456 });
457 }(jQuery));
458 </script>
459
460 <style>
461 .content-control-notice {
462 display: flex;
463 align-items: center;
464 gap: 16px;
465 padding: 8px;
466 margin-top: 16px;
467 margin-bottom: 16px;
468 }
469
470 .content-control-notice .notice-logo {
471 flex: 0 0 110px;
472 max-width: 110px;
473 }
474
475 .content-control-notice .notice-content {
476 flex-grow: 1;
477 }
478
479 .content-control-notice p {
480 margin-bottom: 0;
481 max-width: 800px;
482 }
483
484 .content-control-notice .review-actions {
485 margin-top: 10px;
486 margin-bottom: 0;
487 padding-left: 0;
488 list-style: none;
489
490 display: flex;
491 gap: 16px;
492 align-items: center;
493 }
494 .content-control-notice .five-stars::before {
495 content: "�
496
497
498
499
500 ";
501 color: #f0ad4e;
502 }
503 </style>
504
505 <div class="notice notice-success is-dismissible content-control-notice">
506
507 <div class="notice-logo">
508 <img class="logo" width="110" src="<?php echo esc_url( plugin()->get_url( 'assets/images/illustration-check.svg' ) ); ?>" />
509 </div>
510
511 <div class="notice-content">
512 <p>
513 <?php
514 // Review messages support post-safe inline markup such as emphasis and the star-rating span.
515 echo wp_kses_post( $trigger['message'] );
516 ?>
517 ~ <a target="_blank" href="https://twitter.com/danieliser" title="Follow Daniel on Twitter">@danieliser</a>
518 </p>
519 <ul class="review-actions">
520 <li>😁
521 <a class="content-control-dismiss" target="_blank" href="<?php echo esc_url( $trigger['link'] ); ?>" data-reason="am_now">
522 <strong><?php esc_html_e( 'Ok, you deserve it', 'content-control' ); ?></strong>
523 </a>
524 </li>
525 <li>
526 🤔
527 <a href="#" class="content-control-dismiss" data-reason="maybe_later">
528 <?php esc_html_e( 'Maybe later, okay?', 'content-control' ); ?>
529 </a>
530 </li>
531 <li>🙌
532 <a href="#" class="content-control-dismiss" data-reason="already_did">
533 <?php esc_html_e( 'I already did', 'content-control' ); ?>
534 </a>
535 </li>
536 </ul>
537
538 </div>
539
540 </div>
541
542 <?php
543 }
544
545 /**
546 * Checks if notices should be shown.
547 *
548 * @return bool
549 */
550 public function hide_notices() {
551 if ( $this->debug ) {
552 return false;
553 }
554
555 $code = $this->get_trigger_code();
556
557 return $this->already_did()
558 || ( $this->last_dismissed() && strtotime( $this->last_dismissed() . ' +2 weeks' ) > time() )
559 || empty( $code );
560 }
561
562 /**
563 * Gets the last dismissed date.
564 *
565 * @return false|string
566 */
567 public function last_dismissed() {
568 $user_id = get_current_user_id();
569
570 return get_user_meta( $user_id, 'content_control_reviews_last_dismissed', true );
571 }
572
573 /**
574 * Sort array in reverse by priority value
575 *
576 * @param array{pri:int|null} $a First array to compare.
577 * @param array{pri:int|null} $b Second array to compare.
578 *
579 * @return int
580 */
581 public function rsort_by_priority( $a, $b ) {
582 if ( ! isset( $a['pri'] ) || ! isset( $b['pri'] ) || $a['pri'] === $b['pri'] ) {
583 return 0;
584 }
585
586 return ( $a['pri'] < $b['pri'] ) ? 1 : - 1;
587 }
588 }
589