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 |