* @license GPL-2.0+ * @link https://wpadvancedads.com * @copyright 2013-2018 Thomas Maier, Advanced Ads GmbH */ use AdvancedAds\Constants; use AdvancedAds\Abstracts\Ad; use AdvancedAds\Abstracts\Group; use AdvancedAds\Framework\Utilities\Params; use AdvancedAds\Utilities\WordPress; use AdvancedAds\Utilities\Conditional; use AdvancedAds\Installation\Capabilities; /** * Plugin class. This class should ideally be used to work with the * public-facing side of the WordPress site. * * @package Advanced_Ads * @author Thomas Maier */ class Advanced_Ads { /** * Instance of this class. * * @var object */ private static $instance = null; /** * Ad types * * @deprecated 1.48.2 * * @var array Ad types */ public $ad_types = []; /** * Plugin options * * @var array $options */ protected $options; /** * Interal plugin options – set by the plugin * * @var array $internal_options */ protected $internal_options; /** * Adblocker Plugin options * * @var array (if loaded) */ protected $adblocker_options = null; /** * Whether the loop started in an inner `the_content`. * * @var bool */ protected $was_in_the_loop = false; /** * Signifies Whether the loop has started and the caller is in the loop. * * We need it because Some the "Divi" theme calls the `loop_start/loop_end` hooks * instead of calling `the_post()` to signify that the caller is in the loop. * * @var bool */ protected $in_the_loop = false; /** * Is the query the main query?, when WP_Query is used * * @var bool */ private $is_main_query; /** * Save number of ads * * @var array */ private $number_of_ads = []; /** * Initialize frontend features */ private function __construct() { add_action( 'plugins_loaded', [ $this, 'wp_plugins_loaded' ] ); // allow add-ons to interact. add_action( 'init', [ $this, 'advanced_ads_loaded' ], 9 ); add_filter( 'the_content', [ $this, 'set_was_in_the_loop' ], ~PHP_INT_MAX ); } /** * Return an instance of this class. * * @return Advanced_Ads A single instance of this class. */ public static function get_instance() { // if the single instance hasn't been set, set it now. if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Initialize the plugin by setting localization and loading public scripts * and styles. */ public function wp_plugins_loaded() { // register hooks and filters for auto ad injection. $this->init_injection(); // add meta robots noindex, nofollow to images, which are part of 'Image ad' ad type. add_action( 'wp_head', [ $this, 'noindex_attachment_images' ] ); // use custom CSS or other custom header code. add_action( 'wp_head', [ $this, 'custom_header_code' ] ); // check if ads are disabled in secondary queries. add_action( 'the_post', [ $this, 'set_query_type' ], 10, 2 ); add_action( 'loop_start', [ $this, 'set_loop_start' ], 10, 0 ); add_action( 'loop_end', [ $this, 'set_loop_end' ], 10, 0 ); add_action( 'transition_post_status', [ $this, 'transition_ad_status' ], 10, 3 ); // register debug parameter $this->debug_parameter(); Advanced_Ads_Display_Conditions::get_instance(); ( new Advanced_Ads_Frontend_Checks() ); Advanced_Ads_Ad_Health_Notices::get_instance(); } /** * Allow add-ons to hook */ public function advanced_ads_loaded() { do_action( 'advanced-ads-plugin-loaded' ); } /** * Load filters to inject ads into various sections of our site */ public function init_injection() { add_filter( 'the_content', [ $this, 'inject_content' ], $this->get_content_injection_priority() ); } /** * Log error messages when debug is enabled * * @param string $message error message. * @link http://www.smashingmagazine.com/2011/03/08/ten-things-every-wordpress-plugin-developer-should-know/ */ public static function log( $message ) { if ( true === WP_DEBUG ) { if ( is_array( $message ) || is_object( $message ) ) { error_log( __( 'Advanced Ads Error following:', 'advanced-ads' ) ); error_log( print_r( $message, true ) ); } else { /* translators: %s is an error message generated by the plugin. */ $message = sprintf( __( 'Advanced Ads Error: %s', 'advanced-ads' ), $message ); error_log( $message ); } } } /** * Compat method * * @return array with plugin options */ public function options() { // we can’t store options if WPML String Translations is enabled, or it would not translate the "Ad Label" option. if ( ! isset( $this->options ) || class_exists( 'WPML_ST_String' ) ) { $this->options = get_option( ADVADS_SLUG, [] ); } // allow to change options dynamically $this->options = apply_filters( 'advanced-ads-options', $this->options ); return $this->options; } /** * Compat method * * @return array with adblocker options */ public function get_adblocker_options() { // we can’t store options if WPML String Translations is enabled, or it would not translate the "Ad Label" option. if ( ! isset( $this->adblocker_options ) || class_exists( 'WPML_ST_String' ) ) { $this->adblocker_options = wp_parse_args( get_option( ADVADS_SETTINGS_ADBLOCKER, [] ), [ 'method' => false, ] ); } return $this->adblocker_options; } /** * Compat method * * @return array with internal plugin options */ public function internal_options() { if ( ! isset( $this->internal_options ) ) { $defaults = [ 'version' => ADVADS_VERSION, 'installed' => time(), // when was this installed. ]; $this->internal_options = get_option( ADVADS_SLUG . '-internal', [] ); // save defaults. if ( [] === $this->internal_options ) { $this->internal_options = $defaults; $this->update_internal_options( $this->internal_options ); ( new Capabilities() )->create_capabilities(); } // for versions installed prior to 1.5.3 set installed date for now. if ( ! isset( $this->internal_options['installed'] ) ) { $this->internal_options['installed'] = time(); $this->update_internal_options( $this->internal_options ); } } return $this->internal_options; } /** * Injected ad into content (before and after) * Displays ALL ads * * @param string $content post content. * * @return string */ public function inject_content( $content = '' ) { $options = $this->options(); // do not inject in content when on a BuddyPress profile upload page (avatar & cover image). if ( ( function_exists( 'bp_is_user_change_avatar' ) && \bp_is_user_change_avatar() ) || ( function_exists( 'bp_is_user_change_cover_image' ) && bp_is_user_change_cover_image() ) ) { return $content; } // do not inject ads multiple times, e.g., when the_content is applied multiple times. if ( $this->has_many_the_content() ) { return $content; } // Check if ads are disabled in secondary queries. if ( ! empty( $options['disabled-ads']['secondary'] ) ) { // this function was called by ajax (in secondary query). if ( wp_doing_ajax() ) { return $content; } // get out of wp_router_page post type if ads are disabled in secondary queries. if ( 'wp_router_page' === get_post_type() ) { return $content; } } // No need to inject ads because all tags are stripped from excepts. if ( doing_filter( 'get_the_excerpt' ) ) { return $content; } // make sure that no ad is injected into another ad. if ( get_post_type() === Constants::POST_TYPE_AD ) { return $content; } // Do not inject on admin pages. if ( is_admin() && ! wp_doing_ajax() ) { return $content; } // Do not inject in writing REST requests. if ( Conditional::is_gutenberg_writing_request() && Conditional::is_rest_request() ) { return $content; } if ( ! $this->can_inject_into_content() ) { return $content; } $placements = wp_advads_get_all_placements(); if ( ! apply_filters( 'advanced-ads-can-inject-into-content', true, $content, $placements ) ) { return $content; } foreach ( $placements as $placement_id => $placement ) { $item = $placement->get_item(); $type_object_id = $placement->get_type_object()->get_id(); if ( empty( $item ) || 'default' === $type_object_id || ! apply_filters( 'advanced-ads-can-inject-into-content-' . $placement_id, true, $content, $placement ) ) { continue; } $placement_options = $placement->get_data(); switch ( $type_object_id ) { case 'post_top': $placement_content = get_the_placement( $placement_id, '', $placement_options ); $content = $placement_content . $content; break; case 'post_bottom': $content .= get_the_placement( $placement_id, '', $placement_options ); break; case 'post_content': $content = Advanced_Ads_In_Content_Injector::inject_in_content( $placement_id, $placement_options, $content ); break; } } if ( ! empty( Params::cookie( 'advads_frontend_picker' ) ) ) { // Make possible to know where the content starts and ends. $content = ' ' . $content; } return $content; } /** * Whether injection using `the_content` is allowed * * @return bool */ private function can_inject_into_content() { global $wp_query; if ( is_feed() || Conditional::is_rest_request() ) { return true; } $options = $this->options(); /** * Allows experienced user to control up to the nth post ads will be injected via post content placement in a post list. * * Post displaying an excerpt instead of the full content will still be skipped (mainly controlled by the active theme). * * @param int|string $archive_injection_count the string "true" when ads is injected into all posts. */ $archive_injection_count = apply_filters( 'advanced-ads-content-injection-index', $options['content-injection-everywhere'] ?? 1 ); // Run only within the loop on single pages of public post types. $public_post_types = get_post_types( [ 'public' => true, 'publicly_queryable' => true, ], 'names', 'or' ); // Check if admin allows injection in all places. $injection_enabled = $options['content-injection-enabled'] ?? 'off'; if ( ( $injection_enabled === 'off' || 0 === $archive_injection_count ) && ( ! is_singular( $public_post_types ) || ( ! Conditional::is_amp() && ! $this->in_the_loop() && ! $this->was_in_the_loop ) ) ) { return false; } if ( is_main_query() && ! empty( $options ) && 'true' !== $archive_injection_count && isset( $wp_query->current_post ) && $wp_query->current_post >= $archive_injection_count ) { return false; } return true; } /** * General check if ads can be displayed for the whole page impression * * @return bool true, if ads can be displayed. * @todo move this to set_disabled_constant(). */ public function can_display_ads() { $options = $this->options(); /** * Check if ads are disabled on the currently displayed page. Allow user to define their own rules if needed. * * @param bool $ads_disabled whether ads are disabled by the plugin options. * @param array $options plugin options. * * @return bool `true` if ads are disabled */ $ads_disabled = apply_filters( 'advanced-ads-disabled-ads', Conditional::is_ad_disabled(), $options ); if ( $ads_disabled ) { return false; } // check if ads are disabled in secondary queries. // and this is not main query and this is not ajax (because main query does not exist in ajax but ad needs to be shown). if ( ! empty( $options['disabled-ads']['secondary'] ) && ! $this->is_main_query() && ! wp_doing_ajax() ) { return false; } return true; } /** * Add meta robots noindex, nofollow to images, which are part of 'Image ad' ad type */ public function noindex_attachment_images() { global $post; if ( is_attachment() && is_object( $post ) && isset( $post->post_parent ) ) { $post_parent = get_post( $post->post_parent ); $parent_is_ad = $post_parent && Constants::POST_TYPE_AD === $post_parent->post_type; // if the image was not attached to any post and if at least one image ad contains the image. Needed for backward compatibility. $parent_is_image_ad = ( empty( $post->post_parent ) && 0 < get_post_meta( get_the_ID(), '_advanced-ads_parent_id', true ) ); if ( $parent_is_ad || $parent_is_image_ad ) { echo ''; } } } /** * Show custom CSS in the header */ public function custom_header_code(){ if ( ! defined( 'ADVANCED_ADS_DISABLE_EDIT_BAR' ) && Conditional::user_can( 'advanced_ads_edit_ads' ) ) { ?> is_main_query=true" while main query is being executed * * @param WP_Post $post The Post object (passed by reference). * @param WP_Query $query The current Query object (passed by reference). */ public function set_query_type( $post, $query = null ) { if ( $query instanceof WP_Query ) { $this->is_main_query = $query->is_main_query(); } } /** * Check if main query is being executed * * @return bool true while main query is being executed or not in the loop, false otherwise */ public function is_main_query() { if ( ! $this->in_the_loop() ) { // the secondary query check only designed for within post content. return true; } return true === $this->is_main_query; } /** * Sets whether the loop has started. */ public function set_loop_start() { $this->in_the_loop = true; } /** * Sets whether the loop has ended. */ public function set_loop_end() { $this->in_the_loop = false; } /** * Whether the loop has started and the caller is in the loop. * * @return bool */ public function in_the_loop() { if ( in_the_loop() ) { return true; } if ( $this->in_the_loop ) { return true; } } /** * Find the calls to `the_content` inside functions hooked to `the_content`. * * @return bool */ public function has_many_the_content() { global $wp_current_filter; if ( count( array_keys( $wp_current_filter, 'the_content', true ) ) > 1 ) { // More then one `the_content` in the stack. return true; } return false; } /** * Get an "Advertisement" label to use before single ad or before first ad in a group * * @param Ad|Group $item Ad or group. * @param string $placement_state default/enabled/disabled. * * @return string label, empty string if label should not be displayed. */ public function get_label( $item, $placement_state = 'default' ) { if ( 'disabled' === $placement_state ) { return ''; } $advads_options = self::get_instance()->options(); if ( 'enabled' !== $placement_state && empty( $advads_options['custom-label']['enabled'] ) ) { return ''; } $label = $advads_options['custom-label']['text'] ?? _x( 'Advertisements', 'label above ads', 'advanced-ads' ); $allowed_tags = [ 'a' => [ 'href' => true, 'title' => true, ], 'b' => [], 'blockquote' => [ 'cite' => true, ], 'cite' => [], 'code' => [], 'del' => [ 'datetime' => true, ], 'em' => [], 'i' => [], 'q' => [ 'cite' => true, ], 's' => [], 'span' => [ 'style' => true, ], 'strike' => [], 'br' => [], 'strong' => [], ]; // ad level label if ( ! empty( $item->get_prop( 'ad_label' ) ) ) { $label = $item->get_prop( 'ad_label' ); } $label = ! empty( $advads_options['custom-label']['html_enabled'] ) ? wp_kses( $label, $allowed_tags ) : esc_html( $label ); $template = sprintf( '
%s
', wp_advads()->get_frontend_prefix() . 'adlabel', $label ); return apply_filters( 'advanced-ads-custom-label', $template, $label ); } /** * Retrieve the number of ads in any status * excludes trash status by default * * @deprecated 1.48.2 * * @param string|array $post_status default post status. * * @return int number of ads. */ public static function get_number_of_ads( $post_status = 'any' ) { _deprecated_function( __METHOD__, '1.48.0', 'AdvancedAds\Utilities\WordPress::get_count_ads()' ); return WordPress::get_count_ads( $post_status ); } /** * Get the array with ad placements * * @deprecated 2.0.0 wp_advads_get_all_placements * * @return array $ad_placements */ public static function get_ad_placements_array() { return wp_advads_get_all_placements(); } /** * Return the Advanced_Ads_Model responsible for loading ads, groups and placements into the frontend * * @deprecated 2.0.0 use new entity functions. * * @return mixed */ public function get_model() { if ( ! isset( $this->model ) ) { $this->model = new Advanced_Ads_Model(); } return $this->model; } /** * Store whether the loop started in an inner `the_content`. * * If so, let us assume that we are in the loop when we are in the outermost `the_content`. * Makes sense only when a hooked to `the_content` function that produces an inner `the_content` has * lesser priority then `$this->plugin->get_content_injection_priority()`. * * @param string $content Post content (unchanged). * * @return string */ public function set_was_in_the_loop( $content ) { if ( self::get_instance()->has_many_the_content() ) { $this->was_in_the_loop = $this->was_in_the_loop || $this->in_the_loop(); } else { // Next top level `the_content`, forget that the loop started. $this->was_in_the_loop = false; } return $content; } /** * Listen to URL parameters for debugging */ private function debug_parameter() { $referer = Params::server( 'HTTP_REFERER' ); if ( wp_doing_ajax() && $referer ) { $query_string = wp_parse_url( $referer, PHP_URL_QUERY ); if ( $query_string ) { parse_str( $query_string, $query ); } if ( empty( $query['aa-debug'] ) ) { return; } $debug_query = $query['aa-debug']; } else { $debug_query = Params::get( 'aa-debug' ); if ( empty( $debug_query ) ) { return; } } $parameters = explode( ',', sanitize_text_field( $debug_query ) ); foreach ( $parameters as $parameter ) { switch ( trim( $parameter ) ) { case 'dummy': // switch all ads to "dummy" add_filter( 'advanced-ads-ad-option-type', function() { return 'dummy'; } ); break; case 'vcoff': // disable ad visitor conditions add_filter( 'advanced-ads-ad-option-visitors', '__return_empty_array' ); break; case 'cboff': // disable cache-busting for all ads add_filter( 'advanced-ads-ad-select-args', function( $args ) { $args['cache-busting'] = 'ignore'; return $args; } ); break; } } } /** * Get priority used for injection inside content */ public function get_content_injection_priority() { $options = $this->options(); return isset( $options['content-injection-priority'] ) ? (int) $options['content-injection-priority'] : 100; } /** * Update internal plugin options * * @param array $options new internal options. */ public function update_internal_options( array $options ) { // do not allow to clear options. if ( [] === $options ) { return; } $this->internal_options = $options; update_option( ADVADS_SLUG . '-internal', $options ); } /** * Fires when a post is transitioned from one status to another. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param WP_Post $post Post object. */ public function transition_ad_status( $new_status, $old_status, $post ) { if ( ! isset( $post->post_type ) || Constants::POST_TYPE_AD !== $post->post_type || ! isset( $post->ID ) ) { return; } $ad = wp_advads_get_ad( $post->ID ); if ( $old_status !== $new_status ) { /** * Fires when an ad has transitioned from one status to another. * * @param Ad $ad Ad object. */ do_action( "advanced-ads-ad-status-{$old_status}-to-{$new_status}", $ad ); } if ( 'publish' === $new_status && 'publish' !== $old_status ) { /** * Fires when an ad has transitioned from any other status to `publish`. * * @param Ad $ad Ad object. */ do_action( 'advanced-ads-ad-status-published', $ad ); } if ( 'publish' === $old_status && 'publish' !== $new_status ) { /** * Fires when an ad has transitioned from `publish` to any other status. * * @param Ad $ad Ad object. */ do_action( 'advanced-ads-ad-status-unpublished', $ad ); } if ( $old_status === 'publish' && $new_status === Constants::AD_STATUS_EXPIRED ) { /** * Fires when an ad is expired. * * @param int $id * @param Ad $ad */ do_action( 'advanced-ads-ad-expired', $ad->get_id(), $ad ); } } }