array( * 'text' - if not given, it uses the default text for output ) * 'orig_key' - original notice key * ) * * @var array */ public $notices = []; /** * All ignored notices * * @var array */ public $ignore = []; /** * All displayed notices ($notices minus $hidden) * * @var array */ public $displayed_notices = []; /** * Load default notices * * @var array */ public $default_notices = []; /** * The last notice key saved * * @var string */ public $last_saved_notice_key = false; /** * Name of the transient saved for daily checks in the backend * * @const string */ const DAILY_CHECK_TRANSIENT_NAME = 'advanced-ads-daily-ad-health-check-ran'; /** * Return an instance of this class. * * @return object A single instance of this class. */ public static function get_instance() { static $instance; // If the single instance hasn't been set, set it now. if ( null === $instance ) { $instance = new self(); } return $instance; } /** * Advanced_Ads_Ad_Health_Notices constructor. */ public function __construct() { // failsafe for there were some reports of 502 errors. if ( 1 < did_action( 'plugins_loaded' ) ) { return; } // stop here if notices are disabled. if ( ! self::notices_enabled() ) { return; } add_action( 'init', [ $this, 'load_default_notices' ] ); add_action( 'init', [ $this, 'load_notices' ] ); /** * Run checks * needs to run after plugins_loaded with priority 10 * current_screen seems like the perfect hook */ add_action( 'current_screen', [ $this, 'run_checks' ], 20 ); // add notification when an ad expires. add_action( 'advanced-ads-ad-expired', [ $this, 'ad_expired' ] ); } /** * Check if notices are enabled using "disable-notices" option in plugin settings * * @return bool */ public static function notices_enabled() { $options = Advanced_Ads::get_instance()->options(); return empty( $options['disable-notices'] ); } public function load_default_notices() { // load default notices. if ( [] === $this->default_notices ) { include ADVADS_ABSPATH . '/admin/includes/ad-health-notices.php'; $this->default_notices = $advanced_ads_ad_health_notices; } } /** * Load notice arrays */ public function load_notices() { $options = $this->options(); // load notices from "notices". $this->notices = $options['notices'] ?? []; /** * Cleanup notices */ foreach ( $this->notices as $_key => $_notice ) { // without valid key caused by an issue prior to 1.13.3. if ( empty( $_key ) ) { unset( $this->notices[ $_key ] ); } $time = current_time( 'timestamp', 0 ); $notice_array = $this->get_notice_array_for_key( $_key ); // handle notices with a timeout. if ( isset( $_notice['closed'] ) ) { // remove notice when timeout expired – was closed longer ago than timeout set in the notice options. if ( empty( $notice_array['timeout'] ) || ( ( $time - $_notice['closed'] ) > $notice_array['timeout'] ) ) { $this->remove( $_key ); } else { // just ignore notice if timeout is still valid. unset( $this->notices[ $_key ] ); } } // check if notice still exists. if ( [] === $this->get_notice_array_for_key( $_key ) ) { unset( $this->notices[ $_key ] ); } } // unignore notices if `show-hidden=true` is set in the URL. $nonce = Params::get( 'advads_nonce' ); if ( $nonce && wp_verify_nonce( wp_unslash( $nonce ), 'advanced-ads-show-hidden-notices' ) && true === Params::get( 'advads-show-hidden-notices', false, FILTER_VALIDATE_BOOLEAN ) ) { $this->unignore(); // remove the argument from the URL. add_filter( 'removable_query_args', [ $this, 'remove_query_vars_after_notice_update' ] ); } // load hidden notices. $this->ignore = $this->get_valid_ignored(); // get displayed notices // get keys of notices. $notice_keys = array_keys( $this->notices ); $this->displayed_notices = array_diff( $notice_keys, $this->ignore ); } /** * Remove query var from URL after notice was updated * * @param array $removable_query_args array with removable query vars. * @return array updated query vars. */ public function remove_query_vars_after_notice_update( $removable_query_args ) { $removable_query_args[] = 'advads-show-hidden-notices'; $removable_query_args[] = 'advads_nonce'; return $removable_query_args; } /** * Manage when to run checks * - only when users have ads * - once per day on any backend page * - on each Advanced Ads related page */ public function run_checks() { // run in WP Admin only and if there are any ads. if ( ! is_admin() || ! WordPress::get_count_ads() ) { return; } // don’t run on AJAX calls. if ( wp_doing_ajax() ) { return; } // run only daily unless we are on an Advanced Ads related page. if ( ! Conditional::is_screen_advanced_ads() && get_transient( self::DAILY_CHECK_TRANSIENT_NAME ) ) { return; } $this->checks(); } /** * General checks done on each Advanced Ads-related page or once per day */ public function checks() { $checks = [ 'old_php' => ! Advanced_Ads_Checks::php_version_minimum(), 'conflicting_plugins' => count( Advanced_Ads_Checks::conflicting_plugins() ), 'php_extensions_missing' => count( Advanced_Ads_Checks::php_extensions() ), 'ads_disabled' => Advanced_Ads_Checks::ads_disabled(), 'constants_enabled' => Advanced_Ads_Checks::get_defined_constants(), 'assets_expired' => Advanced_Ads_Checks::assets_expired(), 'license_invalid' => Advanced_Ads_Checks::licenses_invalid(), 'buddypress_no_pro' => class_exists( 'BuddyPress', false ) && ! defined( 'BP_PLATFORM_VERSION' ) && ! defined( 'AAP_VERSION' ), 'buddyboss_no_pro' => defined( 'BP_PLATFORM_VERSION' ) && ! defined( 'AAP_VERSION' ), 'gamipress_no_pro' => class_exists( 'GamiPress', false ) && ! defined( 'AAP_VERSION' ), 'pmp_no_pro' => defined( 'PMPRO_VERSION' ) && ! defined( 'AAP_VERSION' ), 'members_no_pro' => function_exists( 'members_plugin' ) && ! defined( 'AAP_VERSION' ), 'translatepress_no_pro' => function_exists( 'trp_enable_translatepress' ) && ! defined( 'AAP_VERSION' ), 'weglot_no_pro' => defined( 'WEGLOT_VERSION' ) && ! defined( 'AAP_VERSION' ), 'learndash' => defined( 'LEARNDASH_VERSION' ), 'aawp' => defined( 'AAWP_PLUGIN_FILE' ), 'polylang' => defined( 'POLYLANG_VERSION' ), 'mailpoet' => function_exists( 'mailpoet_check_requirements' ), 'wp_rocket' => Advanced_Ads_Checks::active_wp_rocket(), 'quiz_plugins_no_pro' => Advanced_Ads_Checks::active_quiz_plugins(), 'elementor' => defined( 'ELEMENTOR_VERSION' ), 'siteorigin' => defined( 'SITEORIGIN_PANELS_VERSION' ), 'divi_no_pro' => function_exists( 'et_setup_theme' ) || defined( 'ET_BUILDER_PLUGIN_VERSION' ), 'beaver_builder' => class_exists( 'FLBuilderLoader' ), 'pagelayer' => defined( 'PAGELAYER_FILE' ), 'wpb' => defined( 'WPB_VC_VERSION' ), 'newspaper' => defined( 'TAGDIV_ROOT' ), 'bbpress_no_pro' => class_exists( 'bbPress', false ) && ! defined( 'AAP_VERSION' ), 'WPML_active' => defined( 'ICL_SITEPRESS_VERSION' ), 'AMP_active' => Advanced_Ads_Checks::active_amp_plugin(), 'wpengine' => Advanced_Ads_Checks::wp_engine_hosting(),// do not remove 'ads_txt_plugins_enabled' => count( Advanced_Ads_Checks::ads_txt_plugins() ), 'header_footer_plugins_enabled' => count( Advanced_Ads_Checks::header_footer_plugins() ), ]; foreach ( $checks as $key => $check ) { if ( $check ) { $this->add( $key ); } elseif ( 'wpengine' !== $key ) { $this->remove( $key ); } } set_transient( self::DAILY_CHECK_TRANSIENT_NAME, true, DAY_IN_SECONDS ); } /** * Add a notice to the queue * * @param string $notice_key notice key to be added to the notice array. * @param array $atts additional attributes. * * attributes * - append_key string attached to the key; enables to create multiple messages for one original key * - append_text text added to the default message * - ad_id ID of an ad, attaches the link to the ad edit page to the message */ public function add( $notice_key, $atts = [] ) { // Early bail!! if ( empty( $notice_key ) || ! self::notices_enabled() ) { return; } // add string to key. if ( ! empty( $atts['append_key'] ) ) { $orig_notice_key = $notice_key; $notice_key .= $atts['append_key']; } $options = $this->options(); $notice_key = sanitize_key( $notice_key ); // load notices from "queue". $notices = $options['notices'] ?? []; // check if notice_key was already saved, this prevents the same notice from showing up in different forms. if ( isset( $notices[ $notice_key ] ) ) { return; } // save the new notice key. $notices[ $notice_key ] = []; // save text, if given. if ( ! empty( $atts['text'] ) ) { $notices[ $notice_key ]['text'] = $atts['text']; } // attach link to ad, if given. if ( ! empty( $atts['ad_id'] ) ) { $id = absint( $atts['ad_id'] ); $ad = wp_advads_get_ad( $id ); if ( $id && '' !== $ad->get_title() ) { $edit_link = ' ' . $ad->get_title() . ''; $notices[ $notice_key ]['append_text'] = isset( $notices[ $notice_key ]['append_text'] ) ? $notices[ $notice_key ]['append_text'] . $edit_link : $edit_link; } } // save the original key, if we manipulated it. if ( ! empty( $atts['append_key'] ) ) { $notices[ $notice_key ]['orig_key'] = $orig_notice_key; } // add more text. if ( ! empty( $atts['append_text'] ) ) { $notices[ $notice_key ]['append_text'] = esc_attr( $atts['append_text'] ); } // add current time – we store localized time including the offset set in WP. $notices[ $notice_key ]['time'] = current_time( 'timestamp', 0 ); $this->last_saved_notice_key = $notice_key; $this->update_notices( $notices ); } /** * Updating an existing notice or add it, if it doesn’t exist, yet * * @param string $notice_key notice key to be added to the notice array. * @param array $atts additional attributes. * * attributes: * - append_text – text added to the default message */ public function update( $notice_key, $atts = [] ) { // Early bail!! if ( empty( $notice_key ) || ! self::notices_enabled() ) { return; } // check if the notice already exists. $notice_key = esc_attr( $notice_key ); $options = $this->options(); // load notices from "queue". $notices = isset( $options['notices'] ) ? $options['notices'] : []; // check if notice_key was already saved, this prevents the same notice from showing up in different forms. if ( ! isset( $notices[ $notice_key ] ) ) { $this->add( $notice_key, $atts ); $notice_key = $this->last_saved_notice_key; // just in case, get notices again. $notices = $this->notices; } else { // add more text if this is an update. if ( ! empty( $atts['append_text'] ) ) { $notices[ $notice_key ]['append_text'] = isset( $notices[ $notice_key ]['append_text'] ) ? $notices[ $notice_key ]['append_text'] . $atts['append_text'] : $atts['append_text']; } // add `closed` marker, if given. if ( ! empty( $atts['closed'] ) ) { $notices[ $notice_key ]['closed'] = absint( $atts['closed'] ); } } // update db. $this->update_notices( $notices ); } /** * Decide based on the notice, whether to remove or ignore it * * @param string $notice_key key of the notice. */ public function hide( $notice_key ) { if ( empty( $notice_key ) ) { return; } // get original notice array for the "hide" attribute. $notice_array = $this->get_notice_array_for_key( $notice_key ); // handle notices with a timeout. // set `closed` timestamp if the notice definition has a timeout information. if ( isset( $notice_array['timeout'] ) ) { $this->update( $notice_key, [ 'closed' => current_time( 'timestamp', 0 ) ] ); return; } if ( isset( $notice_array['hide'] ) && false === $notice_array['hide'] ) { // remove item. $this->remove( $notice_key ); } else { // hide item. $this->ignore( $notice_key ); } } /** * Remove notice * Would remove it from "notice" array. The notice can be added anytime again * practically, this allows users to "skip" an notice if they are sure that it was only temporary * * @param string $notice_key notice key to be removed. */ public function remove( $notice_key ) { // Early bail!! if ( empty( $notice_key ) || ! self::notices_enabled() ) { return; } $options = $this->options(); if ( ! isset( $options['notices'] ) || ! is_array( $options['notices'] ) || ! isset( $options['notices'][ $notice_key ] ) ) { return; } unset( $options['notices'][ $notice_key ] ); $this->update_notices( $options['notices'] ); } /** * Ignore any notice * adds notice key into "ignore" array * does not remove it from "notices" array * * @param string $notice_key key of the notice to be ignored. */ public function ignore( $notice_key ) { // Early bail!! if ( empty( $notice_key ) || ! self::notices_enabled() ) { return; } $options = $this->options(); $ignored = isset( $options['ignore'] ) && is_array( $options['ignore'] ) ? $options['ignore'] : []; // adds notice key to ignore array if it doesn’t exist already. if ( false === array_search( $notice_key, $ignored, true ) ) { $ignored[] = $notice_key; } // update db. $this->update_ignore( $ignored ); } /** * Clear all "ignore" messages */ public function unignore() { $this->update_ignore(); } /** * Update ignored notices if there is any change * * @param string[] $ignore_list list of ignored keys. * * @return void */ public function update_ignore( $ignore_list = [] ) { $options = $this->options(); $before = Arr::get( $options, 'ignore', [] ); if ( $ignore_list === $before ) { return; } $options['ignore'] = $ignore_list; $this->update_options( $options ); } /** * Update notices list if there is any change * * @param array $notices New options. * * @return void */ public function update_notices( $notices ): void { $options = $this->options(); if ( Arr::get( $options, 'notices', [] ) === $notices ) { return; } $options['notices'] = $notices; $this->update_options( $options ); $this->load_notices(); } /** * Render notice widget on overview page */ public function render_widget() { $ignored_count = count( $this->ignore ); include ADVADS_ABSPATH . 'views/admin/widgets/aa-dashboard/overview-notices.php'; } /** * Display notices in a list * * @param string $type which type of notice to show; default: 'problem'. * * @return void */ public function display( $type = 'problem' ) { // Early baill!! if ( ! is_array( $this->notices ) ) { return; } foreach ( $this->notices as $_notice_key => $_notice ) { $notice_array = $this->get_notice_array_for_key( $_notice_key ); // remove the notice if key doesn’t exist anymore. if ( [] === $notice_array ) { $this->remove( $_notice_key ); } $notice_type = isset( $notice_array['type'] ) ? $notice_array['type'] : 'problem'; // skip if type is not correct. if ( $notice_type !== $type ) { continue; } if ( ! empty( $_notice['text'] ) ) { $text = $_notice['text']; } elseif ( isset( $notice_array['text'] ) ) { $text = $notice_array['text']; } else { continue; } // attach "append_text". if ( ! empty( $_notice['append_text'] ) ) { $text .= $_notice['append_text']; } // attach "get help" link. if ( ! empty( $_notice['get_help_link'] ) ) { $text .= $this->get_help_link( $_notice['get_help_link'] ); } elseif ( isset( $notice_array['get_help_link'] ) ) { $text .= $this->get_help_link( $notice_array['get_help_link'] ); } $can_hide = ( ! isset( $notice_array['can_hide'] ) || true === $notice_array['can_hide'] ) ? true : false; $hide = ( ! isset( $notice_array['hide'] ) || true === $notice_array['hide'] ) ? true : false; $is_hidden = in_array( $_notice_key, $this->ignore, true ) ? true : false; $date = isset( $_notice['time'] ) ? date_i18n( get_option( 'date_format' ), $_notice['time'] ) : false; $dashicon = 'dashicons-warning'; if ( 'notice' === $type ) { $dashicon = 'dashicons-info'; } elseif ( 'pitch' === $type ) { $dashicon = 'dashicons-lightbulb'; } include ADVADS_ABSPATH . '/admin/views/overview-notice-row.php'; } } /** * Display plugins and themes pitches * * @return void */ public function display_pitches() { $this->display( 'pitch' ); } /** * Display problems. */ public function display_problems() { $this->display( 'problem' ); } /** * Display notices. */ public function display_notices() { $this->display( 'notice' ); } /** * Return notices option from DB * * @return array $options */ public function options() { if ( ! isset( $this->options ) ) { $this->options = get_option( ADVADS_SLUG . '-ad-health-notices', [] ); } if ( ! is_array( $this->options ) ) { $this->options = []; } return $this->options; } /** * Update notice options * * @param array $options new options. */ public function update_options( array $options ) { // do not allow to clear options. if ( [] === $options ) { return; } $this->options = $options; update_option( ADVADS_SLUG . '-ad-health-notices', $options ); } /** * Get the number of overall visible notices */ public static function get_number_of_notices() { $displayed_notices = self::get_instance()->displayed_notices; if ( ! is_array( $displayed_notices ) ) { return 0; } return count( $displayed_notices ); } /** * Get ignored messages that are also in the notices * also updates ignored array, if needed */ public function get_valid_ignored() { $options = $this->options(); $ignore_before = $options['ignore'] ?? []; // get keys from notices. $notice_keys = array_keys( $this->notices ); // get the errors that are in ignore AND notices and reset the keys. $ignore = array_values( array_intersect( $ignore_before, $notice_keys ) ); // only update if changed. if ( $ignore !== $ignore_before ) { $this->update_ignore( $ignore ); } return $ignore; } /** * Check if there are visible problems (notices of type "problem") * * @return bool true if there are visible notices (notices that are not hidden) */ public static function has_visible_problems() { $displayed_notices = self::get_instance()->displayed_notices; if ( ! is_array( $displayed_notices ) ) { return false; } return 0 < count( $displayed_notices ); } /** * Get visible notices by type – hidden and displayed * * @param string $type type of the notice. * * @return array */ public function get_visible_notices_by_type( $type = 'problem' ) { $notices_by_type = []; foreach ( $this->notices as $_key => $_notice ) { $notice_array = $this->get_notice_array_for_key( $_key ); if ( isset( $notice_array['type'] ) && $type === $notice_array['type'] && ( ! isset( $this->ignore ) || false === array_search( $_key, $this->ignore, true ) ) ) { $notices_by_type[ $_key ] = $_notice; } } return $notices_by_type; } /** * Check if there are notices * * @return bool true if there are notices, false if not */ public function has_notices() { return isset( $this->notices ) && is_array( $this->notices ) && count( $this->notices ); } /** * Check if there are visible notices for a given type * * @param string $type type of the notice. * * @return integer */ public function has_notices_by_type( $type = 'problem' ) { $notices = $this->get_visible_notices_by_type( $type ); if ( ! is_array( $notices ) ) { return 0; } return count( $notices ); } /** * Get the notice array for a notice key * useful, if a notice key was manipulated * * @param string $notice_key key of the notice. * * @return array type */ public function get_notice_array_for_key( $notice_key ) { // check if there is an original key. $orig_key = isset( $this->notices[ $notice_key ]['orig_key'] ) ? $this->notices[ $notice_key ]['orig_key'] : $notice_key; return isset( $this->default_notices[ $orig_key ] ) ? $this->default_notices[ $orig_key ] : []; } /** * Add notification when an ad expires based on the expiry date * * @param integer $ad_id ID of the ad. * * @return void */ public function ad_expired( $ad_id ): void { $id = ! empty( $ad_id ) ? absint( $ad_id ) : 0; $this->update( 'ad_expired', [ 'append_key' => $id, 'ad_id' => $id, ] ); } /** * Get AdSense error link * this is a copy of Advanced_Ads_AdSense_MAPI::get_adsense_error_link() which might not be available all the time * * @param string $code error code. * * @return string link */ public static function get_adsense_error_link( $code ) { if ( ! empty( $code ) ) { $code = '-' . $code; } if ( class_exists( 'Advanced_Ads_AdSense_MAPI', false ) ) { return Advanced_Ads_AdSense_MAPI::get_adsense_error_link( 'disapprovedAccount' ); } // is a copy of Advanced_Ads_AdSense_MAPI::get_adsense_error_link(). return sprintf( /* translators: %1$s is an anchor (link) opening tag, %2$s is the closing tag. */ esc_attr__( 'Learn more about AdSense account issues %1$shere%2$s.', 'advanced-ads' ), '', '' ); } /** * Return a "Get Help" link * * @param string $link target URL. * * @return string HTML of the target link */ public function get_help_link( $link ) { $link = esc_url( $link ); if ( ! $link ) { return ''; } return ' ' . __( 'Get help', 'advanced.ads' ) . ''; } }