PluginProbe ʕ •ᴥ•ʔ
Strong Testimonials / 3.3.6
Strong Testimonials v3.3.6
3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.0.1 2.30.9 2.31.10 2.32 2.32.1 2.32.2 2.32.3 2.32.4 2.33 2.34 2.35 2.36 2.37 2.38 2.38.1 2.39 2.39.1 2.39.2 2.39.3 2.40.0 2.40.1 2.40.2 2.40.3 2.40.4 2.40.5 2.40.6 2.40.7 2.41.0 2.41.1 2.50.0 2.50.1 2.50.2 2.50.3 2.50.4 2.51.0 2.51.1 2.51.2 2.51.3 2.51.4 2.51.5 2.51.6 2.51.7 2.51.8 2.51.9 3.0.0 3.0.1 3.0.2 3.0.3 3.1.0 3.1.1 3.1.10 3.1.11 3.1.12 3.1.13 3.1.14 3.1.15 3.1.16 3.1.17 3.1.18 3.1.19 3.1.2 3.1.20 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.1 3.2.10 3.2.11 3.2.12 3.2.13 3.2.14 3.2.15 3.2.16 3.2.17 3.2.18 3.2.19 3.2.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 3.3.0
strong-testimonials / admin / wpchill / class-wpchill-upsells.php
strong-testimonials / admin / wpchill Last commit date
apps 9 months ago assets 1 year ago icons 1 year ago scripts 9 months ago class-wpchill-about-us.php 1 year ago class-wpchill-notifications.php 9 months ago class-wpchill-rest-api.php 1 year ago class-wpchill-upsells.php 2 months ago
class-wpchill-upsells.php
474 lines
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 if ( ! class_exists( 'WPChill_Remote_Upsells' ) ) {
8 /**
9 * Class WPChill_Remote_Upsells
10 *
11 * Handles remote upsell promotions fetched from external REST API.
12 * Checks daily via WP Cron if promotions are still valid.
13 *
14 */
15 class WPChill_Remote_Upsells {
16
17 /**
18 * Singleton instance
19 *
20 * @var WPChill_Remote_Upsells
21 */
22 private static $instance;
23
24 /**
25 * Hook name for the cron event
26 *
27 * @var string
28 */
29 private $cron_hook = 'wpchill_upsells_check';
30
31 /**
32 * Option name for storing upsell data
33 *
34 * @var string
35 */
36 private $option_name = 'wpchill_upsells_data';
37
38 /**
39 * Transient name for caching API requests
40 *
41 * @var string
42 */
43 private $cache_transient = 'wpchill_upsells_cache';
44
45 /**
46 * Remote API URL
47 *
48 * @var string
49 */
50 private $api_url = '';
51
52 /**
53 * Current active promotions data (array of promotions)
54 *
55 * @var array
56 */
57 private $active_promotions = array();
58
59 /**
60 * Constructor
61 *
62 * @param array $args Optional arguments (api_url).
63 */
64 public function __construct( $args = array() ) {
65 // Set API URL from arguments
66 if ( ! empty( $args['api_url'] ) ) {
67 $this->api_url = $args['api_url'];
68 }
69
70 // Schedule daily cron if not already scheduled
71 if ( ! wp_next_scheduled( $this->cron_hook ) ) {
72 wp_schedule_event( time(), 'daily', $this->cron_hook );
73 }
74
75 // Hook the cron action
76 add_action( $this->cron_hook, array( $this, 'fetch_remote_upsells' ) );
77
78 // Load and apply active promotions
79 $this->load_active_promotions();
80
81 // Register activation and deactivation hooks
82 $plugin_slug = explode( '/', plugin_basename( __FILE__ ) )[0];
83 $active_plugins = (array) get_option( 'active_plugins', array() );
84 foreach ( $active_plugins as $active_plugin ) {
85 if ( 0 === strpos( $active_plugin, $plugin_slug . '/' ) ) {
86 $plugin_file = WP_PLUGIN_DIR . '/' . $active_plugin;
87 register_activation_hook( $plugin_file, array( $this, 'activate' ) );
88 register_deactivation_hook( $plugin_file, array( $this, 'deactivate' ) );
89 break;
90 }
91 }
92 }
93
94 /**
95 * Get singleton instance
96 *
97 * @param array $args Optional arguments (api_url).
98 * @return WPChill_Remote_Upsells
99 */
100 public static function get_instance( $args = array() ) {
101 if ( ! isset( self::$instance ) || ! ( self::$instance instanceof WPChill_Remote_Upsells ) ) {
102 self::$instance = new WPChill_Remote_Upsells( $args );
103 }
104
105 return self::$instance;
106 }
107
108 /**
109 * Load active promotions from options and apply filters for each valid one
110 */
111 private function load_active_promotions() {
112 $data = get_option( $this->option_name, array() );
113
114 if ( empty( $data ) || ! is_array( $data ) ) {
115 return;
116 }
117
118 $has_css = false;
119
120 foreach ( $data as $key => $promotion ) {
121 // Skip invalid promotions
122 if ( ! $this->validate_single_promotion( $promotion ) ) {
123 continue;
124 }
125
126 // Skip expired promotions
127 if ( ! $this->is_promotion_valid( $promotion ) ) {
128 continue;
129 }
130
131 // Store active promotion
132 $filter_hook = sanitize_text_field( $promotion['filter'] );
133 $this->active_promotions[ $filter_hook ] = $promotion;
134
135 // Apply the upsell button filter for this promotion
136 add_filter( $filter_hook, array( $this, 'override_upsell_buttons' ), 15, 2 );
137
138 // Check if any promotion has CSS
139 if ( ! empty( $promotion['css'] ) ) {
140 $has_css = true;
141 }
142 }
143
144 // Add CSS output only once if any promotion has CSS
145 if ( $has_css ) {
146 add_action( 'admin_print_styles', array( $this, 'output_promotion_styles' ), 999 );
147 }
148 }
149
150 /**
151 * Check if promotion is still valid based on start/end dates
152 *
153 * @param array $data Promotion data.
154 * @return bool
155 */
156 private function is_promotion_valid( $data ) {
157 if ( empty( $data ) || ! is_array( $data ) ) {
158 return false;
159 }
160
161 $now = time();
162
163 // Check start date if provided
164 if ( ! empty( $data['start_date'] ) ) {
165 $start = strtotime( $data['start_date'] );
166 if ( $now < $start ) {
167 return false;
168 }
169 }
170
171 // Check end date if provided
172 if ( ! empty( $data['end_date'] ) ) {
173 $end = strtotime( $data['end_date'] );
174 if ( $now > $end ) {
175 return false;
176 }
177 }
178
179 // Check active flag if provided
180 if ( isset( $data['active'] ) && ! $data['active'] ) {
181 return false;
182 }
183
184 return true;
185 }
186
187 /**
188 * Fetch upsell data from remote API
189 */
190 public function fetch_remote_upsells() {
191 // Return cached data if available
192 $cached = get_transient( $this->cache_transient );
193 if ( false !== $cached ) {
194 return $cached;
195 }
196
197 $api_url = apply_filters( 'wpchill_upsells_api_url', $this->api_url );
198
199 if ( empty( $api_url ) ) {
200 return array();
201 }
202
203 $response = wp_remote_get(
204 $api_url,
205 array(
206 'timeout' => 15,
207 )
208 );
209
210 if ( is_wp_error( $response ) ) {
211 return array();
212 }
213
214 $status_code = wp_remote_retrieve_response_code( $response );
215 if ( 200 !== $status_code ) {
216 return array();
217 }
218
219 $body = wp_remote_retrieve_body( $response );
220 $data = json_decode( $body, true );
221
222 if ( json_last_error() !== JSON_ERROR_NONE ) {
223 return array();
224 }
225
226 // Validate - must be an array of promotions
227 if ( ! $this->validate_promotions_data( $data ) ) {
228 $this->clear_promotions();
229 return array();
230 }
231
232 // Store the promotions data
233 update_option( $this->option_name, $data );
234 set_transient( $this->cache_transient, $data, DAY_IN_SECONDS );
235
236 return $data;
237 }
238
239 /**
240 * Validate promotions data structure (array of promotions)
241 *
242 * Expected structure:
243 * [
244 * {
245 * "active": true,
246 * "start_date": "2024-11-25",
247 * "end_date": "2024-12-02",
248 * "filter": "modula_upsell_buttons",
249 * "buttons": [...],
250 * "css": "..."
251 * },
252 * {
253 * "active": true,
254 * "start_date": "2024-11-25",
255 * "end_date": "2024-12-02",
256 * "filter": "dlm_upsell_buttons",
257 * "buttons": [...],
258 * "css": "..."
259 * }
260 * ]
261 *
262 * @param array $data Promotions data.
263 * @return bool
264 */
265 private function validate_promotions_data( $data ) {
266 if ( empty( $data ) || ! is_array( $data ) ) {
267 return false;
268 }
269
270 // Check if it's an array of promotions (not a single promotion)
271 // If first key is numeric, it's an array of promotions
272 if ( ! isset( $data[0] ) ) {
273 return false;
274 }
275
276 // Validate at least one promotion
277 foreach ( $data as $promotion ) {
278 if ( $this->validate_single_promotion( $promotion ) ) {
279 return true;
280 }
281 }
282
283 return false;
284 }
285
286 /**
287 * Validate a single promotion data structure
288 *
289 * @param array $promotion Single promotion data.
290 * @return bool
291 */
292 private function validate_single_promotion( $promotion ) {
293 if ( empty( $promotion ) || ! is_array( $promotion ) ) {
294 return false;
295 }
296
297 // We need filter and buttons data
298 if ( empty( $promotion['filter'] ) ) {
299 return false;
300 }
301
302 if ( empty( $promotion['buttons'] ) || ! is_array( $promotion['buttons'] ) ) {
303 return false;
304 }
305
306 return true;
307 }
308
309 /**
310 * Override upsell buttons with promotion data
311 *
312 * @param string $buttons Original buttons HTML.
313 * @param string $context The upsell context/location.
314 * @return string Modified buttons HTML.
315 */
316 public function override_upsell_buttons( $buttons, $context = '' ) {
317 // Get current filter being executed
318 $current_filter = current_filter();
319
320 // Find the promotion for this filter
321 if ( ! isset( $this->active_promotions[ $current_filter ] ) ) {
322 return $buttons;
323 }
324
325 $promotion = $this->active_promotions[ $current_filter ];
326
327 if ( empty( $promotion['buttons'] ) ) {
328 return $buttons;
329 }
330
331 // Extract original URLs from the buttons
332 preg_match_all( '~<a(.*?)href="([^"]+)"(.*?)>~', $buttons, $matches );
333 $original_urls = isset( $matches[2] ) ? $matches[2] : array();
334
335 $new_buttons = '';
336 $button_index = 0;
337
338 foreach ( $promotion['buttons'] as $button ) {
339 if ( empty( $button['text'] ) ) {
340 continue;
341 }
342
343 // Determine the URL
344 $url = '';
345 if ( isset( $button['url'] ) ) {
346 if ( 'use_original' === $button['url'] && isset( $original_urls[ $button_index ] ) ) {
347 $url = $original_urls[ $button_index ];
348 } else {
349 $url = $button['url'];
350 }
351 } elseif ( isset( $original_urls[ $button_index ] ) ) {
352 $url = $original_urls[ $button_index ];
353 }
354
355 // Build button attributes
356 $target = isset( $button['target'] ) ? $button['target'] : '_blank';
357 $class = isset( $button['class'] ) ? $button['class'] : 'button';
358 $style = isset( $button['style'] ) ? ' style="' . esc_attr( $button['style'] ) . '"' : '';
359
360 $new_buttons .= sprintf(
361 '<a target="%s" href="%s" class="%s"%s>%s</a>',
362 esc_attr( $target ),
363 esc_url( $url ),
364 esc_attr( $class ),
365 $style,
366 esc_html( $button['text'] )
367 );
368
369 ++$button_index;
370 }
371
372 return $new_buttons;
373 }
374
375 /**
376 * Output promotion CSS styles from all active promotions
377 */
378 public function output_promotion_styles() {
379 if ( empty( $this->active_promotions ) ) {
380 return;
381 }
382
383 $css = '';
384
385 foreach ( $this->active_promotions as $promotion ) {
386 if ( ! empty( $promotion['css'] ) ) {
387 $css .= $promotion['css'] . "\n";
388 }
389 }
390
391 if ( ! empty( $css ) ) {
392 echo '<style>' . wp_strip_all_tags( $css ) . '</style>';
393 }
394 }
395
396 /**
397 * Clear stored promotions data
398 */
399 public function clear_promotions() {
400 delete_option( $this->option_name );
401 delete_transient( $this->cache_transient );
402 $this->active_promotions = array();
403 }
404
405 /**
406 * Get current active promotions data
407 *
408 * @return array
409 */
410 public function get_active_promotions() {
411 return $this->active_promotions;
412 }
413
414 /**
415 * Get promotion for a specific filter
416 *
417 * @param string $filter Filter hook name.
418 * @return array|false
419 */
420 public function get_promotion_for_filter( $filter ) {
421 return isset( $this->active_promotions[ $filter ] ) ? $this->active_promotions[ $filter ] : false;
422 }
423
424 /**
425 * Check if there are any active promotions
426 *
427 * @return bool
428 */
429 public function has_active_promotions() {
430 return ! empty( $this->active_promotions );
431 }
432
433 /**
434 * Manually set API URL
435 *
436 * @param string $url API URL.
437 */
438 public function set_api_url( $url ) {
439 $this->api_url = $url;
440 }
441
442 /**
443 * Run initial promotions check on plugin activation
444 */
445 public function activate() {
446 $this->fetch_remote_upsells();
447 }
448
449 /**
450 * Clean up on plugin deactivation
451 */
452 public function deactivate() {
453 wp_clear_scheduled_hook( $this->cron_hook );
454 }
455
456 /**
457 * Get transients to be cleared on uninstall
458 *
459 * @param array $transients Existing transients array.
460 * @return array
461 */
462 public function get_transients_to_clear( $transients ) {
463 $transients[] = $this->cache_transient;
464 return $transients;
465 }
466 }
467 // Initiate WPChill Upsells (remote promotions)
468 WPChill_Remote_Upsells::get_instance(
469 array(
470 'api_url' => 'https://wp-modula.com/wp-json/upsells/v1/get',
471 )
472 );
473 }
474