Commit inicial - WordPress Análisis de Precios Unitarios

- WordPress core y plugins
- Tema Twenty Twenty-Four configurado
- Plugin allow-unfiltered-html.php simplificado
- .gitignore configurado para excluir wp-config.php y uploads

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-03 21:04:30 -06:00
commit a22573bf0b
24068 changed files with 4993111 additions and 0 deletions

View File

@@ -0,0 +1,837 @@
<?php // phpcs:ignoreFile
use AdvancedAds\Framework\Utilities\Arr;
use AdvancedAds\Framework\Utilities\Params;
use AdvancedAds\Utilities\Conditional;
use AdvancedAds\Utilities\WordPress;
/**
* Container class for Ad Health notice handling
*
* @package WordPress
* @subpackage Advanced Ads Plugin
* @since 1.12
*
* related scripts / functions
*
* advads_push_notice() function to push notifications using AJAX in admin/assets/js/admin-global.js
* push_ad_health_notice() in AdvancedAds\Admin\Ajax to push notifications sent via AJAX
* Advanced_Ads_Checks  for the various checks
* list of notification texts in admin/includes/ad-health-notices.php
*/
class Advanced_Ads_Ad_Health_Notices {
/**
* Options
*
* @var array
*/
protected $options;
/**
* All detected notices
*
* Structure is
* [notice_key] => 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;
}
// dont 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 = ' <a href="' . admin_url( 'post.php?post=' . $id . '&action=edit' ) . '">' . $ad->get_title() . '</a>';
$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 doesnt 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 doesnt 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 doesnt 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' ),
'<a href="https://wpadvancedads.com/adsense-errors/?utm_source=advanced-ads&utm_medium=link&utm_campaign=adsense-error' . $code . '" target="_blank">',
'</a>'
);
}
/**
* 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 '&nbsp;<a href="' . $link . '" target="_blank">' . __( 'Get help', 'advanced.ads' ) . '</a>';
}
}

View File

@@ -0,0 +1,397 @@
<?php // phpcs:ignoreFile
use AdvancedAds\Constants;
use AdvancedAds\Utilities\Conditional;
use AdvancedAds\Utilities\Data;
/**
* Checks for various things
*
* @since 1.6.9
*/
class Advanced_Ads_Checks {
/**
* Minimum required PHP version of Advanced Ads
*/
const MINIMUM_PHP_VERSION = '5.6.20';
/**
* A Quiz plugin is active
*
* @return bool true if any quiz plugin is active.
*/
public static function active_quiz_plugins() {
return defined( 'AYS_QUIZ_VERSION' )
|| defined( 'FORMINATOR_PLUGIN_BASENAME' )
|| defined( 'QSM_PLUGIN_PATH' )
|| class_exists( 'GFForms', false );
}
/**
* Show the list of potential issues
*/
public static function show_issues() {
include_once ADVADS_ABSPATH . '/admin/views/checks.php';
}
/**
* PHP version minimum
*
* @return bool true if uses the minimum PHP version or higher
*/
public static function php_version_minimum() {
if ( version_compare( phpversion(), self::MINIMUM_PHP_VERSION, '>=' ) ) {
return true;
}
return false;
}
/**
* Caching used
*
* @deprecated 1.48.0
*
* @return bool true if active
*/
public static function cache() {
_deprecated_function( __METHOD__, '1.48.0', '\AdvancedAds\Utilities\Conditional::has_cache_plugins()' );
return Conditional::has_cache_plugins();
}
/**
* WordPress update available
*
* @return bool true if WordPress update available
*/
public static function wp_update_available() {
$update_data = wp_get_update_data();
$count = absint( $update_data['counts']['wordpress'] );
if ( $count ) {
return true;
}
return false;
}
/**
* Check if license keys are missing or invalid or expired
*
* @since 1.6.6
* @update 1.8.21 also check for expired licenses
* @return true if there are missing licenses
*/
public static function licenses_invalid() {
$add_ons = Data::get_addons();
if ( [] === $add_ons ) {
Advanced_Ads_Ad_Health_Notices::get_instance()->remove( 'license_invalid' );
return false;
}
foreach ( $add_ons as $_add_on_key => $_add_on ) {
$status = Advanced_Ads_Admin_Licenses::get_instance()->get_license_status( $_add_on['options_slug'] );
// check expiry date.
$expiry_date = Advanced_Ads_Admin_Licenses::get_instance()->get_license_expires( $_add_on['options_slug'] );
if ( $expiry_date && 'lifetime' !== $expiry_date && strtotime( $expiry_date ) < time() ) {
return true;
}
// dont check if license is valid.
if ( 'valid' === $status ) {
continue;
}
// retrieve our license key from the DB.
$licenses = Advanced_Ads_Admin_Licenses::get_instance()->get_licenses();
$license_key = isset( $licenses[ $_add_on_key ] ) ? $licenses[ $_add_on_key ] : false;
if ( ! $license_key || 'valid' !== $status ) {
return true;
}
}
// remove notice, if one is given.
Advanced_Ads_Ad_Health_Notices::get_instance()->remove( 'license_invalid' );
return false;
}
/**
* Autoptimize plugin installed
* can change ad tags, especially inline css and scripts
*
* @link https://wordpress.org/plugins/autoptimize/
* @return bool true if Autoptimize is installed
*/
public static function active_autoptimize() {
if ( defined( 'AUTOPTIMIZE_PLUGIN_VERSION' ) ) {
return true;
}
return false;
}
/**
* WP rocket plugin installed
*
* @return bool true if WP rocket is installed
*/
public static function active_wp_rocket() {
if ( defined( 'WP_ROCKET_SLUG' ) ) {
return true;
}
return false;
}
/**
* Checks the settings of wp rocket to find out if combining of javascript files is enabled
*
* @return boolean true, when "Combine JavaScript files" is enabled
*/
public static function is_wp_rocket_combine_js_enabled() {
if ( self::active_wp_rocket() ) {
$settings = get_option( 'wp_rocket_settings' );
if ( $settings ) {
if ( isset( $settings['minify_concatenate_js'] ) && $settings['minify_concatenate_js'] ) {
return true;
}
}
}
return false;
}
/**
* Any AMP plugin enabled
*
* @return bool true if AMP plugin is installed
*/
public static function active_amp_plugin() {
// Accelerated Mobile Pages.
if ( function_exists( 'ampforwp_is_amp_endpoint' ) ) {
return true;
}
// AMP plugin.
if ( function_exists( 'is_amp_endpoint' ) ) {
return true;
}
// other plugins.
if ( function_exists( 'is_wp_amp' ) ) {
return true;
}
return false;
}
/**
* Checks if the preconditions are met to wrap an ad with <!--noptimize--> comments
*
* @return boolean
*/
public static function requires_noptimize_wrapping() {
return self::active_autoptimize() || self::is_wp_rocket_combine_js_enabled();
}
/**
* Check for additional conflicting plugins
*
* @return array $plugins names of conflicting plugins
*/
public static function conflicting_plugins() {
$conflicting_plugins = [];
if ( defined( 'Publicize_Base' ) ) { // JetPack Publicize module.
$conflicting_plugins[] = 'Jetpack Publicize';
}
if ( defined( 'PF__PLUGIN_DIR' ) ) { // Facebook Instant Articles & Google AMP Pages by PageFrog.
$conflicting_plugins[] = 'Facebook Instant Articles & Google AMP Pages by PageFrog';
}
if ( defined( 'GT_VERSION' ) ) { // GT ShortCodes.
$conflicting_plugins[] = 'GT ShortCodes';
}
if ( class_exists( 'SimilarPosts', false ) ) { // Similar Posts, https://de.wordpress.org/plugins/similar-posts/.
$conflicting_plugins[] = 'Similar Posts';
}
return $conflicting_plugins;
}
/**
* Check if any of the global hide ads options is set
* ignore RSS feed setting, because it is standard
*
* @since 1.7.10
* @return bool
*/
public static function ads_disabled() {
$options = Advanced_Ads::get_instance()->options();
if ( isset( $options['disabled-ads'] ) && is_array( $options['disabled-ads'] ) ) {
foreach ( $options['disabled-ads'] as $_key => $_value ) {
// dont warn if "RSS Feed", "404", or "REST API" option are enabled, because they are normally not critical.
if ( ! empty( $_value ) && ! in_array( (string) $_key, [ 'feed', '404', 'rest-api' ], true ) ) {
return true;
}
}
}
return false;
}
/**
* Check for required php extensions
*
* @return array
*/
public static function php_extensions() {
$missing_extensions = [];
if ( ! extension_loaded( 'dom' ) ) {
$missing_extensions[] = 'dom';
}
if ( ! extension_loaded( 'mbstring' ) ) {
$missing_extensions[] = 'mbstring';
}
return $missing_extensions;
}
/**
* Get the list of Advanced Ads constant defined by the user.
*
* @return array
*/
public static function get_defined_constants() {
$constants = apply_filters(
'advanced-ads-constants',
[
'ADVADS_ADS_DISABLED',
'ADVADS_ALLOW_ADSENSE_ON_404',
'ADVADS_DISABLE_RESPONSIVE_IMAGES',
'ADVANCED_ADS_AD_DEBUG_FOR_ADMIN_ONLY',
'ADVANCED_ADS_DISABLE_ANALYTICS_ANONYMIZE_IP',
'ADVANCED_ADS_DISABLE_CHANGE',
'ADVANCED_ADS_DISABLE_CODE_HIGHLIGHTING',
'ADVANCED_ADS_DISABLE_SHORTCODE_BUTTON',
'ADVANCED_ADS_DISALLOW_PHP',
'ADVANCED_ADS_ENABLE_REVISIONS',
'ADVANCED_ADS_GEO_TEST_IP',
'ADVANCED_ADS_PRO_CUSTOM_POSITION_MOVE_INTO_HIDDEN',
'ADVANCED_ADS_PRO_PAGE_IMPR_EXDAYS',
'ADVANCED_ADS_PRO_REFERRER_EXDAYS',
'ADVANCED_ADS_RESPONSIVE_DISABLE_BROWSER_WIDTH',
'ADVANCED_ADS_SHOW_LICENSE_RESPONSE',
'ADVANCED_ADS_SUPPRESS_PLUGIN_ERROR_NOTICES',
'ADVANCED_ADS_TRACKING_DEBUG',
'ADVANCED_ADS_TRACKING_NO_HOURLY_LIMIT',
]
);
$result = [];
foreach ( $constants as $constant ) {
if ( defined( $constant ) ) {
$result[] = $constant;
}
}
return $result;
}
/**
* WP Engine hosting detected
*
* @return bool true if site is hosted by WP Engine
*/
public static function wp_engine_hosting() {
if ( defined( 'WPE_APIKEY' ) ) {
return true;
}
return false;
}
/**
* Notice for Adblocker module if assets have expired
*/
public static function assets_expired() {
$plugin_options = Advanced_Ads::get_instance()->get_adblocker_options();
$adblocker_options = Advanced_Ads_Ad_Blocker::get_instance()->options();
return ! empty( $plugin_options['use-adblocker'] ) && empty( $adblocker_options['module_can_work'] );
}
/**
* Check for other ads.txt plugins
*
* @return array
*/
public static function ads_txt_plugins() {
$ads_txt_plugins = [];
// Ads.txt Manager.
if ( function_exists( 'tenup_display_ads_txt' ) ) {
$ads_txt_plugins[] = 'Ads.txt Manager';
}
// todo:
// ads-txt-admin/unveil-media-ads-txt.php
// simple-ads-txt/bs_ads_txt.php
// ads-txt-manager/adstxtmanager.php
// monetizemore-ads-txt/wp-ads-txt.php
// authorized-sellers-manager/ads-txt-publisher.php.
return $ads_txt_plugins;
}
/**
* Check for activated plugins that manage header or footer code
*
* @return array
*/
public static function header_footer_plugins() {
$plugins = [];
// Header Footer Code Manager.
if ( function_exists( 'hfcm_options_install' ) ) {
$plugins[] = 'Header Footer Code Manager';
}
// Head, Footer and Post Injections.
if ( function_exists( 'hefo_template_redirect' ) ) {
$plugins[] = 'Head, Footer and Post Injections';
}
// Insert Headers and Footers /insert-headers-and-footers/.
if ( class_exists( 'InsertHeadersAndFooters', false ) ) {
$plugins[] = 'Insert Headers and Footers';
}
// Header and Footer Scripts /header-and-footer-scripts/.
if ( class_exists( 'HeaderAndFooterScripts', false ) ) {
$plugins[] = 'Header and Footer Scripts';
}
return $plugins;
}
/**
* Check if monetize wizard has been completed or notice dismissed
*
* @return bool
*/
public static function can_launch_wizard(): bool {
$wizard_done = get_option( Constants::OPTION_WIZARD_COMPLETED, false );
$notice_dismissed = get_user_meta( get_current_user_id(), Constants::USER_WIZARD_DISMISS, true );
return ! $wizard_done && ! $notice_dismissed;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
<?php // phpcs:ignoreFile
/**
* @since 1.7.17
*/
class Advanced_Ads_Filesystem {
/**
* Singleton instance of the class
*
* @var Advanced_Ads_Filesystem
*/
protected static $instance;
/**
* Return an instance of Advanced_Ads_Filesystem
*
* @return Advanced_Ads_Filesystem
*/
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self;
}
return self::$instance;
}
private function __construct() {}
/**
* Connect to the filesystem.
*
* @param array $directories A list of directories. If any of these do
* not exist, a WP_Error object will be returned.
* @return bool|WP_Error True if able to connect, false or a WP_Error otherwise.
*/
public function fs_connect( $directories = [] ) {
global $wp_filesystem;
$directories = ( is_array( $directories ) && count( $directories ) ) ? $directories : [ WP_CONTENT_DIR ];
// This will output a credentials form in event of failure, We don't want that, so just hide with a buffer.
ob_start();
$credentials = request_filesystem_credentials( '', '', false, $directories[0] );
ob_end_clean();
if ( false === $credentials ) {
return false;
}
if ( ! WP_Filesystem( $credentials ) ) {
$error = true;
if ( is_object( $wp_filesystem ) && $wp_filesystem->errors->get_error_code() ) {
$error = $wp_filesystem->errors;
}
// Failed to connect, Error and request again.
ob_start();
request_filesystem_credentials( '', '', $error, $directories[0] );
ob_end_clean();
return false;
}
if ( ! is_object( $wp_filesystem) ) {
return new WP_Error( 'fs_unavailable', __( 'Could not access filesystem.', 'advanced-ads' ) );
}
if ( is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->get_error_code() ) {
return new WP_Error( 'fs_error', __( 'Filesystem error.', 'advanced-ads' ), $wp_filesystem->errors);
}
foreach ( (array) $directories as $dir ) {
switch ( $dir ) {
case ABSPATH:
if ( ! $wp_filesystem->abspath() )
return new WP_Error( 'fs_no_root_dir', __( 'Unable to locate WordPress root directory.', 'advanced-ads' ) );
break;
case WP_CONTENT_DIR:
if ( ! $wp_filesystem->wp_content_dir() )
return new WP_Error( 'fs_no_content_dir', __( 'Unable to locate WordPress content directory (wp-content).', 'advanced-ads' ) );
break;
default:
if ( ! $wp_filesystem->find_folder( $dir ) )
/* translators: %s directory */
return new WP_Error( 'fs_no_folder', sprintf( __( 'Unable to locate needed folder (%s).', 'advanced-ads' ) , esc_html( basename( $dir ) ) ) );
break;
}
}
return true;
}
/**
* Replace the 'direct' absolute path with the Filesystem API path. Useful only when the 'direct' method is not used.
* Works only with folders.
* Check https://codex.wordpress.org/Filesystem_API for info
*
* @param string existing path
* @return string normalized path
*/
public function normalize_path( $path ) {
global $wp_filesystem;
return $wp_filesystem->find_folder( $path );
}
/**
* Recursive directory creation based on full path.
*
* @param string $target Full path to attempt to create.
* @return bool Whether the path was created. True if path already exists.
*/
public function mkdir_p( $target ) {
global $wp_filesystem;
if ( $wp_filesystem instanceof WP_Filesystem_Direct ) {
return wp_mkdir_p( $target );
}
$target = rtrim($target, '/');
if ( empty($target) ) {
$target = '/';
}
if ( $wp_filesystem->exists( $target ) ) {
return $wp_filesystem->is_dir( $target );
}
$target_parent = dirname( $target );
while ( '.' != $target_parent && ! $wp_filesystem->is_dir( $target_parent ) ) {
$target_parent = dirname( $target_parent );
}
$folder_parts = explode( '/', substr( $target, strlen( $target_parent ) + 1 ) );
for ( $i = 1, $c = count( $folder_parts ); $i <= $c; $i++ ) {
$dir = $target_parent . '/' . implode( '/', array_slice( $folder_parts, 0, $i ) );
if ( $wp_filesystem->exists( $dir ) ) { continue; }
if ( ! $wp_filesystem->mkdir( $dir ) ) {
return false;
}
}
return true;
}
/**
* Print the filesystem credentials modal when needed.
*/
public function print_request_filesystem_credentials_modal() {
$filesystem_method = get_filesystem_method();
ob_start();
$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
ob_end_clean();
$request_filesystem_credentials = ( $filesystem_method != 'direct' && ! $filesystem_credentials_are_stored );
if ( ! $request_filesystem_credentials ) {
return;
}
?>
<div id="advanced-ads-rfc-dialog" class="notification-dialog-wrap request-filesystem-credentials-dialog">
<div class="notification-dialog-background"></div>
<div class="notification-dialog" role="dialog" aria-labelledby="request-filesystem-credentials-title" tabindex="0">
<div class="request-filesystem-credentials-dialog-content">
<?php request_filesystem_credentials( site_url() ); ?>
</div>
</div>
</div>
<?php
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,758 @@
<?php // phpcs:ignoreFileName
use AdvancedAds\Utilities\Conditional;
/**
* Injects ads in the content based on an XPath expression.
*/
class Advanced_Ads_In_Content_Injector {
/**
* Gather placeholders which later are replaced by the ads
*
* @var array $ads_for_placeholders
*/
private static $ads_for_placeholders = [];
/**
* Inject ads directly into the content
*
* @param string $placement_id Id of the placement.
* @param array $placement_opts Placement options.
* @param string $content Content to inject placement into.
* @param array $options {
* Injection options.
*
* @type bool $allowEmpty Whether the tag can be empty to be counted.
* @type bool $paragraph_select_from_bottom Whether to select ads from buttom.
* @type string $position Position. Can be one of 'before', 'after', 'append', 'prepend'
* @type number $alter_nodes Whether to alter nodes, for example to prevent injecting ads into `a` tags.
* @type bool $repeat Whether to repeat the position.
* @type number $paragraph_id Paragraph Id.
* @type number $itemLimit If there are too few items at this level test nesting. Set to '-1` to prevent testing.
* }
*
* @return string $content Content with injected placement.
*/
public static function &inject_in_content( $placement_id, $placement_opts, &$content, $options = [] ) {
if ( ! extension_loaded( 'dom' ) ) {
return $content;
}
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
// phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
// parse arguments.
$tag = isset( $placement_opts['tag'] ) ? $placement_opts['tag'] : 'p';
$tag = preg_replace( '/[^a-z0-9]/i', '', $tag ); // simplify tag.
/**
* Store the original tag value since $tag is changed on the fly and we might want to know the original selected
* options for some checks later.
*/
$tag_option = $tag;
// allow more complex xPath expression.
$tag = apply_filters( 'advanced-ads-placement-content-injection-xpath', $tag, $placement_opts );
// get plugin options.
$plugin_options = Advanced_Ads::get_instance()->options();
$defaults = [
'allowEmpty' => false,
'paragraph_select_from_bottom' => isset( $placement_opts['start_from_bottom'] ) && $placement_opts['start_from_bottom'],
'position' => isset( $placement_opts['position'] ) ? $placement_opts['position'] : 'after',
// only has before and after.
'before' => isset( $placement_opts['position'] ) && 'before' === $placement_opts['position'],
// Whether to alter nodes, for example to prevent injecting ads into `a` tags.
'alter_nodes' => true,
'repeat' => false,
];
$defaults['paragraph_id'] = isset( $placement_opts['index'] ) ? $placement_opts['index'] : 1;
$defaults['paragraph_id'] = max( 1, (int) $defaults['paragraph_id'] );
// if there are too few items at this level test nesting.
$defaults['itemLimit'] = 'p' === $tag_option ? 2 : 1;
// trigger such a high item limit that all elements will be considered.
if ( ! empty( $plugin_options['content-injection-level-disabled'] ) ) {
$defaults['itemLimit'] = 1000;
}
// Handle tags that are empty by definition or could be empty ("custom" option).
if ( in_array( $tag_option, [ 'img', 'iframe', 'custom' ], true ) ) {
$defaults['allowEmpty'] = true;
}
// Merge the options if possible. If there are common keys, we don't merge them to prevent overriding and unexpected behavior.
$common_keys = array_intersect_key( $options, $placement_opts );
if ( empty( $common_keys ) ) {
$options = array_merge( $options, $placement_opts );
}
// allow hooks to change some options.
$options = apply_filters(
'advanced-ads-placement-content-injection-options',
wp_parse_args( $options, $defaults ),
$tag_option
);
$wp_charset = get_bloginfo( 'charset' );
// parse document as DOM (fragment - having only a part of an actual post given).
$content_to_load = self::get_content_to_load( $content );
if ( ! $content_to_load ) {
return $content;
}
$dom = new DOMDocument( '1.0', $wp_charset );
// may loose some fragments or add autop-like code.
$libxml_use_internal_errors = libxml_use_internal_errors( true ); // avoid notices and warnings - html is most likely malformed.
$success = $dom->loadHtml( '<!DOCTYPE html><html><meta http-equiv="Content-Type" content="text/html; charset=' . $wp_charset . '" /><body>' . $content_to_load );
libxml_use_internal_errors( $libxml_use_internal_errors );
if ( true !== $success ) {
// -TODO handle cases were dom-parsing failed (at least inform user)
return $content;
}
/**
* Handle advanced tags.
*/
switch ( $tag_option ) {
case 'p':
// Exclude paragraphs within blockquote tags.
$tag = 'p[not(parent::blockquote)]';
break;
case 'pwithoutimg':
// Convert option name into correct path, exclude paragraphs within blockquote tags.
$tag = 'p[not(descendant::img) and not(parent::blockquote)]';
break;
case 'img':
/*
* Handle: 1) "img" tags 2) "image" block 3) "gallery" block 4) "gallery shortcode" 5) "wp_caption" shortcode
* Handle the gallery created by the block or the shortcode as one image.
* Prevent injection of ads next to images in tables.
*/
// Default shortcodes, including non-HTML5 versions.
$shortcodes = "@class and (
contains(concat(' ', normalize-space(@class), ' '), ' gallery-size') or
contains(concat(' ', normalize-space(@class), ' '), ' wp-caption ') )";
$tag = "*[self::img or self::figure or self::div[$shortcodes]]
[not(ancestor::table or ancestor::figure or ancestor::div[$shortcodes])]";
break;
// Any headline. By default h2, h3, and h4.
case 'headlines':
$headlines = apply_filters( 'advanced-ads-headlines-for-ad-injection', [ 'h2', 'h3', 'h4' ] );
foreach ( $headlines as &$headline ) {
$headline = 'self::' . $headline;
}
$tag = '*[' . implode( ' or ', $headlines ) . ']'; // /html/body/*[self::h2 or self::h3 or self::h4]
break;
// Any HTML element that makes sense in the content.
case 'anyelement':
$exclude = [
'html',
'body',
'script',
'style',
'tr',
'td',
// Inline tags.
'a',
'abbr',
'b',
'bdo',
'br',
'button',
'cite',
'code',
'dfn',
'em',
'i',
'img',
'kbd',
'label',
'option',
'q',
'samp',
'select',
'small',
'span',
'strong',
'sub',
'sup',
'textarea',
'time',
'tt',
'var',
];
$tag = '*[not(self::' . implode( ' or self::', $exclude ) . ')]';
break;
case 'custom':
// Get the path for the "custom" tag choice, use p as a fallback to prevent it from showing any ads if users left it empty.
$tag = ! empty( $placement_opts['xpath'] ) ? stripslashes( $placement_opts['xpath'] ) : 'p';
break;
}
// select positions.
$xpath = new DOMXPath( $dom );
if ( -1 !== $options['itemLimit'] ) {
$items = $xpath->query( '/html/body/' . $tag );
if ( $items->length < $options['itemLimit'] ) {
$items = $xpath->query( '/html/body/*/' . $tag );
}
// try third level.
if ( $items->length < $options['itemLimit'] ) {
$items = $xpath->query( '/html/body/*/*/' . $tag );
}
// try all levels as last resort.
if ( $items->length < $options['itemLimit'] ) {
$items = $xpath->query( '//' . $tag );
}
} else {
$items = $xpath->query( $tag );
}
// allow to select other elements.
$items = apply_filters( 'advanced-ads-placement-content-injection-items', $items, $xpath, $tag_option );
// filter empty tags from items.
$whitespaces = json_decode( '"\t\n\r \u00A0"' );
$paragraphs = [];
foreach ( $items as $item ) {
if ( $options['allowEmpty'] || ( isset( $item->textContent ) && trim( $item->textContent, $whitespaces ) !== '' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
$paragraphs[] = $item;
}
}
$ancestors_to_limit = self::get_ancestors_to_limit( $xpath );
$paragraphs = self::filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit );
$options['paragraph_count'] = count( $paragraphs );
if ( $options['paragraph_count'] >= $options['paragraph_id'] ) {
$offset = $options['paragraph_select_from_bottom'] ? $options['paragraph_count'] - $options['paragraph_id'] : $options['paragraph_id'] - 1;
$offsets = apply_filters( 'advanced-ads-placement-content-offsets', [ $offset ], $options, $placement_opts, $xpath, $paragraphs, $dom );
$did_inject = false;
foreach ( $offsets as $offset ) {
// inject.
$node = apply_filters( 'advanced-ads-placement-content-injection-node', $paragraphs[ $offset ], $tag, $options['before'] );
if ( $options['alter_nodes'] ) {
// Prevent injection into image caption and gallery.
$parent = $node;
for ( $i = 0; $i < 4; $i++ ) {
$parent = $parent->parentNode;
if ( ! $parent instanceof DOMElement ) {
break;
}
if ( preg_match( '/\b(wp-caption|gallery-size)\b/', $parent->getAttribute( 'class' ) ) ) {
$node = $parent;
break;
}
}
// Make sure that the ad is injected outside the link.
if ( 'img' === $tag_option && 'a' === $node->parentNode->tagName ) {
if ( $options['before'] ) {
$node->parentNode;
} else {
// Go one level deeper if inserted after to not insert the ad into the link; probably after the paragraph.
$node->parentNode->parentNode;
}
}
}
$ad_content = (string) get_the_placement( $placement_id, '', $placement_opts );
if ( trim( $ad_content, $whitespaces ) === '' ) {
continue;
}
// phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
$ad_content = self::filter_ad_content( $ad_content, $node->tagName, $options );
// convert HTML to XML!
$ad_dom = new DOMDocument( '1.0', $wp_charset );
$libxml_use_internal_errors = libxml_use_internal_errors( true );
$ad_dom->loadHtml( '<!DOCTYPE html><html><meta http-equiv="Content-Type" content="text/html; charset=' . $wp_charset . '" /><body>' . $ad_content );
switch ( $options['position'] ) {
case 'append':
$ref_node = $node;
foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
$importedNode = $dom->importNode( $importedNode, true );
$ref_node->appendChild( $importedNode );
}
break;
case 'prepend':
$ref_node = $node;
foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
$importedNode = $dom->importNode( $importedNode, true );
$ref_node->insertBefore( $importedNode, $ref_node->firstChild );
}
break;
case 'before':
$ref_node = $node;
foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
$importedNode = $dom->importNode( $importedNode, true );
$ref_node->parentNode->insertBefore( $importedNode, $ref_node );
}
break;
case 'after':
default:
// append before next node or as last child to body.
$ref_node = $node->nextSibling;
if ( isset( $ref_node ) ) {
foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
$importedNode = $dom->importNode( $importedNode, true );
$ref_node->parentNode->insertBefore( $importedNode, $ref_node );
}
} else {
// append to body; -TODO using here that we only select direct children of the body tag.
foreach ( $ad_dom->getElementsByTagName( 'body' )->item( 0 )->childNodes as $importedNode ) {
$importedNode = $dom->importNode( $importedNode, true );
$node->parentNode->appendChild( $importedNode );
}
}
}
libxml_use_internal_errors( $libxml_use_internal_errors );
$did_inject = true;
}
if ( ! $did_inject ) {
return $content;
}
$content_orig = $content;
// convert to text-representation.
$content = $dom->saveHTML();
$content = self::prepare_output( $content, $content_orig );
/**
* Show a warning to ad admins in the Ad Health bar in the frontend, when
*
* * the level limitation was not disabled
* * could not inject one ad (as by use of `elseif` here)
* * but there are enough elements on the site, but just in sub-containers
*/
} elseif ( Conditional::user_can( 'advanced_ads_manage_options' )
&& -1 !== $options['itemLimit']
&& empty( $plugin_options['content-injection-level-disabled'] ) ) {
// Check if there are more elements without limitation.
$all_items = $xpath->query( '//' . $tag );
$paragraphs = [];
foreach ( $all_items as $item ) {
if ( $options['allowEmpty'] || ( isset( $item->textContent ) && trim( $item->textContent, $whitespaces ) !== '' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar
$paragraphs[] = $item;
}
}
$paragraphs = self::filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit );
if ( $options['paragraph_id'] <= count( $paragraphs ) ) {
// Add a warning to ad health.
add_filter( 'advanced-ads-ad-health-nodes', [ 'Advanced_Ads_In_Content_Injector', 'add_ad_health_node' ] );
}
}
// phpcs:enable
return $content;
}
/**
* Get content to load.
*
* @param string $content Original content.
*
* @return string $content Content to load.
*/
private static function get_content_to_load( $content ) {
// Prevent removing closing tags in scripts.
$content_to_load = preg_replace( '/<script.*?<\/script>/si', '<!--\0-->', $content );
// check which priority the wpautop filter has; might have been disabled on purpose.
$wpautop_priority = has_filter( 'the_content', 'wpautop' );
if ( $wpautop_priority && Advanced_Ads::get_instance()->get_content_injection_priority() < $wpautop_priority ) {
$content_to_load = wpautop( $content_to_load );
}
return $content_to_load;
}
/**
* Filter ad content.
*
* @param string $ad_content Ad content.
* @param string $tag_name tar before/after the content.
* @param array $options Injection options.
*
* @return string ad content.
*/
private static function filter_ad_content( $ad_content, $tag_name, $options ) {
// Replace `</` with `<\/` in ad content when placed within `document.write()` to prevent code from breaking.
$ad_content = preg_replace( '#(document.write.+)</(.*)#', '$1<\/$2', $ad_content );
// Inject placeholder.
$id = count( self::$ads_for_placeholders );
self::$ads_for_placeholders[] = [
'id' => $id,
'tag' => $tag_name,
'position' => $options['position'],
'ad' => $ad_content,
];
return '%advads_placeholder_' . $id . '%';
}
/**
* Prepare output.
*
* @param string $content Modified content.
* @param string $content_orig Original content.
*
* @return string $content Content to output.
*/
private static function prepare_output( $content, $content_orig ) {
$content = self::inject_ads( $content, $content_orig, self::$ads_for_placeholders );
self::$ads_for_placeholders = [];
return $content;
}
/**
* Search for ad placeholders in the `$content` to determine positions at which to inject ads.
* Given the positions, inject ads into `$content_orig.
*
* @param string $content Post content with injected ad placeholders.
* @param string $content_orig Unmodified post content.
* @param array $ads_for_placeholders Array of ads.
* Each ad contains placeholder id, before or after which tag to inject the ad, the ad content.
*
* @return string $content
*/
private static function inject_ads( $content, $content_orig, $ads_for_placeholders ) {
$self_closing_tags = [
'area',
'base',
'basefont',
'bgsound',
'br',
'col',
'embed',
'frame',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr',
];
// It is not possible to append/prepend in self closing tags.
foreach ( $ads_for_placeholders as &$ad_content ) {
if (
( 'prepend' === $ad_content['position'] || 'append' === $ad_content['position'] ) &&
in_array( $ad_content['tag'], $self_closing_tags, true )
) {
$ad_content['position'] = 'after';
}
}
unset( $ad_content );
usort( $ads_for_placeholders, [ 'Advanced_Ads_In_Content_Injector', 'sort_ads_for_placehoders' ] );
// Add tags before/after which ad placehoders were injected.
$alts = [];
foreach ( $ads_for_placeholders as $ad_content ) {
$tag = $ad_content['tag'];
switch ( $ad_content['position'] ) {
case 'before':
case 'prepend':
$alts[] = "<{$tag}[^>]*>";
break;
case 'after':
if ( in_array( $tag, $self_closing_tags, true ) ) {
$alts[] = "<{$tag}[^>]*>";
} else {
$alts[] = "</{$tag}>";
}
break;
case 'append':
$alts[] = "</{$tag}>";
break;
}
}
$alts = array_unique( $alts );
$tag_regexp = implode( '|', $alts );
// Add ad placeholder.
$alts[] = '%advads_placeholder_(?:\d+)%';
$tag_and_placeholder_regexp = implode( '|', $alts );
preg_match_all( "#{$tag_and_placeholder_regexp}#i", $content, $tag_matches );
$count = 0;
// For each tag located before/after an ad placeholder, find its offset among the same tags.
foreach ( $tag_matches[0] as $r ) {
if ( preg_match( '/%advads_placeholder_(\d+)%/', $r, $result ) ) {
$id = $result[1];
$found_ad = false;
foreach ( $ads_for_placeholders as $n => $ad ) {
if ( (int) $ad['id'] === (int) $id ) {
$found_ad = $ad;
break;
}
}
if ( ! $found_ad ) {
continue;
}
switch ( $found_ad['position'] ) {
case 'before':
case 'append':
$ads_for_placeholders[ $n ]['offset'] = $count;
break;
case 'after':
case 'prepend':
$ads_for_placeholders[ $n ]['offset'] = $count - 1;
break;
}
} else {
++$count;
}
}
// Find tags before/after which we need to inject ads.
preg_match_all( "#{$tag_regexp}#i", $content_orig, $orig_tag_matches, PREG_OFFSET_CAPTURE );
$new_content = '';
$pos = 0;
foreach ( $orig_tag_matches[0] as $n => $r ) {
$to_inject = [];
// Check if we need to inject an ad at this offset.
foreach ( $ads_for_placeholders as $ad ) {
if ( isset( $ad['offset'] ) && $ad['offset'] === $n ) {
$to_inject[] = $ad;
}
}
foreach ( $to_inject as $item ) {
switch ( $item['position'] ) {
case 'before':
case 'append':
$found_pos = $r[1];
break;
case 'after':
case 'prepend':
$found_pos = $r[1] + strlen( $r[0] );
break;
}
$new_content .= substr( $content_orig, $pos, $found_pos - $pos );
$pos = $found_pos;
$new_content .= $item['ad'];
}
}
$new_content .= substr( $content_orig, $pos );
return $new_content;
}
/**
* Callback function for usort() to sort ads for placeholders.
*
* @param array $first The first array to compare.
* @param array $second The second array to compare.
*
* @return int 0 if both objects equal. -1 if second array should come first, 1 otherwise.
*/
public static function sort_ads_for_placehoders( $first, $second ) {
if ( $first['position'] === $second['position'] ) {
return 0;
}
$num = [
'before' => 1,
'prepend' => 2,
'append' => 3,
'after' => 4,
];
return $num[ $first['position'] ] > $num[ $second['position'] ] ? 1 : - 1;
}
/**
* Add a warning to 'Ad health'.
*
* @param array $nodes .
*
* @return array $nodes.
*/
public static function add_ad_health_node( $nodes ) {
$nodes[] = [
'type' => 1,
'data' => [
'parent' => 'advanced_ads_ad_health',
'id' => 'advanced_ads_ad_health_the_content_not_enough_elements',
'title' => sprintf(
/* translators: %s stands for the name of the "Disable level limitation" option and automatically translated as well */
__( 'Set <em>%s</em> to show more ads', 'advanced-ads' ),
__( 'Disable level limitation', 'advanced-ads' )
),
'href' => admin_url( '/admin.php?page=advanced-ads-settings#top#general' ),
'meta' => [
'class' => 'advanced_ads_ad_health_warning',
'target' => '_blank',
],
],
];
return $nodes;
}
/**
* Get paths of ancestors that should not contain ads.
*
* @param object $xpath DOMXPath object.
*
* @return array Paths of ancestors.
*/
private static function get_ancestors_to_limit( $xpath ) {
$query = self::get_ancestors_to_limit_query();
if ( ! $query ) {
return [];
}
$node_list = $xpath->query( $query );
$ancestors_to_limit = [];
foreach ( $node_list as $a ) {
$ancestors_to_limit[] = $a->getNodePath();
}
return $ancestors_to_limit;
}
/**
* Remove paragraphs that has ancestors that should not contain ads.
*
* @param array $paragraphs An array of `DOMNode` objects to insert ads before or after.
* @param array $ancestors_to_limit Paths of ancestor that should not contain ads.
*
* @return array $new_paragraphs An array of `DOMNode` objects to insert ads before or after.
*/
private static function filter_by_ancestors_to_limit( $paragraphs, $ancestors_to_limit ) {
$new_paragraphs = [];
foreach ( $paragraphs as $k => $paragraph ) {
foreach ( $ancestors_to_limit as $a ) {
if ( 0 === stripos( $paragraph->getNodePath(), $a ) ) {
continue 2;
}
}
$new_paragraphs[] = $paragraph;
}
return $new_paragraphs;
}
/**
* Get query to select ancestors that should not contain ads.
*
* @return string/false DOMXPath query or false.
*/
private static function get_ancestors_to_limit_query() {
/**
* TODO:
* - support `%` (rand) at the start
* - support plain text that node should contain instead of CSS selectors
* - support `prev` and `next` as `type`
*/
/**
* Filter the nodes that limit injection.
*
* @param array An array of arrays, each of which contains:
*
* @type string $type Accept: `ancestor` - limit injection inside the ancestor.
* @type string $node A "class selector" which targets one class (.) or "id selector" which targets one id (#),
* optionally with `%` at the end.
*/
$items = apply_filters(
'advanced-ads-content-injection-nodes-without-ads',
[
[
// a class anyone can use to prevent automatic ad injection into a specific element.
'node' => '.advads-stop-injection',
'type' => 'ancestor',
],
[
// Product Slider for Beaver Builder by WooPack.
'node' => '.woopack-product-carousel',
'type' => 'ancestor',
],
[
// WP Author Box Lite.
'node' => '#wpautbox-%',
'type' => 'ancestor',
],
[
// GeoDirectory Post Slider.
'node' => '.geodir-post-slider',
'type' => 'ancestor',
],
]
);
$query = [];
foreach ( $items as $p ) {
$sel = $p['node'];
$sel_type = substr( $sel, 0, 1 );
$sel = substr( $sel, 1 );
$rand_pos = strpos( $sel, '%' );
$sel = str_replace( '%', '', $sel );
$sel = sanitize_html_class( $sel );
if ( '.' === $sel_type ) {
if ( false !== $rand_pos ) {
$query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel')";
} else {
$query[] = "@class and contains(concat(' ', normalize-space(@class), ' '), ' $sel ')";
}
}
if ( '#' === $sel_type ) {
if ( false !== $rand_pos ) {
$query[] = "@id and starts-with(@id, '$sel')";
} else {
$query[] = "@id and @id = '$sel'";
}
}
}
if ( ! $query ) {
return false;
}
return '//*[' . implode( ' or ', $query ) . ']';
}
}

View File

@@ -0,0 +1,123 @@
<?php // phpcs:ignore WordPress.Files.FileName
use AdvancedAds\Abstracts\Ad;
/**
* Handles Advanced Ads Inline CSS settings.
*/
class Advanced_Ads_Inline_Css {
/**
* Holds the state if inline css should be output or not.
*
* @var bool
*/
protected $add_inline_css;
/**
* Initialize the module.
*/
public function __construct() {
/**
* Filters the state if inline css should be output or not.
* Ajax CB container could have added inline css already.
*
* Set to false if an addon output inline css before the main plugin.
*
* @param bool Contains the state.
*/
$this->add_inline_css = apply_filters( 'advanced-ads-output-inline-css', true );
if ( ! $this->add_inline_css ) {
return;
}
// Add inline css to the tcf container.
$this->check_tcf_option();
}
/**
* Adds inline css.
*
* @param array $wrapper Add wrapper array.
* @param string $css Custom inline css.
* @param bool|null $global_output Whether this ad is using cache-busting.
*
* @return array
*/
public function add_css( $wrapper, $css, $global_output ) {
$this->add_inline_css = $this->add_inline_css && false !== $global_output;
if ( ! $this->add_inline_css ) {
return $wrapper;
}
$styles = $this->get_styles_by_string( $css );
$wrapper['style'] = empty( $wrapper['style'] ) ? $styles : array_merge( $wrapper['style'], $styles );
$this->add_inline_css = false;
return $wrapper;
}
/**
* Extend TCF output with a container containing inline css.
*
* @param string $output The output string.
* @param Ad $ad Ad instance.
*
* @return string
*/
public function add_tcf_container( $output, Ad $ad ) {
$inline_css = $ad->get_prop( 'inline-css' );
if (
! $ad->get_prop( 'ad_args.global_output' )
|| empty( $inline_css )
|| strpos( $output, '<div class="tcf-container"' ) === 0
) {
return $output;
}
return sprintf(
'<div class="tcf-container" style="' . $inline_css . '">%s</div>',
$output
);
}
/**
* Reformat css styles string to array.
*
* @param string $string CSS-Style.
*
* @return array
*/
private function get_styles_by_string( string $string ): array { // phpcs:ignore
$chunks = array_chunk( preg_split( '/[:;]/', $string ), 2 );
array_walk_recursive(
$chunks,
function ( &$value ) {
$value = trim( $value );
}
);
$keys = array_filter( array_column( $chunks, 0 ) );
$values = array_filter( array_column( $chunks, 1 ) );
$length = min( count( $keys ), count( $values ) );
return array_combine( array_slice( $keys, 0, $length ), array_slice( $values, 0, $length ) );
}
/**
* If TCF is active, i.e. there is a TCF container, add the options to this container.
*/
private function check_tcf_option() {
static $privacy_options;
if ( null === $privacy_options ) {
$privacy_options = Advanced_Ads_Privacy::get_instance()->options();
}
if ( ! empty( $privacy_options['enabled'] ) && 'on' === $privacy_options['enabled'] && 'iab_tcf_20' === $privacy_options['consent-method'] ) {
add_filter( 'advanced-ads-output-final', [ $this, 'add_tcf_container' ], 20, 2 );
$this->add_inline_css = false;
}
}
}

View File

@@ -0,0 +1,292 @@
<?php // phpcs:ignoreFilename
/**
* Class Advanced_Ads_Utils
*/
class Advanced_Ads_Utils {
/**
* Merges multiple arrays, recursively, and returns the merged array.
*
* This function is similar to PHP's array_merge_recursive() function, but it
* handles non-array values differently. When merging values that are not both
* arrays, the latter value replaces the former rather than merging with it.
*
* Example:
* $link_options_1 = array( 'fragment' => 'x', 'class' => array( 'a', 'b' ) );
* $link_options_2 = array( 'fragment' => 'y', 'class' => array( 'c', 'd' ) );
* // This results in array( 'fragment' => 'y', 'class' => array( 'a', 'b', 'c', 'd' ) ).
*
* @param array $arrays An arrays of arrays to merge.
* @param bool $preserve_integer_keys (optional) If given, integer keys will be preserved and merged instead of appended.
* @return array The merged array.
* @copyright Copyright 2001 - 2013 Drupal contributors. License: GPL-2.0+. Drupal is a registered trademark of Dries Buytaert.
*/
public static function merge_deep_array( array $arrays, $preserve_integer_keys = false ) {
$result = [];
foreach ( $arrays as $array ) {
if ( ! is_array( $array ) ) {
continue; }
foreach ( $array as $key => $value ) {
// Renumber integer keys as array_merge_recursive() does unless
// $preserve_integer_keys is set to TRUE. Note that PHP automatically
// converts array keys that are integer strings (e.g., '1') to integers.
if ( is_integer( $key ) && ! $preserve_integer_keys ) {
$result[] = $value;
} elseif ( isset( $result[ $key ] ) && is_array( $result[ $key ] ) && is_array( $value ) ) {
// recurse when both values are arrays.
$result[ $key ] = self::merge_deep_array( [ $result[ $key ], $value ], $preserve_integer_keys );
} else {
// otherwise, use the latter value, overriding any previous value.
$result[ $key ] = $value;
}
}
}
return $result;
}
/**
* Convert array of html attributes to string.
*
* @param array $data attributes.
* @return string
* @since untagged
*/
public static function build_html_attributes( $data ) {
$result = '';
foreach ( $data as $_html_attr => $_values ) {
if ( 'style' === $_html_attr ) {
$_style_values_string = '';
foreach ( $_values as $_style_attr => $_style_values ) {
if ( is_array( $_style_values ) ) {
$_style_values_string .= $_style_attr . ': ' . implode( ' ', array_filter( $_style_values ) ) . ';';
} else {
$_style_values_string .= $_style_attr . ': ' . $_style_values . ';';
}
}
$result .= " style=\"$_style_values_string\"";
} else {
if ( is_array( $_values ) ) {
$_values_string = esc_attr( implode( ' ', array_filter( $_values ) ) );
} else {
$_values_string = esc_attr( $_values );
}
if ( '' !== $_values_string ) {
$result .= " $_html_attr=\"$_values_string\"";
}
}
}
return $result;
}
/**
* Get inline asset.
*
* @param string $content existing content.
* @return string $content
*/
public static function get_inline_asset( $content ) {
// WP Fastest Cache Premium: "Render Blocking Js" feature.
$content = ltrim( $content );
if ( class_exists( 'WpFastestCache', false )
&& '<script' === substr( $content, 0, 7 ) ) {
$content = substr_replace( $content, '<script data-wpfc-render="false"', 0, 7 );
}
if ( Advanced_Ads_Checks::active_autoptimize() || Advanced_Ads_Checks::active_wp_rocket() ) {
return '<!--noptimize-->' . $content . '<!--/noptimize-->';
}
return $content;
}
/**
* Get nested ads of an ad or a group.
*
* @param string $id Id.
* @param string $type Type (placement, ad or group).
*
* @return array of Ad objects.
*/
public static function get_nested_ads( $id, $type ) {
$result = [];
$id = absint( $id );
// Early bail!!
if ( $id < 1 ) {
return $result;
}
switch ( $type ) {
// No idea if this is intentional fall through
case 'placement':
$placements = wp_advads_get_placements();
$item = $placements[ $id ]->get_item();
if ( ! empty( $item ) ) {
$item = explode( '_', $item );
if ( isset( $item[1] ) ) {
return self::get_nested_ads( $item[1], $item[0] );
}
}
case 'ad':
$ad = wp_advads_get_ad( $id );
$result[] = $ad;
if ( $ad->is_type( 'group' ) && ! empty( $ad->get_prop( 'group_id' ) ) ) {
$result = array_merge( $result, self::get_nested_ads( $ad->get_prop( 'group_id' ), 'group' ) );
}
break;
case 'group':
$group = wp_advads_get_group( $id );
$ads = $group->get_ads();
foreach ( $ads as $ad ) {
$result = array_merge( $result, self::get_nested_ads( $ad->get_id(), 'ad' ) );
}
break;
}
return $result;
}
/**
* Maybe translate a capability to a set of roles.
*
* @param string/array $roles_or_caps A set of roles or capabilities.
* @return array $roles A list of roles.
*/
public static function maybe_translate_cap_to_role( $roles_or_caps ) {
global $wp_roles;
$roles_or_caps = (array) $roles_or_caps;
$roles = [];
foreach ( $roles_or_caps as $cap ) {
if ( $wp_roles->is_role( $cap ) ) {
$roles[] = $cap;
continue;
}
foreach ( $wp_roles->roles as $id => $role ) {
if ( isset( $role['capabilities'][ $cap ] ) ) {
$roles[] = $id;
}
}
}
return array_unique( $roles );
}
/**
* Check if the page is loaded in an iframe.
*
* @return bool
*/
public static function is_iframe() {
if ( is_customize_preview() ) {
return true;
}
if ( self::is_elementor_preview_or_edit() ) {
return true;
}
return false;
}
/**
* Check if the Elementor preview mode is used.
*
* @deprecated
*
* @return bool
*/
private static function is_elementor_preview_or_edit() {
if ( class_exists( '\Elementor\Plugin' ) ) {
try {
$refl_plugin = new ReflectionClass( '\Elementor\Plugin' );
if ( $refl_plugin->hasMethod( 'instance' ) ) {
$refl_instance_method = $refl_plugin->getMethod( 'instance' );
if ( $refl_instance_method->isPublic() && $refl_instance_method->isStatic() ) {
if ( class_exists( '\Elementor\Preview' ) && $refl_plugin->hasProperty( 'preview' ) ) {
$preview_property = new ReflectionProperty( '\Elementor\Plugin', 'preview' );
if ( $preview_property->isPublic() && ! $preview_property->isStatic() ) {
if ( method_exists( '\Elementor\Preview', 'is_preview_mode' )
&& \Elementor\Plugin::$instance->preview->is_preview_mode() ) {
return true;
}
}
}
if ( class_exists( '\Elementor\Editor' ) && $refl_plugin->hasProperty( 'editor' ) ) {
$editor_property = new ReflectionProperty( '\Elementor\Plugin', 'editor' );
if ( $editor_property->isPublic() && ! $editor_property->isStatic() ) {
if ( method_exists( '\Elementor\Editor', 'is_edit_mode' )
&& \Elementor\Plugin::$instance->editor->is_edit_mode() ) {
return true;
}
}
}
}
}
} catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// not much we can do here.
}
}
return false;
}
/**
* Get DateTimeZone object for the WP installation
*
* @return DateTimeZone DateTimeZone object.
*/
public static function get_wp_timezone() {
static $date_time_zone;
if ( ! is_null( $date_time_zone ) ) {
return $date_time_zone;
}
// wp_timezone() is available since WordPress 5.3.0.
if ( function_exists( 'wp_timezone' ) ) {
$date_time_zone = wp_timezone();
return $date_time_zone;
}
$time_zone = get_option( 'timezone_string' );
// no timezone string but gmt offset.
if ( empty( $time_zone ) ) {
$time_zone = get_option( 'gmt_offset' );
// gmt + x but not prefixed with a "+".
if ( preg_match( '/^\d/', $time_zone ) ) {
$time_zone = '+' . $time_zone;
}
}
$date_time_zone = new DateTimeZone( $time_zone );
return $date_time_zone;
}
/**
* Get literal expression of timezone.
*
* @return string Human readable timezone name.
*/
public static function get_timezone_name() {
$time_zone = self::get_wp_timezone()->getName();
if ( 'UTC' === $time_zone ) {
return 'UTC+0';
}
if ( strpos( $time_zone, '+' ) === 0 || strpos( $time_zone, '-' ) === 0 ) {
return 'UTC' . $time_zone;
}
/* translators: timezone name */
return sprintf( __( 'time of %s', 'advanced-ads' ), $time_zone );
}
}

View File

@@ -0,0 +1,515 @@
<?php // phpcs:ignore WordPress.Files.FileName
use AdvancedAds\Utilities\WordPress;
/**
* Visitor conditions under which to (not) show an ad
*
* @since 1.5.4
*/
class Advanced_Ads_Visitor_Conditions {
/**
* Instance of Advanced_Ads_Visitor_Conditions
*
* @var Advanced_Ads_Visitor_Conditions
*/
protected static $instance;
/**
* Registered visitor conditions
*
* @var array $conditions
*/
public $conditions;
/**
* Start of name in form elements
*/
const FORM_NAME = 'advanced_ad[visitors]';
/**
* Advanced_Ads_Visitor_Conditions constructor
*/
public function __construct() {
// register conditions.
$this->conditions = apply_filters(
'advanced-ads-visitor-conditions',
[
'mobile' => [
// type of the condition.
'label' => __( 'Device', 'advanced-ads' ),
'metabox' => [ 'Advanced_Ads_Visitor_Conditions', 'mobile_is_or_not' ], // callback to generate the metabox.
'check' => [ 'Advanced_Ads_Visitor_Conditions', 'check_device' ], // callback for frontend check.
'helplink' => 'https://wpadvancedads.com/manual/display-ads-either-on-mobile-or-desktop/?utm_source=advanced-ads&utm_medium=link&utm_campaign=condition-device',
'device_types' => [
'mobile' => [
'id' => 'mobile',
'label' => _x( 'Mobile', 'Device condition', 'advanced-ads' ),
],
'tablet' => [
'id' => 'tablet',
'label' => _x( 'Tablet', 'Device condition', 'advanced-ads' ),
],
'desktop' => [
'id' => 'desktop',
'label' => _x( 'Desktop', 'Device condition', 'advanced-ads' ),
],
],
],
'loggedin' => [
'label' => __( 'logged-in visitor', 'advanced-ads' ),
'description' => __( 'Whether the visitor has to be logged in or not in order to see the ad.', 'advanced-ads' ),
'metabox' => [ 'Advanced_Ads_Visitor_Conditions', 'metabox_is_or_not' ], // callback to generate the metabox.
'check' => [ 'Advanced_Ads_Visitor_Conditions', 'check_logged_in' ], // callback for frontend check.
'helplink' => 'https://wpadvancedads.com/manual/logged-in-visitors/?utm_source=advanced-ads&utm_medium=link&utm_campaign=condition-logged-in-visitors',
'passive_info' => [
'hash_fields' => null,
'remove' => 'login',
'function' => 'is_user_logged_in',
],
],
]
);
}
/**
* Load instance of Advanced_Ads_Visitor_Conditions
*
* @return Advanced_Ads_Visitor_Conditions
*/
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;
}
/**
* Get the conditions array alphabetically by label
*
* @since 1.8.12
*/
public function get_conditions() {
uasort( $this->conditions, [ WordPress::class, 'sort_array_by_label' ] );
return $this->conditions;
}
/**
* Callback to render the mobile condition using the "is not" condition
*
* @param array $options options of the condition.
* @param int $index index of the condition.
* @param string $form_name name of the form, falls back to class constant.
*/
public static function mobile_is_or_not( $options, $index = 0, $form_name = '' ) {
if ( ! isset( $options['type'] ) || '' === $options['type'] ) {
return;
}
$type_options = self::get_instance()->conditions;
if ( ! isset( $type_options[ $options['type'] ] ) ) {
return;
}
// options.
$operator = isset( $options['operator'] ) ? $options['operator'] : 'is';
// convert previous binary option to device selector.
if ( ! array_key_exists( 'value', $options ) ) {
$options['value'] = 'is' === $operator ? [ 'tablet', 'mobile' ] : [ 'desktop' ];
$operator = 'is';
}
$type_options[ $options['type'] ]['device_types'] = array_map(
function ( $device_type ) use ( $options ) {
$device_type['checked'] = in_array( $device_type['id'], $options['value'], true );
return $device_type;
},
$type_options[ $options['type'] ]['device_types']
);
// form name basis.
$name = self::get_form_name_with_index( $form_name, $index );
include ADVADS_ABSPATH . 'admin/views/conditions/condition-device.php';
}
/**
* Callback to display the "is not" condition
*
* @param array $options options of the condition.
* @param int $index index of the condition.
* @param string $form_name name of the form, falls back to class constant.
*/
public static function metabox_is_or_not( $options, $index = 0, $form_name = '' ) {
if ( ! isset( $options['type'] ) || '' === $options['type'] ) {
return;
}
$type_options = self::get_instance()->conditions;
if ( ! isset( $type_options[ $options['type'] ] ) ) {
return;
}
// form name basis.
$name = self::get_form_name_with_index( $form_name, $index );
$operator = isset( $options['operator'] ) ? $options['operator'] : 'is';
include ADVADS_ABSPATH . 'admin/views/conditions/condition-is-or-not.php';
}
/**
* Callback to display the any condition based on a number
*
* @param array $options options of the condition.
* @param int $index index of the condition.
* @param string $form_name name of the form, falls back to class constant.
*/
public static function metabox_number( $options, $index = 0, $form_name = '' ) {
if ( ! isset( $options['type'] ) || '' === $options['type'] ) {
return;
}
$type_options = self::get_instance()->conditions;
if ( ! isset( $type_options[ $options['type'] ] ) ) {
return;
}
// form name basis.
$name = self::get_form_name_with_index( $form_name, $index );
// options.
$value = isset( $options['value'] ) ? $options['value'] : 0;
$operator = isset( $options['operator'] ) ? $options['operator'] : 'is_equal';
include ADVADS_ABSPATH . 'admin/views/conditions/condition-number.php';
}
/**
* Callback to display the any condition based on a number
*
* @param array $options options of the condition.
* @param int $index index of the condition.
* @param string $form_name name of the form, falls back to class constant.
*/
public static function metabox_string( $options, $index = 0, $form_name = '' ) {
if ( ! isset( $options['type'] ) || '' === $options['type'] ) {
return;
}
$type_options = self::get_instance()->conditions;
if ( ! isset( $type_options[ $options['type'] ] ) ) {
return;
}
// form name basis.
$name = self::get_form_name_with_index( $form_name, $index );
// options.
$value = isset( $options['value'] ) ? $options['value'] : '';
$operator = isset( $options['operator'] ) ? $options['operator'] : 'contains';
include ADVADS_ABSPATH . 'admin/views/conditions/condition-string.php';
}
/**
* Controls frontend checks for conditions
*
* @param array $options Options of the condition.
* @param bool|Ad $ad Ad instance.
*
* @return bool false, if ad cant be delivered
*/
public static function frontend_check( $options = [], $ad = false ) {
$visitor_conditions = self::get_instance()->conditions;
if ( is_array( $options ) && isset( $visitor_conditions[ $options['type'] ]['check'] ) ) {
$check = $visitor_conditions[ $options['type'] ]['check'];
} else {
return true;
}
// call frontend check callback.
if ( method_exists( $check[0], $check[1] ) ) {
return call_user_func( [ $check[0], $check[1] ], $options, $ad );
}
return true;
}
/**
* Render the list of set visisor conditions
*
* @param array $set_conditions array of existing conditions.
* @param string $list_target ID of the list with the conditions.
* @param string $form_name prefix of the form.
*/
public static function render_condition_list( array $set_conditions, $list_target = '', $form_name = '' ) {
$conditions = self::get_instance()->get_conditions();
// use default form name if not given explicitly.
// TODO: create a random form name, in case we have more than one form per page and the parameter is not given.
$form_name = ! $form_name ? self::FORM_NAME : $form_name;
include ADVADS_ABSPATH . 'admin/views/conditions/visitor-conditions-list.php';
/**
* Prepare condition form
*
* @todo if needed, allow to disable the form to add new conditions
*/
// add mockup conditions if add-ons are missing.
$pro_conditions = [];
if ( ! defined( 'AAP_VERSION' ) ) {
$pro_conditions[] = __( 'browser language', 'advanced-ads' );
$pro_conditions[] = __( 'cookie', 'advanced-ads' );
$pro_conditions[] = __( 'max. ad clicks', 'advanced-ads' );
$pro_conditions[] = __( 'max. ad impressions', 'advanced-ads' );
$pro_conditions[] = __( 'new visitor', 'advanced-ads' );
$pro_conditions[] = __( 'page impressions', 'advanced-ads' );
$pro_conditions[] = __( 'geo location', 'advanced-ads' );
$pro_conditions[] = __( 'referrer url', 'advanced-ads' );
$pro_conditions[] = __( 'user agent', 'advanced-ads' );
$pro_conditions[] = __( 'user can (capabilities)', 'advanced-ads' );
$pro_conditions[] = __( 'user role', 'advanced-ads' );
$pro_conditions[] = __( 'browser width', 'advanced-ads' );
}
asort( $pro_conditions );
// the action to call using AJAX.
$action = 'load_visitor_conditions_metabox';
$connector_default = 'and';
$empty_options = ! is_array( $set_conditions ) || ! count( $set_conditions );
include ADVADS_ABSPATH . 'admin/views/conditions/conditions-form.php';
}
/**
* Render connector option
*
* @param int $index incremental index of the options.
* @param string $value connector value.
* @param string $form_name name of the form, falls back to class constant.
*
* @return string HTML of the connector option
* @todo combine this with the same function used for Display Conditions
*
* @since 1.7.0.4
*/
public static function render_connector_option( $index, $value, $form_name ) {
$label = ( 'or' === $value ) ? __( 'or', 'advanced-ads' ) : __( 'and', 'advanced-ads' );
$name = self::get_form_name_with_index( $form_name, $index );
// create random value to identify the form field.
$rand = uniqid();
return '<input type="checkbox" name="' . $name . '[connector]' . '" value="or" id="advads-conditions-' . // phpcs:ignore
$index . '-connector-' . $rand . '"' .
checked( 'or', $value, false )
. '><label for="advads-conditions-' . $index . '-connector-' . $rand . '">' . $label . '</label>';
}
/**
* Helper function to the name of a form field.
* falls back to default
*
* @param string $form_name form name if submitted.
* @param int $index index of the condition.
*
* @return string
*/
public static function get_form_name_with_index( $form_name = '', $index = 0 ) {
return ! empty( $form_name ) ? $form_name . '[' . $index . ']' : self::FORM_NAME . '[' . $index . ']';
}
/**
* Check whether device visitor condition in frontend is true.
*
* @param array $options options of the condition.
*
* @return bool
*/
public static function check_device( $options = [] ) {
if ( ! array_key_exists( 'value', $options ) ) {
return self::check_mobile( $options );
}
$mobile_detect = new \Detection\MobileDetect();
// register callbacks to decide whether device "is".
$callbacks = array_intersect_key(
[
'mobile' => function () use ( $mobile_detect ) {
return $mobile_detect->isMobile() && ! $mobile_detect->isTablet();
},
'tablet' => function () use ( $mobile_detect ) {
return $mobile_detect->isTablet();
},
'desktop' => function () use ( $mobile_detect ) {
return ! $mobile_detect->isTablet() && ! $mobile_detect->isMobile();
},
],
array_flip( $options['value'] )
);
// Only call devices that are part of the condition.
array_walk(
$callbacks,
function ( callable &$value ) {
$value = $value();
}
);
return array_filter( $callbacks ) !== [];
}
/**
* Check mobile visitor condition in frontend
*
* @param array $options options of the condition.
* @deprecated -- Only used if new options hasn't been saved
*
* @return bool
*/
private static function check_mobile( $options ) {
if ( ! isset( $options['operator'] ) ) {
return true;
}
switch ( $options['operator'] ) {
case 'is':
if ( ! wp_is_mobile() ) {
return false;
}
break;
case 'is_not':
if ( wp_is_mobile() ) {
return false;
}
break;
}
return true;
}
/**
* Check mobile visitor condition in frontend
*
* @param array $options options of the condition.
*
* @return bool true if can be displayed
* @since 1.6.3
*/
public static function check_logged_in( $options = [] ) {
if ( ! isset( $options['operator'] ) ) {
return true;
}
switch ( $options['operator'] ) {
case 'is':
if ( ! is_user_logged_in() ) {
return false;
}
break;
case 'is_not':
if ( is_user_logged_in() ) {
return false;
}
break;
}
return true;
}
/**
* Helper for check with strings
*
* @param string $string string that is going to be checked.
* @param array $options options of this condition.
*
* @return bool true if ad can be displayed
* @since 1.6.3
*/
public static function helper_check_string( $string = '', $options = [] ) { // phpcs:ignore
if ( ! isset( $options['operator'] ) || empty( $options['value'] ) ) {
return true;
}
$operator = $options['operator'];
$string = (string) maybe_serialize( $string );
$value = (string) maybe_serialize( $options['value'] );
// check the condition by mode and bool.
$condition = true;
switch ( $operator ) {
// referrer contains string on any position.
case 'contain':
$condition = stripos( $string, $value ) !== false;
break;
// referrer does not contain string on any position.
case 'contain_not':
$condition = stripos( $string, $value ) === false;
break;
// referrer starts with the string.
case 'start':
$condition = stripos( $string, $value ) === 0;
break;
// referrer does not start with the string.
case 'start_not':
$condition = stripos( $string, $value ) !== 0;
break;
// referrer ends with the string.
case 'end':
$condition = substr( $string, - strlen( $value ) ) === $value;
break;
// referrer does not end with the string.
case 'end_not':
$condition = substr( $string, - strlen( $value ) ) !== $value;
break;
// referrer is equal to the string.
case 'match':
// strings do match, but should not or not match but should.
$condition = strcasecmp( $value, $string ) === 0;
break;
// referrer is not equal to the string.
case 'match_not':
// strings do match, but should not or not match but should.
$condition = strcasecmp( $value, $string ) !== 0;
break;
case 'regex':
case 'regex_not':
$condition = @preg_match( sprintf( '/%s/', $value ), $string ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
// If the return value is `false`, the regex is incorrect.
if ( false === $condition ) {
Advanced_Ads::log( "Advanced Ads: regular expression '$value' in visitor condition is broken." );
break;
}
if ( 'regex_not' === $operator ) {
$condition = ! $condition;
}
break;
}
return $condition;
}
}