api_url = $args['api_url'];
}
// Schedule daily cron if not already scheduled
if ( ! wp_next_scheduled( $this->cron_hook ) ) {
wp_schedule_event( time(), 'daily', $this->cron_hook );
}
// Hook the cron action
add_action( $this->cron_hook, array( $this, 'fetch_remote_upsells' ) );
// Load and apply active promotions
$this->load_active_promotions();
// Register activation and deactivation hooks
$plugin_slug = explode( '/', plugin_basename( __FILE__ ) )[0];
$active_plugins = (array) get_option( 'active_plugins', array() );
foreach ( $active_plugins as $active_plugin ) {
if ( 0 === strpos( $active_plugin, $plugin_slug . '/' ) ) {
$plugin_file = WP_PLUGIN_DIR . '/' . $active_plugin;
register_activation_hook( $plugin_file, array( $this, 'activate' ) );
register_deactivation_hook( $plugin_file, array( $this, 'deactivate' ) );
break;
}
}
}
/**
* Get singleton instance
*
* @param array $args Optional arguments (api_url).
* @return WPChill_Remote_Upsells
*/
public static function get_instance( $args = array() ) {
if ( ! isset( self::$instance ) || ! ( self::$instance instanceof WPChill_Remote_Upsells ) ) {
self::$instance = new WPChill_Remote_Upsells( $args );
}
return self::$instance;
}
/**
* Load active promotions from options and apply filters for each valid one
*/
private function load_active_promotions() {
$data = get_option( $this->option_name, array() );
if ( empty( $data ) || ! is_array( $data ) ) {
return;
}
$has_css = false;
foreach ( $data as $key => $promotion ) {
// Skip invalid promotions
if ( ! $this->validate_single_promotion( $promotion ) ) {
continue;
}
// Skip expired promotions
if ( ! $this->is_promotion_valid( $promotion ) ) {
continue;
}
// Store active promotion
$filter_hook = sanitize_text_field( $promotion['filter'] );
$this->active_promotions[ $filter_hook ] = $promotion;
// Apply the upsell button filter for this promotion
add_filter( $filter_hook, array( $this, 'override_upsell_buttons' ), 15, 2 );
// Check if any promotion has CSS
if ( ! empty( $promotion['css'] ) ) {
$has_css = true;
}
}
// Add CSS output only once if any promotion has CSS
if ( $has_css ) {
add_action( 'admin_print_styles', array( $this, 'output_promotion_styles' ), 999 );
}
}
/**
* Check if promotion is still valid based on start/end dates
*
* @param array $data Promotion data.
* @return bool
*/
private function is_promotion_valid( $data ) {
if ( empty( $data ) || ! is_array( $data ) ) {
return false;
}
$now = time();
// Check start date if provided
if ( ! empty( $data['start_date'] ) ) {
$start = strtotime( $data['start_date'] );
if ( $now < $start ) {
return false;
}
}
// Check end date if provided
if ( ! empty( $data['end_date'] ) ) {
$end = strtotime( $data['end_date'] );
if ( $now > $end ) {
return false;
}
}
// Check active flag if provided
if ( isset( $data['active'] ) && ! $data['active'] ) {
return false;
}
return true;
}
/**
* Fetch upsell data from remote API
*/
public function fetch_remote_upsells() {
// Return cached data if available
$cached = get_transient( $this->cache_transient );
if ( false !== $cached ) {
return $cached;
}
$api_url = apply_filters( 'wpchill_upsells_api_url', $this->api_url );
if ( empty( $api_url ) ) {
return array();
}
$response = wp_remote_get(
$api_url,
array(
'timeout' => 15,
)
);
if ( is_wp_error( $response ) ) {
return array();
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
return array();
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return array();
}
// Validate - must be an array of promotions
if ( ! $this->validate_promotions_data( $data ) ) {
$this->clear_promotions();
return array();
}
// Store the promotions data
update_option( $this->option_name, $data );
set_transient( $this->cache_transient, $data, DAY_IN_SECONDS );
return $data;
}
/**
* Validate promotions data structure (array of promotions)
*
* Expected structure:
* [
* {
* "active": true,
* "start_date": "2024-11-25",
* "end_date": "2024-12-02",
* "filter": "modula_upsell_buttons",
* "buttons": [...],
* "css": "..."
* },
* {
* "active": true,
* "start_date": "2024-11-25",
* "end_date": "2024-12-02",
* "filter": "dlm_upsell_buttons",
* "buttons": [...],
* "css": "..."
* }
* ]
*
* @param array $data Promotions data.
* @return bool
*/
private function validate_promotions_data( $data ) {
if ( empty( $data ) || ! is_array( $data ) ) {
return false;
}
// Check if it's an array of promotions (not a single promotion)
// If first key is numeric, it's an array of promotions
if ( ! isset( $data[0] ) ) {
return false;
}
// Validate at least one promotion
foreach ( $data as $promotion ) {
if ( $this->validate_single_promotion( $promotion ) ) {
return true;
}
}
return false;
}
/**
* Validate a single promotion data structure
*
* @param array $promotion Single promotion data.
* @return bool
*/
private function validate_single_promotion( $promotion ) {
if ( empty( $promotion ) || ! is_array( $promotion ) ) {
return false;
}
// We need filter and buttons data
if ( empty( $promotion['filter'] ) ) {
return false;
}
if ( empty( $promotion['buttons'] ) || ! is_array( $promotion['buttons'] ) ) {
return false;
}
return true;
}
/**
* Override upsell buttons with promotion data
*
* @param string $buttons Original buttons HTML.
* @param string $context The upsell context/location.
* @return string Modified buttons HTML.
*/
public function override_upsell_buttons( $buttons, $context = '' ) {
// Get current filter being executed
$current_filter = current_filter();
// Find the promotion for this filter
if ( ! isset( $this->active_promotions[ $current_filter ] ) ) {
return $buttons;
}
$promotion = $this->active_promotions[ $current_filter ];
if ( empty( $promotion['buttons'] ) ) {
return $buttons;
}
// Extract original URLs from the buttons
preg_match_all( '~~', $buttons, $matches );
$original_urls = isset( $matches[2] ) ? $matches[2] : array();
$new_buttons = '';
$button_index = 0;
foreach ( $promotion['buttons'] as $button ) {
if ( empty( $button['text'] ) ) {
continue;
}
// Determine the URL
$url = '';
if ( isset( $button['url'] ) ) {
if ( 'use_original' === $button['url'] && isset( $original_urls[ $button_index ] ) ) {
$url = $original_urls[ $button_index ];
} else {
$url = $button['url'];
}
} elseif ( isset( $original_urls[ $button_index ] ) ) {
$url = $original_urls[ $button_index ];
}
// Build button attributes
$target = isset( $button['target'] ) ? $button['target'] : '_blank';
$class = isset( $button['class'] ) ? $button['class'] : 'button';
$style = isset( $button['style'] ) ? ' style="' . esc_attr( $button['style'] ) . '"' : '';
$new_buttons .= sprintf(
'%s',
esc_attr( $target ),
esc_url( $url ),
esc_attr( $class ),
$style,
esc_html( $button['text'] )
);
++$button_index;
}
return $new_buttons;
}
/**
* Output promotion CSS styles from all active promotions
*/
public function output_promotion_styles() {
if ( empty( $this->active_promotions ) ) {
return;
}
$css = '';
foreach ( $this->active_promotions as $promotion ) {
if ( ! empty( $promotion['css'] ) ) {
$css .= $promotion['css'] . "\n";
}
}
if ( ! empty( $css ) ) {
echo '';
}
}
/**
* Clear stored promotions data
*/
public function clear_promotions() {
delete_option( $this->option_name );
delete_transient( $this->cache_transient );
$this->active_promotions = array();
}
/**
* Get current active promotions data
*
* @return array
*/
public function get_active_promotions() {
return $this->active_promotions;
}
/**
* Get promotion for a specific filter
*
* @param string $filter Filter hook name.
* @return array|false
*/
public function get_promotion_for_filter( $filter ) {
return isset( $this->active_promotions[ $filter ] ) ? $this->active_promotions[ $filter ] : false;
}
/**
* Check if there are any active promotions
*
* @return bool
*/
public function has_active_promotions() {
return ! empty( $this->active_promotions );
}
/**
* Manually set API URL
*
* @param string $url API URL.
*/
public function set_api_url( $url ) {
$this->api_url = $url;
}
/**
* Run initial promotions check on plugin activation
*/
public function activate() {
$this->fetch_remote_upsells();
}
/**
* Clean up on plugin deactivation
*/
public function deactivate() {
wp_clear_scheduled_hook( $this->cron_hook );
}
/**
* Get transients to be cleared on uninstall
*
* @param array $transients Existing transients array.
* @return array
*/
public function get_transients_to_clear( $transients ) {
$transients[] = $this->cache_transient;
return $transients;
}
}
// Initiate WPChill Upsells (remote promotions)
WPChill_Remote_Upsells::get_instance(
array(
'api_url' => 'https://wp-modula.com/wp-json/upsells/v1/get',
)
);
}