client = &$client; $this->cache_ttl = 12 * HOUR_IN_SECONDS; } public function set_source( ?string $source = null ): SE_License_SDK_Promotions { if ( $source ) { $this->promo_source = esc_url_raw( $source, [ 'https' ] ); } return $this; } public function set_cache_ttl( ?int $seconds = null ): SE_License_SDK_Promotions { if ( $seconds && $seconds >= 12 * HOUR_IN_SECONDS ) { $this->cache_ttl = $seconds; } return $this; } /** * Init Promotions * @return void */ public function init() { add_action( 'admin_init', [ $this, 'init_internal' ] ); add_action( $this->get_refresh_hook(), [ $this, 'refresh_promos' ] ); } /** * Cron hook that refreshes this product's promotions. * * @return string */ protected function get_refresh_hook(): string { return $this->client->getHookName( 'refresh_promos' ); } /** * Set environment variables and init internal hooks * @return void */ public function init_internal() { if ( null === $this->promotions ) { $this->promotions = $this->get_promos(); } // only run if there is active promotions. if ( count( $this->promotions ) ) { add_action( 'admin_notices', [ $this, 'render_promo_notices' ] ); add_action( 'admin_print_styles', [ $this, 'print_promo_notice_styles' ], 99 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_promo_notice_scripts' ] ); add_action( 'admin_print_footer_scripts', [ $this, 'print_promo_notice_scripts' ] ); add_action( 'wp_ajax_se_dismiss_promo', [ $this, 'handle_promo_dismiss_request' ] ); } } /** * Render Promotions * @return void */ public function render_promo_notices() { if ( ! current_user_can( 'manage_options' ) ) { return; } foreach ( $this->promotions as $promotion ) { if ( empty( $promotion['content'] ) ) { continue; } $wrapperStyles = '--se-sdk-primary-color:' . esc_attr( $this->client->getPrimaryColor() ) . ';'; $buttonStyles = ''; $button = $promotion['button'] ?? null; $logo = $promotion['logo'] ?? null; $has_columns = $button && $logo; if ( ! empty( $promotion['wrapperStyle'] ) ) { if ( ! empty( $promotion['wrapperStyle']['color'] ) ) { $wrapperStyles .= 'color:' . $promotion['wrapperStyle']['color'] . ';'; } if ( ! empty( $promotion['wrapperStyle']['padding'] ) ) { $wrapperStyles .= 'padding:' . $promotion['wrapperStyle']['padding'] . ';'; } if ( ! empty( $promotion['wrapperStyle']['backgroundColor'] ) ) { $wrapperStyles .= 'background-color:' . $promotion['wrapperStyle']['backgroundColor'] . ';'; } if ( ! empty( $promotion['wrapperStyle']['backgroundImage'] ) ) { $wrapperStyles .= 'background-image: url("' . $promotion['wrapperStyle']['backgroundImage'] . '");'; } if ( ! empty( $promotion['wrapperStyle']['backgroundRepeat'] ) ) { $wrapperStyles .= 'background-repeat:' . $promotion['wrapperStyle']['backgroundRepeat'] . ';'; } if ( ! empty( $promotion['wrapperStyle']['backgroundSize'] ) ) { $wrapperStyles .= 'background-size:' . $promotion['wrapperStyle']['backgroundSize'] . ';'; } } if ( $button ) { if ( ! empty( $button['backgroundColor'] ) ) { $buttonStyles .= 'background-color:' . $button['backgroundColor'] . ';'; $buttonStyles .= 'border-color:' . $button['backgroundColor'] . ';'; } if ( ! empty( $button['color'] ) ) { $buttonStyles .= 'color:' . $button['color'] . ';'; } } ?>
hidden_promotions ) { $temp_hidden = get_transient( $this->client->getHookName( 'hidden_promos' ) ); $user_hidden = get_user_option( $this->client->getHookName( 'hidden_promos' ), get_current_user_id() ); $temp_hidden = ! is_array( $temp_hidden ) ? [] : $temp_hidden; $user_hidden = ! is_array( $user_hidden ) ? [] : $user_hidden; // Combine hidden for current user. $this->hidden_promotions = array_unique( array_filter( array_merge( $temp_hidden, $user_hidden ) ) ); } return $this->hidden_promotions; } /** * Get Promotion Data * Cache First then fetch source url for json data source. * * @return array[] */ public function get_promos(): array { $this->get_hidden_promos(); $promos = get_transient( $this->client->getHookName( 'cached_promos' ) ); if ( false === $promos || ! is_array( $promos ) ) { // Never fetch on a page load (this runs on admin_init, i.e. on every // admin request including admin-ajax). Refresh in the background and // show nothing until then. if ( ! class_exists( 'SE_License_SDK_Update_Batch' ) ) { require_once __DIR__ . '/SE_License_SDK_Update_Batch.php'; } if ( ! SE_License_SDK_Update_Batch::can_run_inline() ) { if ( ! wp_installing() && ! wp_next_scheduled( $this->get_refresh_hook() ) ) { wp_schedule_single_event( time(), $this->get_refresh_hook() ); } return []; } $promos = $this->refresh_promos(); } return array_filter( $promos, [ $this, 'is_promo_visible' ] ); } /** * Fetch promotions from the source and cache them (an empty list on * failure, so a broken source is asked at most once per cache window). * * @return array */ public function refresh_promos(): array { // Fetch promotions data from JSON source. $args = [ 'timeout' => 15, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout 'method' => 'GET', ]; if ( $this->promo_source ) { $args['url'] = $this->promo_source; } else { $args['route'] = 'promotions'; } $response = $this->client->request( $args ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { // Cache Something, reduce request. $promos = '[]'; } else { $promos = wp_remote_retrieve_body( $response ); } // Decode to array. $promos = json_decode( $promos, true ); // @NOTE: Happens when firewall respond with html. if ( ! $promos || json_last_error() !== JSON_ERROR_NONE ) { $promos = []; } // Filter promotions by date. $promos = array_filter( $promos, [ $this, 'is_promo_active' ] ); // Cache data. set_transient( $this->client->getHookName( 'cached_promos' ), $promos, $this->cache_ttl ); return $promos; } /** * Check if promotion is active by date. * must have start and end property * * @param array|string $promo { Promo items. * Single Promo item * @type string $content string. Promo Content. * @type string $start string. Datetime string (Y-m-d H:i:s). * @type string $end string. Datetime string (Y-m-d H:i:s). * } * * @return bool */ protected function is_promo_active( $promo ): bool { // @NOTE: Occasionally the server returns non-JSON data or an HTML error page with a 200 status. // This often happens if a firewall blocks the request and the SDK User-Agent is not whitelisted. if ( ! is_array( $promo ) ) { return false; } if ( ! empty( $promo['error'] ) ) { return false; } // Valid promo item must have hash, content, start & end date-time. if ( empty( $promo['hash'] ) || empty( $promo['content'] ) || empty( $promo['start'] ) || empty( $promo['end'] ) ) { return false; } $now = time(); return strtotime( $promo['start'] ) <= $now && $now <= strtotime( $promo['end'] ); } protected function is_promo_visible( array $promo ): bool { return ! in_array( $promo['hash'], $this->hidden_promotions, true ) && $this->is_promo_active( $promo ); } /** * Js Dependencies * @return void */ public function enqueue_promo_notice_scripts() { wp_enqueue_script( 'wp-util' ); wp_enqueue_script( 'jquery' ); } /** * Script for hiding promo on user click * @return void */ public function print_promo_notice_scripts() { ?> hidden_promotions[] = $promo; // Temporarily hide for all admin/users for 7 days. set_transient( $this->client->getHookName( 'hidden_promos' ), array_unique( array_filter( $this->hidden_promotions ) ), 7 * DAY_IN_SECONDS ); // Permanently hide for current admin/user. update_user_option( get_current_user_id(), $this->client->getHookName( 'hidden_promos' ), array_unique( array_filter( $this->hidden_promotions ) ) ); wp_send_json_success(); } /** * Clear Hidden Promotion preference for User * @return bool */ public function clear_hidden_promos(): bool { if ( ! did_action( 'admin_init' ) ) { _doing_it_wrong( __METHOD__, esc_html__( 'Method must be invoked inside admin_init action.', 'storeengine-sdk' ), '1.0.0' ); } // @NOTE Only delete the transient for hidden-promos, keep the user-option. // So current admin doesn't get irritated but other admin can see the promos. return delete_transient( $this->client->getHookName( 'hidden_promos' ) ); } /** * Clear Cached Promotion data * @return bool */ public function clear_promo_cache(): bool { return delete_transient( $this->client->getHookName( 'cached_promos' ) ); } final public function __clone() { trigger_error( 'Singleton. No cloning allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } /** * Wakeup. */ final public function __wakeup() { trigger_error( 'Singleton. No serialization allowed!', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } } // End of file SE_License_SDK_Promotions.php.