PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.26.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.26.1
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / models / FrmSalesApi.php

FrmSalesApi.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.26.1, at classes/models/FrmSalesApi.php

543 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 /**
7 * @since 6.17
8 */
9 class FrmSalesApi extends FrmFormApi {
10
11 /**
12 * @var FrmSalesApi|null
13 */
14 private static $instance;
15
16 protected $cache_key;
17
18 /**
19 * All sales from API data.
20 *
21 * @var array|false
22 */
23 private static $sales = false;
24
25 /**
26 * Best sale from API data.
27 *
28 * @var array|false|null
29 */
30 private static $best_sale;
31
32 /**
33 * @since 6.25.1
34 *
35 * @var string|null
36 */
37 private static $cross_sell_text;
38
39 /**
40 * @since 6.25.1
41 *
42 * @var string|null
43 */
44 private static $cross_sell_link;
45
46 public function __construct() {
47 $this->set_cache_key();
48
49 if ( false === self::$sales ) {
50 $this->set_sales();
51 }
52 }
53
54 /**
55 * @since 6.17
56 *
57 * @return void
58 */
59 protected function set_cache_key() {
60 $this->cache_key = 'frm_sales_cache';
61 }
62
63 /**
64 * @since 6.17
65 *
66 * @return string
67 */
68 protected function api_url() {
69 return 'https://plapi.formidableforms.com/sales/';
70 }
71
72 /**
73 * @since 6.17
74 *
75 * @return void
76 */
77 private function set_sales() {
78 self::$sales = array();
79
80 $api = $this->get_api_info();
81
82 if ( empty( $api ) ) {
83 return;
84 }
85
86 foreach ( $api as $sale ) {
87 $this->add_sale( $sale );
88
89 if ( is_array( $sale ) && isset( $sale['cross_sell_text'] ) ) {
90 $this->set_cross_sell( $sale );
91 }
92 }
93 }
94
95 /**
96 * Check for a special array in the sales API array.
97 * Normally cross_sell_text and cross_sell_link are not set.
98 * But one array, that isn't actually a sale, contains cross sell data.
99 * This should be near the end of the array.
100 *
101 * @since 6.25.1
102 *
103 * @param array $data
104 *
105 * @return void
106 */
107 private function set_cross_sell( $data ) {
108 if ( ! self::cross_sell_is_valid( $data ) ) {
109 return;
110 }
111
112 $cross_sell_text = $data['cross_sell_text'];
113 $cross_sell_links = $data['cross_sell_link'];
114 $index = self::determine_cross_sell_index( $cross_sell_text );
115
116 self::$cross_sell_text = sanitize_text_field( $cross_sell_text[ $index ] );
117 self::$cross_sell_link = esc_url_raw( $cross_sell_links[ $index ] );
118 }
119
120 /**
121 * Check that both cross_sell_text and cross_sell_link are set and are arrays of the same size.
122 *
123 * @since 6.25.1
124 *
125 * @param array $data
126 *
127 * @return bool
128 */
129 private function cross_sell_is_valid( $data ) {
130 if ( empty( $data['cross_sell_text'] ) || empty( $data['cross_sell_link'] ) ) {
131 return false;
132 }
133
134 if ( ! is_array( $data['cross_sell_text'] ) || ! is_array( $data['cross_sell_link'] ) ) {
135 return false;
136 }
137
138 return count( $data['cross_sell_link'] ) === count( $data['cross_sell_text'] );
139 }
140
141 /**
142 * Determine which cross sell text to use.
143 * These are shown in order for 30 days before moving on to the next one.
144 *
145 * @since 6.25.1
146 *
147 * @param array $cross_sell_text
148 *
149 * @return int
150 */
151 private static function determine_cross_sell_index( $cross_sell_text ) {
152 $option_name = 'frm_cross_sell_settings';
153 $cross_sell_settings = get_option( $option_name );
154
155 if ( ! is_array( $cross_sell_settings ) ) {
156 $cross_sell_settings = array(
157 reset( $cross_sell_text ) => time(),
158 );
159 update_option( $option_name, $cross_sell_settings );
160 return 0;
161 }
162
163 foreach ( $cross_sell_text as $index => $current_text ) {
164 if ( ! isset( $cross_sell_settings[ $current_text ] ) ) {
165 $cross_sell_settings[ $current_text ] = time();
166 update_option( $option_name, $cross_sell_settings );
167 return $index;
168 }
169
170 $time_elapsed = time() - $cross_sell_settings[ $current_text ];
171
172 if ( $time_elapsed < DAY_IN_SECONDS * 30 ) {
173 return $index;
174 }
175 }
176
177 // If all options are expired, reset the option.
178 delete_option( $option_name );
179 return 0;
180 }
181
182 /**
183 * @param array|string $sale
184 *
185 * @return void
186 */
187 private function add_sale( $sale ) {
188 if ( ! is_array( self::$sales ) ) {
189 // This gets set in the constructor.
190 // This check is just here for Psalm analysis.
191 return;
192 }
193
194 if ( ! is_array( $sale ) || ! isset( $sale['key'] ) ) {
195 // if the API response is invalid, $sale may not be an array.
196 // if there are no sales from the API, it is returning a "No Entries Found" item with no key, so check for a key as well.
197 return;
198 }
199
200 if ( ! $this->sale_is_active( $sale ) ) {
201 return;
202 }
203
204 self::$sales[ $sale['key'] ] = $this->fill_sale( $sale );
205 }
206
207 /**
208 * @param array $sale
209 *
210 * @return array
211 */
212 private function fill_sale( $sale ) {
213 $defaults = array(
214 'key' => '',
215 'starts' => '',
216 'expires' => '',
217 // Use 'free', 'personal', 'business', 'elite', 'grandfathered'.
218 'who' => 'all',
219 'discount_percent' => 0,
220 'test_group' => '',
221 'lite_banner_cta_link' => '',
222 'lite_banner_cta_text' => '',
223 'menu_cta_link' => '',
224 'menu_cta_text' => '',
225 'dashboard_license_cta_link' => '',
226 'dashboard_license_cta_text' => '',
227 'global_settings_license_cta_link' => '',
228 'global_settings_license_cta_text' => '',
229 'global_settings_unlock_more_cta_link' => '',
230 'global_settings_unlock_more_cta_text' => '',
231 'global_settings_upgrade_cta_link' => '',
232 'builder_sidebar_cta_link' => '',
233 'builder_sidebar_cta_text' => '',
234 'banner_title' => '',
235 'banner_body' => '',
236 'banner_icon' => '',
237 'banner_text_color' => '',
238 'banner_bg_color' => '',
239 'banner_cta_link' => '',
240 'banner_cta_text' => '',
241 'banner_cta_text_color' => '',
242 'banner_cta_bg_color' => '',
243 );
244
245 return array_merge( $defaults, $sale );
246 }
247
248 /**
249 * Check if a sale is within the active period.
250 *
251 * @since 6.17
252 *
253 * @param array $sale
254 *
255 * @return bool
256 */
257 private function sale_is_active( $sale ) {
258 $starts = $sale['starts'];
259 $expires = $sale['expires'] + DAY_IN_SECONDS;
260 $date = new DateTime( 'now', new DateTimeZone( 'America/New_York' ) );
261 $today = $date->getTimestamp();
262 return $today >= $starts && $today <= $expires;
263 }
264
265 /**
266 * @since 6.17
267 *
268 * @return array|false
269 */
270 public function get_best_sale() {
271 if ( ! self::$sales ) {
272 return false;
273 }
274
275 if ( isset( self::$best_sale ) ) {
276 return self::$best_sale;
277 }
278
279 $best_sale = false;
280
281 foreach ( self::$sales as $sale ) {
282 if ( ! FrmApiHelper::is_for_user( $sale ) ) {
283 continue;
284 }
285
286 if ( ! $this->matches_ab_group( $sale ) ) {
287 continue;
288 }
289
290 if ( ! $best_sale || $sale['discount_percent'] > $best_sale['discount_percent'] ) {
291 $best_sale = $sale;
292 }
293 }
294
295 self::$best_sale = $best_sale;
296 return self::$best_sale;
297 }
298
299 /**
300 * Get text for best sale if applicable.
301 *
302 * @since 6.17
303 *
304 * @param string $key
305 *
306 * @return false|string False if no sale is active.
307 */
308 public static function get_best_sale_value( $key ) {
309 if ( ! isset( self::$instance ) ) {
310 self::$instance = new FrmSalesApi();
311 }
312
313 $sale = self::$instance->get_best_sale();
314
315 return is_array( $sale ) && ! empty( $sale[ $key ] ) ? $sale[ $key ] : false;
316 }
317
318 /**
319 * @since 6.17
320 *
321 * @param array $sale
322 *
323 * @return bool True if the sale is a match for the applicable group (if one is defined).
324 */
325 private function matches_ab_group( $sale ) {
326 if ( ! is_numeric( $sale['test_group'] ) ) {
327 // No test group, so return true.
328 return true;
329 }
330
331 $ab_group = $this->get_ab_group_for_current_site();
332 return $ab_group === $sale['test_group'];
333 }
334
335 /**
336 * @since 6.17
337 *
338 * @return int 1 or 0.
339 */
340 private function get_ab_group_for_current_site() {
341 $option = get_option( 'frm_sale_ab_group' );
342
343 if ( ! is_numeric( $option ) ) {
344 // Generate either 0 or 1.
345 $option = mt_rand( 0, 1 );
346 update_option( 'frm_sale_ab_group', $option, false );
347 }
348 return (int) $option;
349 }
350
351 /**
352 * Maybe show banner for the best sale.
353 *
354 * @since 6.21
355 *
356 * @return bool
357 */
358 public static function maybe_show_banner() {
359 if ( ! current_user_can( 'frm_change_settings' ) ) {
360 return false;
361 }
362
363 if ( ! isset( self::$instance ) ) {
364 self::$instance = new FrmSalesApi();
365 }
366
367 $sale = self::$instance->get_best_sale();
368
369 if ( ! $sale || ! is_array( $sale ) ) {
370 return false;
371 }
372
373 $banner_title = ! empty( $sale['banner_title'] ) ? $sale['banner_title'] : false;
374 $banner_body = ! empty( $sale['banner_body'] ) ? $sale['banner_body'] : false;
375
376 if ( false === $banner_title || false === $banner_body ) {
377 return false;
378 }
379
380 if ( self::is_banner_dismissed( $sale['key'] ) ) {
381 return false;
382 }
383
384 $banner_icon = ! empty( $sale['banner_icon'] ) ? $sale['banner_icon'] : 'generic';
385 $banner_bg_color = ! empty( $sale['banner_bg_color'] ) ? $sale['banner_bg_color'] : false;
386 $banner_text_color = ! empty( $sale['banner_text_color'] ) ? $sale['banner_text_color'] : false;
387 $banner_cta_link = ! empty( $sale['banner_cta_link'] ) ? $sale['banner_cta_link'] : false;
388
389 // translators: %s is the discount percentage.
390 $banner_cta_text = ! empty( $sale['banner_cta_text'] ) ? $sale['banner_cta_text'] : sprintf( __( 'GET %s OFF NOW', 'formidable' ), $sale['discount_percent'] . '%' );
391 $banner_cta_text_color = ! empty( $sale['banner_cta_text_color'] ) ? $sale['banner_cta_text_color'] : false;
392 $banner_cta_bg_color = ! empty( $sale['banner_cta_bg_color'] ) ? $sale['banner_cta_bg_color'] : false;
393
394 if ( false === $banner_cta_link ) {
395 $banner_cta_link = FrmAppHelper::admin_upgrade_link(
396 array(
397 'medium' => 'sales-api-banner',
398 'content' => $sale['key'],
399 )
400 );
401 }
402
403 $banner_attrs = array(
404 'id' => 'frm_sale_banner',
405 'data-url' => $banner_cta_link,
406 );
407
408 if ( false === $banner_bg_color || 'gradient' === $banner_bg_color ) {
409 $banner_attrs['class'] = 'frm-gradient';
410 } else {
411 $banner_attrs['style'] = 'background-color: ' . esc_attr( $banner_bg_color ) . ';';
412 }
413
414 $cta_attrs = array(
415 'href' => '#',
416 'style' => '',
417 );
418
419 if ( false !== $banner_cta_text_color ) {
420 $cta_attrs['style'] .= 'color: ' . esc_attr( $banner_cta_text_color ) . ';';
421 }
422
423 if ( false !== $banner_cta_bg_color ) {
424 $cta_attrs['style'] .= 'background-color: ' . esc_attr( $banner_cta_bg_color ) . ';';
425 }
426
427 $dismiss_attrs = array(
428 'href' => '#',
429 'class' => 'dismiss',
430 );
431
432 $content_attrs = array();
433
434 if ( false !== $banner_text_color ) {
435 $content_attrs['style'] = 'color: ' . esc_attr( $banner_text_color ) . ';';
436 $dismiss_attrs['style'] = 'color: ' . esc_attr( $banner_text_color ) . ';';
437 }
438
439 ?>
440 <div <?php FrmAppHelper::array_to_html_params( $banner_attrs, true ); ?>>
441 <div>
442 <img src="<?php echo esc_url( FrmAppHelper::plugin_url() . '/images/sales/' . $banner_icon . '.svg' ); ?>" alt="<?php echo esc_attr( $banner_title ); ?>" />
443 </div>
444 <div <?php FrmAppHelper::array_to_html_params( $content_attrs, true ); ?>>
445 <div class="frm-text-md frm-font-semibold">
446 <?php echo esc_html( $banner_title ); ?>
447 </div>
448 <div>
449 <?php echo esc_html( $banner_body ); ?>
450 </div>
451 </div>
452 <div>
453 <a <?php FrmAppHelper::array_to_html_params( $cta_attrs, true ); ?>>
454 <?php echo esc_html( $banner_cta_text ); ?>
455 </a>
456 </div>
457 <a <?php FrmAppHelper::array_to_html_params( $dismiss_attrs, true ); ?>><?php FrmAppHelper::icon_by_class( 'frm_icon_font frm_close_icon' ); ?></a>
458 </div>
459 <?php
460
461 return true;
462 }
463
464 /**
465 * Dismiss a banner via AJAX hook.
466 *
467 * @since 6.21
468 */
469 public static function dismiss_banner() {
470 FrmAppHelper::permission_check( 'frm_view_forms' );
471 check_ajax_referer( 'frm_ajax', 'nonce' );
472
473 if ( ! isset( self::$instance ) ) {
474 self::$instance = new FrmSalesApi();
475 }
476
477 $sale = self::$instance->get_best_sale();
478
479 if ( ! $sale || ! is_array( $sale ) ) {
480 wp_send_json_error();
481 }
482
483 $dismissed_sales = get_user_option( 'frm_dismissed_sales', get_current_user_id() );
484
485 if ( ! is_array( $dismissed_sales ) ) {
486 $dismissed_sales = array();
487 }
488
489 $dismissed_sales[] = $sale['key'];
490 update_user_option( get_current_user_id(), 'frm_dismissed_sales', $dismissed_sales );
491
492 wp_send_json_success();
493 }
494
495 /**
496 * @param string $key
497 *
498 * @return bool
499 */
500 private static function is_banner_dismissed( $key ) {
501 $dismissed_sales = get_user_option( 'frm_dismissed_sales', get_current_user_id() );
502 return is_array( $dismissed_sales ) && in_array( $key, $dismissed_sales, true );
503 }
504
505 /**
506 * @return void
507 */
508 public static function menu() {
509 if ( false === self::$sales ) {
510 new self();
511 }
512
513 if ( ! self::$cross_sell_text || ! self::$cross_sell_link ) {
514 return;
515 }
516
517 add_submenu_page(
518 'formidable',
519 esc_html( self::$cross_sell_text ) . ' | Formidable',
520 esc_html( self::$cross_sell_text ),
521 'activate_plugins',
522 'frm-sales-api-cross-sell',
523 function () {
524 // There is no page. The redirect logic is handled below, before this callback is triggered.
525 }
526 );
527
528 add_action(
529 'admin_init',
530 function () {
531 if ( ! current_user_can( 'activate_plugins' ) ) {
532 return;
533 }
534
535 if ( 'frm-sales-api-cross-sell' === FrmAppHelper::simple_get( 'page' ) && ! empty( self::$cross_sell_link ) ) {
536 wp_redirect( self::$cross_sell_link );
537 exit;
538 }
539 }
540 );
541 }
542 }
543