PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
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 trunk, at classes/models/FrmSalesApi.php

457 lines 11.0 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 * Flag to prevent the $this->set_sales() call in the constructor.
34 *
35 * @since 6.34
36 *
37 * @var bool
38 */
39 private static $prevent_new_sales_request = false;
40
41 public function __construct() {
42 $this->set_cache_key();
43
44 if ( ! self::$prevent_new_sales_request && false === self::$sales ) {
45 $this->set_sales();
46 }
47 }
48
49 /**
50 * @since 6.34
51 *
52 * @return void
53 */
54 public static function prevent_new_sales_requests() {
55 self::$prevent_new_sales_request = true;
56 }
57
58 /**
59 * @since 6.34
60 *
61 * @return void
62 */
63 public static function allow_new_sales_requests() {
64 self::$prevent_new_sales_request = false;
65 }
66
67 /**
68 * @since 6.17
69 *
70 * @return void
71 */
72 protected function set_cache_key() {
73 $this->cache_key = 'frm_sales_cache';
74 }
75
76 /**
77 * @since 6.17
78 *
79 * @return string
80 */
81 protected function api_url() {
82 return 'https://plapi.formidableforms.com/sales/';
83 }
84
85 /**
86 * If the last check was a rate limit, we'll need to check again sooner.
87 * Other APIs use the FrmFormApi function which uses a 5 minute timeout. But for Sales, we can use 1 hour.
88 *
89 * @since 6.32
90 *
91 * @param array $addons
92 *
93 * @return string
94 */
95 protected function get_cache_timeout( $addons ) {
96 if ( isset( $addons['response_code'] ) && 429 === $addons['response_code'] ) {
97 return '+1 hour';
98 }
99 return $this->cache_timeout;
100 }
101
102 /**
103 * @since 6.17
104 *
105 * @return void
106 */
107 private function set_sales() {
108 self::$sales = array();
109
110 $api = $this->get_api_info();
111
112 if ( ! $api ) {
113 return;
114 }
115
116 foreach ( $api as $sale ) {
117 $this->add_sale( $sale );
118 }
119 }
120
121 /**
122 * @param array|string $sale
123 *
124 * @return void
125 */
126 private function add_sale( $sale ) {
127 if ( ! is_array( self::$sales ) ) {
128 // This gets set in the constructor.
129 // This check is just here for Psalm analysis.
130 return;
131 }
132
133 if ( ! is_array( $sale ) || ! isset( $sale['key'] ) ) {
134 // If the API response is invalid, $sale may not be an array.
135 // 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.
136 return;
137 }
138
139 if ( ! $this->sale_is_active( $sale ) ) {
140 return;
141 }
142
143 self::$sales[ $sale['key'] ] = $this->fill_sale( $sale );
144 }
145
146 /**
147 * @param array $sale
148 *
149 * @return array
150 */
151 private function fill_sale( $sale ) {
152 $defaults = array(
153 'key' => '',
154 'starts' => '',
155 'expires' => '',
156 // Use 'free', 'personal', 'business', 'elite', 'grandfathered'.
157 'who' => 'all',
158 'discount_percent' => 0,
159 'test_group' => '',
160 'lite_banner_cta_link' => '',
161 'lite_banner_cta_text' => '',
162 'menu_cta_link' => '',
163 'menu_cta_text' => '',
164 'dashboard_license_cta_link' => '',
165 'dashboard_license_cta_text' => '',
166 'global_settings_license_cta_link' => '',
167 'global_settings_license_cta_text' => '',
168 'global_settings_unlock_more_cta_link' => '',
169 'global_settings_unlock_more_cta_text' => '',
170 'global_settings_upgrade_cta_link' => '',
171 'builder_sidebar_cta_link' => '',
172 'builder_sidebar_cta_text' => '',
173 'banner_title' => '',
174 'banner_body' => '',
175 'banner_icon' => '',
176 'banner_text_color' => '',
177 'banner_bg_color' => '',
178 'banner_cta_link' => '',
179 'banner_cta_text' => '',
180 'banner_cta_text_color' => '',
181 'banner_cta_bg_color' => '',
182 );
183
184 return array_merge( $defaults, $sale );
185 }
186
187 /**
188 * Check if a sale is within the active period.
189 *
190 * @since 6.17
191 *
192 * @param array $sale
193 *
194 * @return bool
195 */
196 private function sale_is_active( $sale ) {
197 $starts = $sale['starts'];
198 $expires = $sale['expires'] + DAY_IN_SECONDS;
199 $date = new DateTime( 'now', new DateTimeZone( 'America/New_York' ) );
200 $today = $date->getTimestamp();
201 return $today >= $starts && $today <= $expires;
202 }
203
204 /**
205 * @since 6.17
206 *
207 * @return array|false
208 */
209 public function get_best_sale() {
210 if ( ! self::$sales ) {
211 return false;
212 }
213
214 if ( isset( self::$best_sale ) ) {
215 return self::$best_sale;
216 }
217
218 $best_sale = false;
219
220 foreach ( self::$sales as $sale ) {
221 if ( ! FrmApiHelper::is_for_user( $sale ) ) {
222 continue;
223 }
224
225 if ( ! $this->matches_ab_group( $sale ) ) {
226 continue;
227 }
228
229 if ( ! $best_sale || $sale['discount_percent'] > $best_sale['discount_percent'] ) {
230 $best_sale = $sale;
231 }
232 }
233
234 self::$best_sale = $best_sale;
235 return self::$best_sale;
236 }
237
238 /**
239 * Get text for best sale if applicable.
240 *
241 * @since 6.17
242 *
243 * @param string $key
244 *
245 * @return false|string False if no sale is active.
246 */
247 public static function get_best_sale_value( $key ) {
248 if ( ! isset( self::$instance ) ) {
249 self::$instance = new FrmSalesApi();
250 }
251
252 $sale = self::$instance->get_best_sale();
253
254 if ( ! is_array( $sale ) || empty( $sale[ $key ] ) ) {
255 return false;
256 }
257
258 $sale_value = $sale[ $key ];
259
260 if ( str_ends_with( $key, '_link' ) && ! str_starts_with( $sale_value, 'https://formidableforms.com' ) ) {
261 return false;
262 }
263
264 return $sale_value;
265 }
266
267 /**
268 * @since 6.17
269 *
270 * @param array $sale
271 *
272 * @return bool True if the sale is a match for the applicable group (if one is defined).
273 */
274 private function matches_ab_group( $sale ) {
275 if ( ! is_numeric( $sale['test_group'] ) ) {
276 // No test group, so return true.
277 return true;
278 }
279
280 $ab_group = $this->get_ab_group_for_current_site();
281 return $ab_group === $sale['test_group'];
282 }
283
284 /**
285 * @since 6.17
286 *
287 * @return int 1 or 0.
288 */
289 private function get_ab_group_for_current_site() {
290 $option = get_option( 'frm_sale_ab_group' );
291
292 if ( ! is_numeric( $option ) ) {
293 // Generate either 0 or 1.
294 $option = mt_rand( 0, 1 );
295 update_option( 'frm_sale_ab_group', $option, false );
296 }
297
298 return (int) $option;
299 }
300
301 /**
302 * Maybe show banner for the best sale.
303 *
304 * @since 6.21
305 *
306 * @return bool
307 */
308 public static function maybe_show_banner() {
309 if ( ! current_user_can( 'frm_change_settings' ) ) {
310 return false;
311 }
312
313 if ( ! isset( self::$instance ) ) {
314 self::$instance = new FrmSalesApi();
315 }
316
317 $sale = self::$instance->get_best_sale();
318
319 if ( ! $sale || ! is_array( $sale ) ) {
320 return false;
321 }
322
323 $banner_title = ! empty( $sale['banner_title'] ) ? $sale['banner_title'] : false;
324 $banner_body = ! empty( $sale['banner_body'] ) ? $sale['banner_body'] : false;
325
326 if ( false === $banner_title || false === $banner_body ) {
327 return false;
328 }
329
330 if ( self::is_banner_dismissed( $sale['key'] ) ) {
331 return false;
332 }
333
334 $banner_icon = ! empty( $sale['banner_icon'] ) ? $sale['banner_icon'] : 'generic';
335 $banner_bg_color = ! empty( $sale['banner_bg_color'] ) ? $sale['banner_bg_color'] : false;
336 $banner_text_color = ! empty( $sale['banner_text_color'] ) ? $sale['banner_text_color'] : false;
337 $banner_cta_link = ! empty( $sale['banner_cta_link'] ) ? $sale['banner_cta_link'] : false;
338
339 // translators: %s is the discount percentage.
340 $banner_cta_text = ! empty( $sale['banner_cta_text'] ) ? $sale['banner_cta_text'] : sprintf( __( 'GET %s OFF NOW', 'formidable' ), $sale['discount_percent'] . '%' );
341 $banner_cta_text_color = ! empty( $sale['banner_cta_text_color'] ) ? $sale['banner_cta_text_color'] : false;
342 $banner_cta_bg_color = ! empty( $sale['banner_cta_bg_color'] ) ? $sale['banner_cta_bg_color'] : false;
343
344 if ( false === $banner_cta_link ) {
345 $banner_cta_link = FrmAppHelper::admin_upgrade_link(
346 array(
347 'medium' => 'sales-api-banner',
348 'content' => $sale['key'],
349 )
350 );
351 }
352
353 $banner_attrs = array(
354 'id' => 'frm_sale_banner',
355 'data-url' => $banner_cta_link,
356 );
357
358 if ( false === $banner_bg_color || 'gradient' === $banner_bg_color ) {
359 $banner_attrs['class'] = 'frm-gradient';
360 } else {
361 $banner_attrs['style'] = 'background-color: ' . esc_attr( $banner_bg_color ) . ';';
362 }
363
364 $cta_attrs = array(
365 'href' => '#',
366 'style' => '',
367 );
368
369 if ( false !== $banner_cta_text_color ) {
370 $cta_attrs['style'] .= 'color: ' . esc_attr( $banner_cta_text_color ) . ';';
371 }
372
373 if ( false !== $banner_cta_bg_color ) {
374 $cta_attrs['style'] .= 'background-color: ' . esc_attr( $banner_cta_bg_color ) . ';';
375 }
376
377 $dismiss_attrs = array(
378 'href' => '#',
379 'class' => 'dismiss',
380 );
381
382 $content_attrs = array();
383
384 if ( false !== $banner_text_color ) {
385 $content_attrs['style'] = 'color: ' . esc_attr( $banner_text_color ) . ';';
386 $dismiss_attrs['style'] = 'color: ' . esc_attr( $banner_text_color ) . ';';
387 }
388
389 // phpcs:disable Generic.WhiteSpace.ScopeIndent
390 ?>
391 <div <?php FrmAppHelper::array_to_html_params( $banner_attrs, true ); ?>>
392 <div>
393 <img src="<?php echo esc_url( FrmAppHelper::plugin_url() . '/images/sales/' . $banner_icon . '.svg' ); ?>" alt="<?php echo esc_attr( $banner_title ); ?>" />
394 </div>
395 <div <?php FrmAppHelper::array_to_html_params( $content_attrs, true ); ?>>
396 <div class="frm-text-md frm-font-semibold">
397 <?php echo esc_html( $banner_title ); ?>
398 </div>
399 <div>
400 <?php echo esc_html( $banner_body ); ?>
401 </div>
402 </div>
403 <div>
404 <a <?php FrmAppHelper::array_to_html_params( $cta_attrs, true ); ?>>
405 <?php echo esc_html( $banner_cta_text ); ?>
406 </a>
407 </div>
408 <a <?php FrmAppHelper::array_to_html_params( $dismiss_attrs, true ); ?>><?php FrmAppHelper::icon_by_class( 'frmfont frm_close_icon' ); ?></a>
409 </div>
410 <?php
411 // phpcs:enable Generic.WhiteSpace.ScopeIndent
412
413 return true;
414 }
415
416 /**
417 * Dismiss a banner via AJAX hook.
418 *
419 * @since 6.21
420 */
421 public static function dismiss_banner() {
422 FrmAppHelper::permission_check( 'frm_view_forms' );
423 check_ajax_referer( 'frm_ajax', 'nonce' );
424
425 if ( ! isset( self::$instance ) ) {
426 self::$instance = new FrmSalesApi();
427 }
428
429 $sale = self::$instance->get_best_sale();
430
431 if ( ! $sale || ! is_array( $sale ) ) {
432 wp_send_json_error();
433 }
434
435 $dismissed_sales = get_user_option( 'frm_dismissed_sales', get_current_user_id() );
436
437 if ( ! is_array( $dismissed_sales ) ) {
438 $dismissed_sales = array();
439 }
440
441 $dismissed_sales[] = $sale['key'];
442 update_user_option( get_current_user_id(), 'frm_dismissed_sales', $dismissed_sales );
443
444 wp_send_json_success();
445 }
446
447 /**
448 * @param string $key
449 *
450 * @return bool
451 */
452 private static function is_banner_dismissed( $key ) {
453 $dismissed_sales = get_user_option( 'frm_dismissed_sales', get_current_user_id() );
454 return is_array( $dismissed_sales ) && in_array( $key, $dismissed_sales, true );
455 }
456 }
457