setup_hooks(); return $instance; } /** * Constructor * * @param string $product Product identifier. * @param array $config Configuration options. */ private function __construct( $product, $config ) { $this->product = $product; $this->api_url = isset( $config['api_url'] ) ? esc_url_raw( $config['api_url'] ) : ''; $this->schedule = isset( $config['schedule'] ) ? $config['schedule'] : 'daily'; $this->capability = isset( $config['capability'] ) ? $config['capability'] : 'manage_options'; $this->dismiss_duration = isset( $config['dismiss_duration'] ) ? absint( $config['dismiss_duration'] ) : WEEK_IN_SECONDS; // Define allowed HTML tags for notices $this->allowed_html = array( 'div' => array( 'class' => array(), 'style' => array(), 'id' => array() ), 'p' => array( 'class' => array(), 'style' => array() ), 'span' => array( 'class' => array(), 'style' => array() ), 'strong' => array( 'class' => array() ), 'em' => array( 'class' => array() ), 'b' => array(), 'i' => array(), 'a' => array( 'href' => array(), 'title' => array(), 'target' => array(), 'class' => array(), 'rel' => array() ), 'br' => array(), 'h1' => array( 'class' => array() ), 'h2' => array( 'class' => array() ), 'h3' => array( 'class' => array() ), 'h4' => array( 'class' => array() ), 'ul' => array( 'class' => array() ), 'ol' => array( 'class' => array() ), 'li' => array( 'class' => array() ), 'img' => array( 'src' => array(), 'alt' => array(), 'class' => array(), 'width' => array(), 'height' => array() ), ); } /** * Setup WordPress hooks */ private function setup_hooks() { // Cron. add_action( 'admin_init', array( $this, 'init_cron' ) ); add_action( 'rnc_fetch_content_' . $this->product, array( $this, 'fetch_content' ) ); // Display notices. add_action( 'admin_notices', array( $this, 'display_notices' ) ); // AJAX handlers. add_action( 'wp_ajax_rnc_dismiss_content_' . $this->product, array( $this, 'ajax_dismiss_content' ) ); } /** * Initialize cron schedule */ public function init_cron() { $hook = 'rnc_fetch_content_' . $this->product; if ( ! wp_next_scheduled( $hook ) ) { wp_schedule_event( time(), $this->schedule, $hook ); } } /** * Fetch content from remote API */ public function fetch_content() { if ( empty( $this->api_url ) ) { return; } $response = wp_remote_get( $this->api_url, array( 'timeout' => 15, 'sslverify' => true, ) ); if ( is_wp_error( $response ) ) { $this->log( 'API Error: ' . $response->get_error_message(), 'error' ); return; } $http_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $http_code ) { $this->log( 'API returned HTTP ' . $http_code, 'error' ); return; } $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( ! is_array( $data ) || ! isset( $data['success'] ) || ! $data['success'] ) { $this->log( 'Invalid API response', 'error' ); return; } $contents = isset( $data['contents'] ) ? $data['contents'] : array(); if ( empty( $contents ) ) { $this->clear_contents(); $this->log( 'No contents - cleared stored notices' ); return; } $this->store_contents( $contents ); $this->log( 'Fetched ' . count( $contents ) . ' contents' ); } /** * Store contents in database * * @param array $contents Contents array. */ private function store_contents( $contents ) { $key = $this->get_option_key( 'contents' ); $old_contents = get_option( $key, array() ); $old_ids = wp_list_pluck( $old_contents, 'id' ); $new_ids = wp_list_pluck( $contents, 'id' ); // Clean up stale dismiss keys. $removed_ids = array_diff( $old_ids, $new_ids ); foreach ( $removed_ids as $removed_id ) { delete_option( $this->get_option_key( 'dismissed_' . $removed_id ) ); } update_option( $key, $contents, false ); update_option( $this->get_option_key( 'fetched_time' ), current_time( 'mysql' ), false ); } /** * Clear all stored contents */ private function clear_contents() { $contents = get_option( $this->get_option_key( 'contents' ), array() ); // Clean up all dismiss keys. if ( is_array( $contents ) ) { foreach ( $contents as $content ) { if ( isset( $content['id'] ) ) { delete_option( $this->get_option_key( 'dismissed_' . $content['id'] ) ); } } } delete_option( $this->get_option_key( 'contents' ) ); delete_option( $this->get_option_key( 'fetched_time' ) ); delete_option( $this->get_option_key( 'temp_dismissed' ) ); } /** * Get non-dismissed contents * * @return array */ private function get_non_dismissed_contents() { $contents = get_option( $this->get_option_key( 'contents' ), array() ); $result = array(); if ( ! is_array( $contents ) ) { return $result; } foreach ( $contents as $content ) { if ( ! isset( $content['id'] ) ) { continue; } $dismissed = get_option( $this->get_option_key( 'dismissed_' . $content['id'] ), false ); if ( ! $dismissed ) { $result[] = $content; } } return $result; } /** * Check if temporarily dismissed * * @return bool */ private function is_temporarily_dismissed() { $dismiss_time = get_option( $this->get_option_key( 'temp_dismissed' ), 0 ); if ( empty( $dismiss_time ) ) { return false; } return ( current_time( 'timestamp' ) - intval( $dismiss_time ) ) < $this->dismiss_duration; } /** * Display admin notices */ public function display_notices() { if ( ! current_user_can( $this->capability ) ) { return; } if ( $this->is_temporarily_dismissed() ) { return; } $contents = $this->get_non_dismissed_contents(); if ( empty( $contents ) ) { return; } foreach ( $contents as $content ) { if ( ! isset( $content['id'] ) || ! isset( $content['content'] ) ) { continue; } $content_id = sanitize_key( $content['id'] ); $html_content = wp_kses( $content['content'], $this->allowed_html ); $nonce = wp_create_nonce( 'rnc_dismiss_' . $this->product . '_' . $content_id ); $ajax_action = 'rnc_dismiss_content_' . $this->product; ?>