PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 1.7.0.9 All 211 releases
charitable / includes / admin / notifications / class-charitable-eventdriven.php

class-charitable-eventdriven.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at includes/admin/notifications/class-charitable-eventdriven.php

614 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Events for notification class.
4 *
5 * @package Charitable/Classes/Charitable_Admin_Form
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.5.0
10 * @version 1.5.0
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Class EventDriven.
20 *
21 * @since 1.8.3
22 */
23 class Charitable_EventDriven {
24
25 /**
26 * The single instance of this class.
27 *
28 * @var Charitable_Notifications|null
29 */
30 private static $instance = null;
31
32 /**
33 * Charitable version when the Event Driven feature has been introduced.
34 *
35 * @since 1.8.3
36 *
37 * @var string
38 */
39 const FEATURE_INTRODUCED = '1.8.3';
40
41 /**
42 * Expected date format for notifications.
43 *
44 * @since 1.8.3
45 *
46 * @var string
47 */
48 const DATE_FORMAT = 'Y-m-d H:i:s';
49
50 /**
51 * Common UTM parameters.
52 *
53 * @since 1.8.3
54 *
55 * @var array
56 */
57 const UTM_PARAMS = [
58 'utm_source' => 'WordPress',
59 'utm_medium' => 'Event Notification',
60 ];
61
62 /**
63 * Common targets for date logic.
64 *
65 * Available items:
66 * - upgraded (upgraded to a latest version)
67 * - activated
68 * - campaigns_first_created
69 * - X.X.X.X (upgraded to a specific version)
70 * - pro (activated/installed)
71 * - lite (activated/installed)
72 *
73 * @since 1.8.3
74 *
75 * @var array
76 */
77 const DATE_LOGIC = [ 'upgraded', 'activated', 'campaigns_first_created' ];
78
79 /**
80 * Timestamps.
81 *
82 * @since 1.8.3
83 *
84 * @var array
85 */
86 private $timestamps = [];
87
88 /**
89 * Create object instance.
90 *
91 * @since 1.8.3
92 */
93 public function __construct() {
94 }
95
96 /**
97 * Initialize class.
98 *
99 * @since 1.8.3
100 */
101 public function init() {
102
103 if ( ! $this->allow_load() ) {
104 return;
105 }
106 }
107
108 /**
109 * Indicate if this is allowed to load.
110 *
111 * @since 1.8.3
112 *
113 * @return bool
114 */
115 private function allow_load() {
116
117 return charitable()->registry()->get( 'notifications' )->has_access() || wp_doing_cron();
118 }
119
120 /**
121 * Hooks.
122 *
123 * @since 1.8.3
124 */
125 private function hooks() {
126 }
127
128 /**
129 * Add Event Driven notifications before saving them in database.
130 *
131 * @since 1.8.3
132 *
133 * @param array $data Notification data.
134 *
135 * @return array
136 */
137 public function update_events( $data ) {
138
139 $updated = [];
140
141 /**
142 * Allow developers to turn on debug mode: store all notifications and then show all of them.
143 *
144 * @since 1.8.3
145 *
146 * @param bool $is_debug True if it's a debug mode. Default: false.
147 */
148 $is_debug = (bool) apply_filters( 'charitable_admin_notifications_event_driven_update_events_debug', false );
149
150 $charitable_notifications = charitable()->registry()->get( 'notifications' );
151
152 foreach ( $this->get_notifications() as $slug => $notification ) {
153
154 $is_processed = ! empty( $data['events'][ $slug ]['start'] );
155 $is_conditional_ok = ! ( isset( $notification['condition'] ) && $notification['condition'] === false );
156
157 // If it's a debug mode OR valid notification has been already processed - skip running logic checks and save it.
158 if (
159 $is_debug ||
160 ( $is_processed && $is_conditional_ok && $charitable_notifications->is_valid( $data['events'][ $slug ] ) )
161 ) {
162 unset( $notification['date_logic'], $notification['offset'], $notification['condition'] );
163
164 // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
165 $notification['start'] = $is_debug ? date( self::DATE_FORMAT ) : $data['events'][ $slug ]['start'];
166 $updated[ $slug ] = $notification;
167
168 continue;
169 }
170
171 // Ignore if a condition is not passed conditional checks.
172 if ( ! $is_conditional_ok ) {
173 continue;
174 }
175
176 $timestamp = $this->get_timestamp_by_date_logic(
177 $this->prepare_date_logic( $notification )
178 );
179
180 if ( empty( $timestamp ) ) {
181 continue;
182 }
183
184 // Probably, notification should be visible after some time.
185 $offset = empty( $notification['offset'] ) ? 0 : absint( $notification['offset'] );
186
187 // Set a start date when notification will be shown.
188 // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
189 $notification['start'] = date( self::DATE_FORMAT, $timestamp + $offset );
190
191 // Ignore if notification data is not valid.
192 if ( ! $charitable_notifications->is_valid( $notification ) ) {
193 continue;
194 }
195
196 // Remove unnecessary values, mark notification as active, and save it.
197 unset( $notification['date_logic'], $notification['offset'], $notification['condition'] );
198 $updated[ $slug ] = $notification;
199 }
200
201 $data['events'] = $updated;
202
203 return $data;
204 }
205
206 /**
207 * Prepare and retrieve date logic.
208 *
209 * @since 1.8.3
210 *
211 * @param array $notification Notification data.
212 *
213 * @return array
214 */
215 private function prepare_date_logic( $notification ) {
216
217 $date_logic = empty( $notification['date_logic'] ) || ! is_array( $notification['date_logic'] ) ? self::DATE_LOGIC : $notification['date_logic'];
218
219 return array_filter( array_filter( $date_logic, 'is_string' ) );
220 }
221
222 /**
223 * Retrieve a notification timestamp based on date logic.
224 *
225 * @since 1.8.3
226 *
227 * @param array $args Date logic.
228 *
229 * @return int
230 */
231 private function get_timestamp_by_date_logic( $args ) {
232
233 foreach ( $args as $target ) {
234
235 if ( ! empty( $this->timestamps[ $target ] ) ) {
236 return $this->timestamps[ $target ];
237 }
238
239 $timestamp = call_user_func(
240 $this->get_timestamp_callback( $target ),
241 $target
242 );
243
244 if ( ! empty( $timestamp ) ) {
245 $this->timestamps[ $target ] = $timestamp;
246
247 return $timestamp;
248 }
249 }
250
251 return 0;
252 }
253
254 /**
255 * Retrieve a callback that determines needed timestamp.
256 *
257 * @since 1.8.3
258 *
259 * @param string $target Date logic target.
260 *
261 * @return callable
262 */
263 private function get_timestamp_callback( $target ) {
264
265 $raw_target = $target;
266
267 // As $target should be a part of name for callback method,
268 // this regular expression allow lowercase characters, numbers, and underscore.
269 $target = strtolower( preg_replace( '/[^a-z0-9_]/', '', $target ) );
270
271 // Basic callback.
272 $callback = [ $this, 'get_timestamp_' . $target ];
273
274 // Determine if a special version number is passed.
275 // Uses the regular expression to check a SemVer string.
276 // @link https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string.
277 if ( preg_match( '/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.([1-9\d*]))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/', $raw_target ) ) {
278 $callback = [ $this, 'get_timestamp_upgraded' ];
279 }
280
281 // If callback is callable, return it. Otherwise, return fallback.
282 return is_callable( $callback ) ? $callback : '__return_zero';
283 }
284
285 /**
286 * Retrieve a timestamp when Charitable was upgraded.
287 *
288 * @since 1.8.3
289 *
290 * @param string $version Charitable version.
291 *
292 * @return int|false Unix timestamp. False on failure.
293 */
294 private function get_timestamp_upgraded( $version = false ) {
295
296 if ( false === $version || $version === 'upgraded' ) {
297 $version = charitable()->get_version();
298 }
299
300 $timestamp = charitable_get_updated_timestamp( $version );
301
302 if ( $timestamp === false ) {
303 return false;
304 }
305
306 // Return a current timestamp if no luck to return a migration's timestamp.
307 return $timestamp <= 0 ? time() : $timestamp;
308 }
309
310 /**
311 * Retrieve a timestamp when Charitable was first installed/activated.
312 *
313 * @since 1.8.3
314 *
315 * @return int|false Unix timestamp. False on failure.
316 */
317 private function get_timestamp_activated() {
318
319 return charitable_get_activated_timestamp();
320 }
321
322 /**
323 * Retrieve a timestamp when Lite was first installed.
324 *
325 * @since 1.8.3
326 *
327 * @return int|false Unix timestamp. False on failure.
328 */
329 private function get_timestamp_lite() {
330
331 $activated = (array) get_option( 'charitable_activated', [] );
332
333 return ! empty( $activated['lite'] ) ? absint( $activated['lite'] ) : false;
334 }
335
336 /**
337 * Retrieve a timestamp when Pro was first installed.
338 *
339 * @since 1.8.3
340 *
341 * @return int|false Unix timestamp. False on failure.
342 */
343 private function get_timestamp_pro() {
344
345 $activated = (array) get_option( 'charitable_activated', [] );
346
347 return ! empty( $activated['pro'] ) ? absint( $activated['pro'] ) : false;
348 }
349
350 /**
351 * Retrieve a timestamp when a first form was created.
352 *
353 * @since 1.8.3
354 *
355 * @return int|false Unix timestamp. False on failure.
356 */
357 private function get_timestamp_campaigns_first_created() {
358
359 $timestamp = get_option( 'charitable_campaigns_first_created' );
360
361 return ! empty( $timestamp ) ? absint( $timestamp ) : false;
362 }
363
364 /**
365 * Retrieve a number of entries.
366 *
367 * @since 1.8.3
368 *
369 * @return int
370 */
371 private function get_entry_count() {
372
373 static $count;
374
375 if ( is_int( $count ) ) {
376 return $count;
377 }
378
379 global $wpdb;
380
381 $count = 0;
382 $entry_handler = charitable()->get( 'entry' );
383 $entry_meta_handler = charitable()->get( 'entry_meta' );
384
385 if ( ! $entry_handler || ! $entry_meta_handler ) {
386 return $count;
387 }
388
389 $query = "SELECT COUNT({$entry_handler->primary_key})
390 FROM {$entry_handler->table_name}
391 WHERE {$entry_handler->primary_key}
392 NOT IN (
393 SELECT entry_id
394 FROM {$entry_meta_handler->table_name}
395 WHERE type = 'backup_id'
396 );";
397
398 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
399 $count = (int) $wpdb->get_var( $query ); // phpcs:ignore
400
401 return $count;
402 }
403
404 /**
405 * Retrieve campaigns.
406 *
407 * @since 1.8.3
408 *
409 * @param int $posts_per_page Number of form to return.
410 *
411 * @return array
412 */
413 private function get_campaigns( $posts_per_page ) { // phpcs:ignore
414
415 return ! empty( $campaigns ) ? (array) $campaigns : [];
416 }
417
418 /**
419 * Determine if the user has at least 1 form.
420 *
421 * @since 1.8.3
422 *
423 * @return bool
424 */
425 private function has_form() {
426
427 return ! empty( $this->get_campaigns( 1 ) );
428 }
429
430 /**
431 * Determine if it is a new user, usually by checking if the plugin has been updated or first installed.
432 *
433 * @since 1.8.3
434 *
435 * @return bool
436 */
437 private function is_new_user() {
438
439 $activated = (array) get_option( 'charitable_activated', array() );
440
441 if ( empty( $activated ) || ! is_array( $activated ) ) {
442 delete_transient( 'charitable_autoshow_plugin_notifications' );
443 return true;
444 }
445
446 // Find for the oldest unix timestamp in the values of $activated and assign it to a variable.
447 $activated_time = max( $activated );
448
449 // If the timestamp eariler than 30 days from current time, then the user is new.
450 $is_new_user = ( time() - $activated_time ) < 30 * DAY_IN_SECONDS;
451
452 if ( ! $is_new_user ) {
453 return false;
454 }
455
456 $versions_upgraded_from = get_option( 'charitable_version_upgraded_from', [] );
457
458 if ( empty( $versions_upgraded_from ) ) {
459 return true;
460 }
461
462 // Get the first value in the $versions_upgraded_from array.
463 $version = reset( $versions_upgraded_from );
464
465 // If the version equals the current version, then it is a new user.
466 $is_new_user = version_compare( $version, charitable()->get_version(), '=' );
467
468 if ( $is_new_user ) {
469 delete_transient( 'charitable_autoshow_plugin_notifications' );
470 return true;
471 }
472
473 return false;
474 }
475
476 /**
477 * Determine if it's an English site.
478 *
479 * @since 1.8.3
480 *
481 * @return bool
482 */
483 private function is_english_site() {
484
485 static $result;
486
487 if ( is_bool( $result ) ) {
488 return $result;
489 }
490
491 $locales = array_unique(
492 array_map(
493 [ $this, 'language_to_iso' ],
494 [ get_locale(), get_user_locale() ]
495 )
496 );
497 $result = count( $locales ) === 1 && $locales[0] === 'en';
498
499 return $result;
500 }
501
502 /**
503 * Convert language to ISO.
504 *
505 * @since 1.8.3
506 *
507 * @param string $lang Language value.
508 *
509 * @return string
510 */
511 private function language_to_iso( $lang ) {
512
513 return $lang === '' ? $lang : explode( '_', $lang )[0];
514 }
515
516 /**
517 * Retrieve a modified URL query string.
518 *
519 * @since 1.8.3
520 *
521 * @param array $args An associative array of query variables.
522 * @param string $url A URL to act upon.
523 *
524 * @return string
525 */
526 private function add_query_arg( $args, $url ) {
527
528 return add_query_arg(
529 array_merge( $this->get_utm_params(), array_map( 'rawurlencode', $args ) ),
530 $url
531 );
532 }
533
534 /**
535 * Retrieve UTM parameters for Event Driven notifications links.
536 *
537 * @since 1.8.3
538 *
539 * @return array
540 */
541 private function get_utm_params() {
542
543 static $utm_params;
544
545 if ( ! $utm_params ) {
546 $utm_params = [
547 'utm_source' => self::UTM_PARAMS['utm_source'],
548 'utm_medium' => rawurlencode( self::UTM_PARAMS['utm_medium'] ),
549 'utm_campaign' => charitable_is_pro() ? 'plugin' : 'liteplugin',
550 ];
551 }
552
553 return $utm_params;
554 }
555
556 /**
557 * Retrieve Event Driven notifications.
558 *
559 * @since 1.8.3
560 *
561 * @return array
562 */
563 private function get_notifications() {
564
565 return [
566 'welcome-message' => [
567 'id' => 'welcome-message',
568 'title' => esc_html__( 'Welcome to Charitable!', 'charitable' ),
569 'content' => esc_html__( 'We’re grateful that you chose Charitable! Now that you’ve installed the plugin, you’re less than 5 minutes away from publishing your first campaign. To make it easy, we’ve got a checklist to get you started!', 'charitable' ),
570 'btns' => [
571 'main' => [
572 'url' => admin_url( 'admin.php?page=charitable-setup-checklist' ),
573 'text' => esc_html__( 'Start Checklist', 'charitable' ),
574 ],
575 'alt' => [
576 'url' => $this->add_query_arg(
577 [ 'utm_content' => 'Welcome Read the Guide' ],
578 'https://wpcharitable.com/documentation/creating-your-first-campaign/'
579 ),
580 'text' => esc_html__( 'Read the Guide', 'charitable' ),
581 ],
582 ],
583 'type' => [
584 'lite',
585 'basic',
586 'plus',
587 'pro',
588 'agency',
589 'elite',
590 'ultimate',
591 'lifetime',
592 ],
593 // Immediately after activation (new users only, not upgrades).
594 'condition' => $this->is_new_user(),
595 ],
596 ];
597 }
598
599 /**
600 * Returns and/or create the single instance of this class.
601 *
602 * @since 1.8.3
603 *
604 * @return Charitable_EventDriven
605 */
606 public static function get_instance() {
607 if ( is_null( self::$instance ) ) {
608 self::$instance = new self();
609 }
610
611 return self::$instance;
612 }
613 }
614