providers = new stdClass();
}
/**
* Load all available ad providers
*
* Also, set selected as ACM_Provider $current_provider which holds all necessary configuration properties.
*/
function action_load_providers() {
$providers_dir = dirname( AD_CODE_MANAGER_FILE ) . '/src/Providers/';
$providers_files = array_diff( scandir( $providers_dir ), array( '..', '.' ) );
foreach ( $providers_files as $providers_file ) {
include_once $providers_dir . '/' . $providers_file;
$module_dir = str_replace( array( 'class-', '.php' ), '', $providers_file );
$tmp = explode( '-', $module_dir );
$class_name = '';
$slug_name = '';
$table_class_name = '';
foreach ( $tmp as $word ) {
if ( $word === 'class' ) {
continue;
}
$class_name .= ucfirst( $word ) . '_';
$slug_name .= $word . '_';
}
$table_class_name = $class_name . 'ACM_WP_List_Table';
$class_name .= 'ACM_Provider';
$slug_name = rtrim( $slug_name, '_' );
// Store class names, but don't instantiate
// We don't need them all at once
if ( class_exists( $class_name ) ) {
$this->providers->$slug_name = array(
'provider' => $class_name,
'table' => $table_class_name,
);
}
}
/**
* Filters the registered ad providers.
*
* We've already gathered a list of default Providers by scanning the ACM plugin
* directory for classes that we can use. To add a provider included via
* a different directory, use this filter.
*
* @since 0.1.3
*
* @param object $providers Object containing provider configurations, keyed by slug.
* Each provider has 'provider' (class name) and 'table' (list table class) keys.
*/
$this->providers = apply_filters( 'acm_register_provider_slug', $this->providers );
/**
* Filters the current ad provider slug.
*
* By default, the provider selected in the admin UI is used.
* Use this filter to programmatically switch to a different ad provider.
*
* @since 0.1.3
*
* @param string|false $provider The provider slug (e.g., 'doubleclick_for_publishers',
* 'google_adsense'). False if not set.
*/
$this->current_provider_slug = apply_filters( 'acm_provider_slug', $this->get_option( 'provider' ) );
// Instantiate one that we need
if ( isset( $this->providers->{$this->current_provider_slug} ) ) {
$current_provider_class = $this->providers->{$this->current_provider_slug}['provider'];
$this->current_provider = new $current_provider_class();
}
// Nothing to do without a provider
if ( ! is_object( $this->current_provider ) ) {
return;
}
/**
* Filters the whitelisted script URLs for ad codes.
*
* A security filter to control which ad code script URLs can be added in the admin.
* URLs not matching domains in this list will be rejected.
*
* @since 0.1
*
* @param array $whitelisted_urls Array of whitelisted domain names (e.g., 'googleads.g.doubleclick.net').
*/
$this->current_provider->whitelisted_script_urls = apply_filters( 'acm_whitelisted_script_urls', $this->current_provider->whitelisted_script_urls );
}
/**
* Code to run on WordPress' 'init' hook
*
* @since 0.1
*/
function action_init() {
// Allow other conditionals to be used
$this->whitelisted_conditionals = array(
'is_home',
'is_front_page',
'is_category',
'has_category',
'is_single',
'is_page',
'is_tag',
'has_tag',
);
/**
* Filters the whitelisted conditional functions.
*
* Extend the list of usable conditional functions with your own.
* These functions determine when an ad code should be displayed.
*
* @since 0.1
*
* @param array $conditionals Array of whitelisted function names
* (e.g., 'is_home', 'is_single', 'is_category').
*/
$this->whitelisted_conditionals = apply_filters( 'acm_whitelisted_conditionals', $this->whitelisted_conditionals );
/**
* Filters the default logical operator for conditionals.
*
* Determines how multiple conditionals are evaluated together.
* 'OR' means any conditional can match; 'AND' means all must match.
*
* @since 0.1
*
* @param string $operator The logical operator. Default 'OR'. Accepts 'OR' or 'AND'.
*/
$this->logical_operator = apply_filters( 'acm_logical_operator', 'OR' );
/**
* Filters the capability required to manage ads.
*
* Controls which users can access the Ad Code Manager admin interface.
*
* @since 0.1
*
* @param string $capability The capability required. Default 'manage_options'.
*/
$this->manage_ads_cap = apply_filters( 'acm_manage_ads_cap', $this->manage_ads_cap );
// Load default ad tags for provider
$this->ad_tag_ids = $this->current_provider->ad_tag_ids;
/**
* Filters the registered ad tag IDs.
*
* Extend the set of default tag IDs. Ad tag IDs are used as parameters
* for template tags, e.g., `do_action( 'acm_tag', 'my_top_leaderboard' )`.
*
* @since 0.1
*
* @param array $ad_tag_ids Array of ad tag configurations. Each tag has:
* - 'tag': (string) The tag identifier.
* - 'url_vars': (array) Variables for URL token replacement.
* - 'enable_ui_mapping': (bool) Optional. Whether tag can be mapped in admin UI.
*/
$this->ad_tag_ids = apply_filters( 'acm_ad_tag_ids', $this->ad_tag_ids );
/**
* Filters the ad code arguments/fields.
*
* Allows customisation of the fields displayed in the admin UI for ad codes.
* Useful for adding custom fields or modifying existing ones.
*
* @since 0.1
*
* @param array $ad_code_args Array of field configurations. Each field has:
* - 'key': (string) Field identifier.
* - 'label': (string) Display label.
* - 'editable': (bool) Whether field is editable.
* - 'required': (bool) Whether field is required.
* - 'type': (string) Optional. Field type (e.g., 'select').
* - 'options': (array) Optional. Options for select fields.
*/
$this->current_provider->ad_code_args = apply_filters( 'acm_ad_code_args', $this->current_provider->ad_code_args );
$this->register_acm_post_type();
// Ad tags are only run on the frontend
if ( ! is_admin() ) {
/**
* Action: acm_tag
*
* Displays an ad tag on the frontend. Use this action in your theme
* templates to output ads.
*
* Example: do_action( 'acm_tag', 'my_leaderboard' );
*
* @since 0.1
*
* @param string $tag_id The unique identifier for the ad tag to display.
*/
add_action( 'acm_tag', array( $this, 'action_acm_tag' ) );
add_filter( 'acm_output_tokens', array( $this, 'filter_output_tokens' ), 5, 3 );
}
// Load all of our registered ad codes
$this->register_ad_codes( $this->get_ad_codes() );
}
/**
* Register our custom post type to store ad codes
*
* @since 0.1
*/
function register_acm_post_type() {
$labels = array(
'name' => _x( 'Ad Codes', 'Post Type General Name', 'ad-code-manager' ),
'singular_name' => _x( 'Ad Code', 'Post Type Singular Name', 'ad-code-manager' ),
);
$args = array(
'labels' => $labels,
'public' => false,
'rewrite' => false,
);
register_post_type( $this->post_type, $args );
}
/**
* Get all ACM options
*
* @since 0.4
*/
function get_options() {
$default_provider = 'doubleclick_for_publishers';
// Make sure our default provider exists. Otherwise, the sky will fall on our head
if ( ! isset( $this->providers->$default_provider ) ) {
foreach ( $this->providers as $slug => $provider ) {
$default_provider = $slug;
break;
}
}
$defaults = array(
'provider' => $default_provider,
);
$options = get_option( 'acm_options', array() );
return array_merge( $defaults, $options );
}
/**
* Get an ACM option
*
* @since 0.4
*/
function get_option( $key ) {
$options = $this->get_options();
if ( isset( $options[ $key ] ) ) {
return $options[ $key ];
} else {
return false;
}
}
/**
* Update an ACM option
*
* @since 0.4
*/
function update_options( $new_options ) {
$options = $this->get_options();
$options = array_merge( $options, $new_options );
update_option( 'acm_options', $options );
}
/**
* Handle any Add, Edit, or Delete actions from the admin interface
* Hooks into admin ajax because it's the proper context for these sort of actions
*
* @since 0.2
*/
function handle_admin_action() {
if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_REQUEST['nonce'] ), 'acm-admin-action' ) ) {
wp_die( esc_html__( 'Doing something fishy, eh?', 'ad-code-manager' ) );
}
if ( ! current_user_can( $this->manage_ads_cap ) ) {
wp_die( esc_html__( 'You do not have the necessary permissions to perform this action', 'ad-code-manager' ) );
}
$method = isset( $_REQUEST['method'] ) ? sanitize_text_field( $_REQUEST['method'] ) : '';
$message = '';
// Depending on the method we're performing, sanitize the requisite data and do it.
switch ( $method ) {
case 'add':
case 'edit':
$id = ( isset( $_REQUEST['id'] ) ) ? absint( $_REQUEST['id'] ) : 0;
$priority = ( isset( $_REQUEST['priority'] ) ) ? absint( $_REQUEST['priority'] ) : 10;
$operator = ( isset( $_REQUEST['operator'] ) && in_array( $_REQUEST['operator'], array( 'AND', 'OR' ) ) ) ? sanitize_text_field( $_REQUEST['operator'] ) : $this->logical_operator;
$ad_code_vals = array(
'priority' => $priority,
'operator' => $operator,
);
foreach ( $this->current_provider->ad_code_args as $arg ) {
$ad_code_vals[ $arg['key'] ] = sanitize_text_field( $_REQUEST['acm-column'][ $arg['key'] ] ?? '' );
}
/**
* Filter to validate ad code data before saving.
*
* Providers can hook into this filter to perform custom validation.
* Return a WP_Error to prevent saving and display an error message.
*
* @since 0.8.0
*
* @param true|WP_Error $valid True if valid, WP_Error if validation fails.
* @param array $ad_code_vals The ad code values being saved.
* @param int $id The ad code ID (0 for new ad codes).
* @param string $method The method being performed ('add' or 'edit').
*/
$validation_result = apply_filters( 'acm_validate_ad_code', true, $ad_code_vals, $id, $method );
if ( is_wp_error( $validation_result ) ) {
if ( isset( $_REQUEST['doing_ajax'] ) && sanitize_text_field( $_REQUEST['doing_ajax'] ) ) {
die( '
' . esc_html( $validation_result->get_error_message() ) . '
' );
}
// Store the error message in a transient for display after redirect.
set_transient( 'acm_validation_error_' . get_current_user_id(), $validation_result->get_error_message(), 30 );
$message = 'validation-error';
break;
}
if ( 'add' === $method ) {
$id = $this->create_ad_code( $ad_code_vals );
} else {
$id = $this->edit_ad_code( $id, $ad_code_vals );
}
if ( is_wp_error( $id ) ) {
// We can die with an error if this is an edit/ajax request
if ( isset( $id->errors['edit-error'][0] ) ) {
die( '' . esc_html( $id->errors['edit-error'][0] ) . '
' );
} else {
$message = 'error-adding-editing-ad-code';
}
break;
}
$new_conditionals = array();
$unsafe_conditionals = $_REQUEST['acm-conditionals'] ?? array();
foreach ( $unsafe_conditionals as $index => $unsafe_conditional ) {
$index = (int) $index;
$arguments = ( isset( $_REQUEST['acm-arguments'][ $index ] ) ) ? sanitize_text_field( $_REQUEST['acm-arguments'][ $index ] ) : '';
$conditional = array(
'function' => sanitize_key( $unsafe_conditional ),
'arguments' => $arguments,
);
if ( ! empty( $conditional['function'] ) ) {
$new_conditionals[] = $conditional;
}
}
if ( 'add' === $method ) {
foreach ( $new_conditionals as $new_conditional ) {
$this->create_conditional( $id, $new_conditional );
}
$message = 'ad-code-added';
} else {
$this->edit_conditionals( $id, $new_conditionals );
$message = 'ad-code-updated';
}
$this->flush_cache();
break;
case 'delete':
$id = absint( $_REQUEST['id'] );
$this->delete_ad_code( $id );
$this->flush_cache();
$message = 'ad-code-deleted';
break;
case 'update_options':
$options = $this->get_options();
foreach ( $options as $key => $value ) {
if ( isset( $_REQUEST[ $key ] ) ) {
$options[ $key ] = sanitize_text_field( $_REQUEST[ $key ] );
}
}
$this->update_options( $options );
$message = 'options-saved';
break;
}
if ( isset( $_REQUEST['doing_ajax'] ) && sanitize_text_field( $_REQUEST['doing_ajax'] ) ) {
if ( 'edit' === $method ) {
set_current_screen( 'ad-code-manager' );
$table_class = $this->providers->{$this->current_provider_slug}['table'];
$this->wp_list_table = new $table_class();
$this->wp_list_table->prepare_items();
$new_ad_code = $this->get_ad_code( $id );
echo $this->wp_list_table->single_row( $new_ad_code );
}
} else {
// @todo support ajax and non-ajax requests
$redirect_url = add_query_arg( 'message', $message, remove_query_arg( 'message', wp_get_referer() ) );
wp_safe_redirect( $redirect_url );
exit();
}
exit;
}
/**
* Get the ad codes stored in our custom post type
*/
function get_ad_codes( $query_args = array() ) {
/**
* Filters the allowed query parameters for retrieving ad codes.
*
* Controls which query parameters can be passed to modify the ad codes query.
*
* @since 0.1
*
* @param array $allowed_params Array of allowed query parameter names. Default array( 'offset' ).
*/
$allowed_query_params = apply_filters( 'acm_allowed_get_posts_args', array( 'offset' ) );
/**
* Filters the maximum number of ad codes to retrieve.
*
* By default, queries are limited to 50 ad codes.
* Use this filter to increase or decrease the limit.
*
* @since 0.1
*
* @param int $count Maximum number of ad codes to retrieve. Default 50.
*/
$args = array(
'post_type' => $this->post_type,
'numberposts' => apply_filters( 'acm_ad_code_count', 50 ),
);
foreach ( (array) $query_args as $query_key => $query_value ) {
if ( ! in_array( $query_key, $allowed_query_params, true ) ) {
unset( $query_args[ $query_key ] );
} else {
$args[ $query_key ] = $query_value;
}
}
$ad_codes_formatted = wp_cache_get( 'ad_codes', 'acm' );
if ( false === $ad_codes_formatted ) {
// Store an empty array when no ad codes exist so this block doesn't run on each page load
$ad_codes_formatted = array();
$ad_codes = get_posts( $args );
foreach ( $ad_codes as $ad_code_cpt ) {
$provider_url_vars = array();
foreach ( $this->current_provider->ad_code_args as $arg ) {
$provider_url_vars[ $arg['key'] ] = get_post_meta( $ad_code_cpt->ID, $arg['key'], true );
}
$priority = get_post_meta( $ad_code_cpt->ID, 'priority', true );
$priority = ( ! empty( $priority ) ) ? (int) $priority : 10;
$operator = get_post_meta( $ad_code_cpt->ID, 'operator', true );
$operator = ( ! empty( $operator ) ) ? esc_html( $operator ) : $this->logical_operator;
$ad_codes_formatted[] = array(
'conditionals' => $this->get_conditionals( $ad_code_cpt->ID ),
'url_vars' => $provider_url_vars,
'priority' => $priority,
'operator' => $operator,
'post_id' => $ad_code_cpt->ID,
);
}
wp_cache_add( 'ad_codes', $ad_codes_formatted, 'acm', 3600 );
}
return $ad_codes_formatted;
}
/**
* Get a single ad code
*
* @param int $post_id Post ID for the ad code that we want
* @return array|bool $ad_code Ad code representation of the data
*/
function get_ad_code( $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
$provider_url_vars = array();
foreach ( $this->current_provider->ad_code_args as $arg ) {
$provider_url_vars[ $arg['key'] ] = get_post_meta( $post->ID, $arg['key'], true );
}
$priority = get_post_meta( $post_id, 'priority', true );
$priority = ( ! empty( $priority ) ) ? (int) $priority : 10;
$operator = get_post_meta( $post_id, 'operator', true );
$operator = ( ! empty( $operator ) ) ? esc_html( $operator ) : $this->logical_operator;
return array(
'conditionals' => $this->get_conditionals( $post->ID ),
'url_vars' => $provider_url_vars,
'priority' => $priority,
'operator' => $operator,
'post_id' => $post->ID,
);
}
/**
* Flush cache
*/
function flush_cache() {
wp_cache_delete( 'ad_codes', 'acm' );
}
/**
* Get the conditional values for an ad code
*/
function get_conditionals( $ad_code_id ) {
$conditionals = get_post_meta( $ad_code_id, 'conditionals', true );
if ( empty( $conditionals ) ) {
$conditionals = array();
}
return $conditionals;
}
/**
* Create a new ad code in the database
*
* @uses register_ad_code()
*
* @param array $ad_code
* @return int|false|WP_Error post_id or false
*/
function create_ad_code( $ad_code = array() ) {
$titles = array();
foreach ( $this->current_provider->ad_code_args as $arg ) {
// We shouldn't create an ad code if any of required fields are not set.
if ( ! isset( $ad_code[ $arg['key'] ] ) && $arg['required'] === true ) {
return new WP_Error();
}
$titles[] = $ad_code[ $arg['key'] ];
}
$acm_post = array(
'post_title' => implode( '-', $titles ),
'post_status' => 'publish',
'comment_status' => 'closed',
'ping_status' => 'closed',
'post_type' => $this->post_type,
);
if ( ! is_wp_error( $acm_inserted_post_id = wp_insert_post( $acm_post, true ) ) ) {
foreach ( $this->current_provider->ad_code_args as $arg ) {
update_post_meta( $acm_inserted_post_id, $arg['key'], $ad_code[ $arg['key'] ] );
}
update_post_meta( $acm_inserted_post_id, 'priority', $ad_code['priority'] );
update_post_meta( $acm_inserted_post_id, 'operator', $ad_code['operator'] );
$this->flush_cache();
return $acm_inserted_post_id;
}
return false;
}
/**
* Update an existing ad code
*/
function edit_ad_code( $ad_code_id, $ad_code = array() ) {
foreach ( $this->current_provider->ad_code_args as $arg ) {
// If a required argument is not set, we return an error message with the missing parameter
if ( ! isset( $ad_code[ $arg['key'] ] ) && $arg['required'] === true ) {
return new WP_Error( 'edit-error', 'Error updating ad code, a parameter for ' . esc_html( $arg['key'] ) . ' is required.' );
}
}
if ( 0 !== $ad_code_id ) {
foreach ( $this->current_provider->ad_code_args as $arg ) {
update_post_meta( $ad_code_id, $arg['key'], $ad_code[ $arg['key'] ] );
}
update_post_meta( $ad_code_id, 'priority', $ad_code['priority'] );
update_post_meta( $ad_code_id, 'operator', $ad_code['operator'] );
}
$this->flush_cache();
return $ad_code_id;
}
/**
* Delete an existing ad code
*/
function delete_ad_code( $ad_code_id ) {
if ( 0 !== $ad_code_id && $this->post_type === get_post_type( $ad_code_id ) ) {
wp_delete_post( $ad_code_id, true ); // Force delete post.
$this->flush_cache();
return true;
}
}
/**
* Create conditional.
*
* @param int $ad_code_id ID of our CPT post.
* @param array $conditional Conditional to add.
* @return bool
*/
function create_conditional( $ad_code_id, $conditional ) {
if ( 0 !== $ad_code_id && ! empty( $conditional ) ) {
$existing_conditionals = get_post_meta( $ad_code_id, 'conditionals', true );
if ( ! is_array( $existing_conditionals ) ) {
$existing_conditionals = array();
}
$existing_conditionals[] = array(
'function' => $conditional['function'],
'arguments' => explode( ';', $conditional['arguments'] ),
);
return update_post_meta( $ad_code_id, 'conditionals', $existing_conditionals );
}
return false;
}
/**
* Update all conditionals for ad code.
*
* @since 0.2.0
*
* @param int $ad_code_id ID of our CPT post.
* @param array $conditionals Conditionals to edit.
* @return bool
*/
function edit_conditionals( $ad_code_id, $conditionals = array() ) {
if ( 0 !== $ad_code_id && ! empty( $conditionals ) ) {
$new_conditionals = array();
foreach ( $conditionals as $conditional ) {
if ( '' == $conditional['function'] ) {
continue;
}
$new_conditionals[] = array(
'function' => $conditional['function'],
'arguments' => (array) $conditional['arguments'],
);
}
return update_post_meta( $ad_code_id, 'conditionals', $new_conditionals );
} elseif ( 0 !== $ad_code_id ) {
return update_post_meta( $ad_code_id, 'conditionals', array() );
}
}
/**
* Hook in our submenu page to the navigation
*/
public function action_admin_menu() {
$hook = add_options_page(
__( 'Ad Code Manager', 'ad-code-manager' ),
__( 'Ad Code Manager', 'ad-code-manager' ),
$this->manage_ads_cap,
$this->plugin_slug,
array( $this, 'admin_view_controller' )
);
add_action( 'load-' . $hook, array( $this, 'action_load_ad_code_manager' ) );
}
/**
* Instantiate the List Table and handle our bulk actions on the load of the page.
*
* @since 0.2.2
*/
function action_load_ad_code_manager() {
// Instantiate this list table
$table_class = $this->providers->{$this->current_provider_slug}['table'];
$this->wp_list_table = new $table_class();
// Handle any bulk action requests
if ( 'delete' === $this->wp_list_table->current_action() ) {
check_admin_referer( 'acm-bulk-action', 'bulk-action-nonce' );
$ad_code_ids = array_map( 'intval', $_REQUEST['ad-codes'] ?? array() );
foreach ( $ad_code_ids as $ad_code_id ) {
$this->delete_ad_code( $ad_code_id );
}
$redirect_url = add_query_arg( 'message', 'ad-codes-deleted', remove_query_arg( 'message', wp_get_referer() ) );
wp_safe_redirect( $redirect_url );
exit;
}
}
/**
* Print the admin interface for managing the ad codes.
*/
function admin_view_controller() {
// Check for edit action.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified in render_edit_page.
if ( isset( $_GET['action'] ) && 'edit' === $_GET['action'] && isset( $_GET['id'] ) ) {
$this->render_edit_page();
return;
}
require_once dirname( AD_CODE_MANAGER_FILE ) . '/views/ad-code-manager.tpl.php';
}
/**
* Render the dedicated edit page for an ad code.
*
* @since 0.10.0
*/
function render_edit_page() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a view, nonce verified on form submission.
$ad_code_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
$ad_code = $this->get_ad_code( $ad_code_id );
// Handle invalid ID gracefully.
if ( ! $ad_code ) {
wp_die(
esc_html__( 'Invalid ad code ID.', 'ad-code-manager' ),
esc_html__( 'Error', 'ad-code-manager' ),
array( 'back_link' => true )
);
}
require_once dirname( AD_CODE_MANAGER_FILE ) . '/views/edit-ad-code.tpl.php';
}
/**
* Register a custom widget to display ad zones
*/
function register_widget() {
register_widget( 'ACM_Ad_Zones' );
}
/**
* Register scripts and styles
*/
function register_scripts_and_styles() {
global $pagenow;
// Only load this on the proper page
if ( 'options-general.php' !== $pagenow || ! isset( $_GET['page'] ) || $_GET['page'] != $this->plugin_slug ) {
return;
}
wp_enqueue_style( 'acm-style', plugins_url( '/', AD_CODE_MANAGER_FILE ) . '/acm.css', array(), AD_CODE_MANAGER_VERSION );
}
/**
* Register an ad tag with the plugin so it can be used
* on the frontend of the site
*
* @since 0.1
*
* @param string $tag Ad tag for this instance of code
* @param string $url Script URL for ad code
* @param array $conditionals WordPress-style conditionals for where this code should be displayed
* @param int $priority What priority this registration runs at
* @param array $url_vars Replace tokens in $script with these values
* @param int $priority Priority of the ad code in comparison to others
* @return bool|WP_Error $success Whether we were successful in registering the ad tag
*/
function register_ad_code( $tag, $url, $conditionals = array(), $url_vars = array(), $priority = 10, $operator = false ) {
// Run $url aganist a whitelist to make sure it's a safe URL
if ( ! $this->validate_script_url( $url ) ) {
return;
}
// @todo Sanitize the conditionals against our possible set of conditionals so that users
// can't just run arbitrary functions. These are whitelisted on execution of the ad code so we're fine for now
// @todo Sanitize all of the other input
// Make sure our priority is an integer
if ( ! is_int( $priority ) ) {
$priority = 10;
}
// Make sure our operator is 'OR' or 'AND'
if ( ! $operator || ! in_array( $operator, array( 'AND', 'OR' ) ) ) {
$operator = $this->logical_operator;
}
// Save the ad code to our set of ad codes
$this->ad_codes[ $tag ][] = array(
'url' => $url,
'priority' => $priority,
'operator' => $operator,
'conditionals' => $conditionals,
'url_vars' => $url_vars,
);
}
/**
* Register an array of ad tags with the plugin
*
* @since 0.1
*
* @param array $ad_codes An array of ad tags
*/
function register_ad_codes( $ad_codes = array() ) {
if ( empty( $ad_codes ) ) {
return;
}
foreach ( (array) $ad_codes as $key => $ad_code ) {
$default = array(
'tag' => '',
'url' => '',
'conditionals' => array(),
'url_vars' => array(),
'priority' => 10,
'operator' => $this->logical_operator,
);
$ad_code = array_merge( $default, $ad_code );
foreach ( (array) $this->ad_tag_ids as $default_tag ) {
/**
* 'enable_ui_mapping' is a special argument which means this ad tag can be
* mapped with ad codes through the admin interface. If that's the case, we
* want to make sure those ad codes are only registered with the tag.
*/
if ( isset( $default_tag['enable_ui_mapping'] ) && $default_tag['tag'] != $ad_code['url_vars']['tag'] ) {
continue;
}
$ad_code['priority'] = $ad_code['priority'] === '' ? 10 : (int) $ad_code['priority']; // make sure priority is int, if it's unset, we set it to 10
// Make sure our operator is 'OR' or 'AND'
if ( ! $ad_code['operator'] || ! in_array( $ad_code['operator'], array( 'AND', 'OR' ) ) ) {
$operator = $this->logical_operator;
}
/**
* Filters the default URL for an ad code.
*
* If you don't specify a URL for your ad code when registering it in
* the WordPress admin or at a code level, you can apply it with this filter.
*
* @since 0.1
*
* @param string $url The ad code URL. May be empty.
*/
$this->register_ad_code( $default_tag['tag'], apply_filters( 'acm_default_url', $ad_code['url'] ), $ad_code['conditionals'], array_merge( $default_tag['url_vars'], $ad_code['url_vars'] ), $ad_code['priority'], $ad_code['operator'] );
}
}
}
/**
* Return the ad code for an ad ID.
*
* @since 0.6.0
*
* @param string $tag_id Unique ID for the ad tag
* @return string The ad code. May be an empty string.
*/
function get_acm_tag( $tag_id ): string {
/**
* Filters whether to disable ad rendering.
*
* By default, ads are disabled on post previews to prevent ad tracking issues.
*
* @since 0.1
*
* @param bool $disable Whether to disable ad rendering. Default is the result of is_preview().
*/
if ( apply_filters( 'acm_disable_ad_rendering', is_preview() ) ) {
return '';
}
$code_to_display = $this->get_matching_ad_code( $tag_id );
// get_matching_ad_code can return an empty value.
if ( empty( $code_to_display ) ) {
return '';
}
// Run $url against a safelist to make sure it's a safe URL.
if ( ! $this->validate_script_url( $code_to_display['url'] ) ) {
return '';
}
/**
* Filters the output HTML template for an ad tag.
*
* Support multiple ad formats (e.g., JavaScript ad tags, or simple HTML tags)
* by adjusting the HTML rendered for a given ad tag.
*
* @since 0.1
*
* @param string $output_html The HTML template with tokens (e.g., '%url%', '%width%').
* @param string $tag_id The ad tag ID being rendered.
*/
$output_html = apply_filters( 'acm_output_html', $this->current_provider->output_html, $tag_id );
// Parse the output and replace any tokens we have left. But first, load the script URL.
$output_html = str_replace( '%url%', $code_to_display['url'], $output_html );
/**
* Filters the output tokens for an ad tag.
*
* Tokens are placeholders in the output HTML that get replaced with actual values.
* Register custom tokens depending on your setup.
*
* @since 0.1
*
* @param array $output_tokens Associative array of tokens and their replacement values.
* Keys should include % (e.g., '%width%' => '300').
* @param string $tag_id The ad tag ID being rendered.
* @param array $code_to_display The matching ad code configuration.
*/
$output_tokens = apply_filters( 'acm_output_tokens', $this->current_provider->output_tokens, $tag_id, $code_to_display );
foreach ( (array) $output_tokens as $token => $val ) {
$output_html = str_replace( $token, esc_attr( $val ), $output_html );
}
/**
* Filters the output HTML after token replacement.
*
* Use this filter for final modifications to the ad HTML after all tokens
* have been processed. Useful for adding wrappers or final adjustments.
*
* @since 0.2
*
* @param string $output_html The processed HTML with all tokens replaced.
* @param string $tag_id The ad tag ID being rendered.
*/
$output_html = apply_filters( 'acm_output_html_after_tokens_processed', $output_html, $tag_id );
/**
* Configuration filter: acm_wrapper_classes
* Filter the CSS classes applied to the ad wrapper div.
*
* @since 0.8.0
*
* @param array $classes Array of CSS class names. Default: 'acm-wrapper', 'acm-tag-{tag_id}'.
* @param string $tag_id The ad tag ID being rendered.
*/
$wrapper_classes = apply_filters(
'acm_wrapper_classes',
array( 'acm-wrapper', 'acm-tag-' . sanitize_html_class( $tag_id ) ),
$tag_id
);
// Allow disabling the wrapper by returning an empty array.
if ( ! empty( $wrapper_classes ) && is_array( $wrapper_classes ) ) {
$output_html = sprintf(
'%s
',
esc_attr( implode( ' ', array_map( 'sanitize_html_class', $wrapper_classes ) ) ),
$output_html
);
}
return $output_html;
}
/**
* Display the ad code for an ad ID.
*
* Example:
* do_action( 'acm_tag, 'your_tag_id' )
*
* @param string $tag_id Unique ID for the ad tag.
*/
public function action_acm_tag( $tag_id ): void {
echo $this->get_acm_tag( $tag_id );
}
/**
* Of all the ad codes registered, get the one that matches our current context
*
* @since 0.4
*/
public function get_matching_ad_code( $tag_id ) {
global $wp_query;
static $checked_ad_codes = array();
// If there aren't any ad codes, it's not worth it for us to do anything.
if ( ! isset( $this->ad_codes[ $tag_id ] ) ) {
return;
}
// This method might be expensive when there's a lot of ad codes
// So instead of executing over and over again, return cached matching ad code.
$cache_key = "acm:{$tag_id}:" . md5( serialize( $wp_query->query_vars ) );
/**
* Filters the cache expiration time for matching ad codes.
*
* Controls how long a matched ad code is cached to improve performance.
* Return false to disable object caching (request-level caching only).
*
* @since 0.4
*
* @param int|false $cache_expiration Cache time in seconds. Default 600 (10 minutes).
* Return false to disable object caching.
* @param string $tag_id The tag ID being matched.
*/
$cache_expiration = apply_filters( 'acm_matching_ad_code_cache_expiration', 600, $tag_id );
if ( false === $cache_expiration ) {
$ad_code = isset( $checked_ad_codes[ $cache_key ] ) ? $checked_ad_codes[ $cache_key ] : false;
} else {
$ad_code = wp_cache_get( $cache_key, 'acm' );
}
if ( false !== $ad_code ) {
return $ad_code;
}
/**
* Filters whether to reset post data before matching ad codes.
*
* Most conditionals check against the global $post. When matching ad codes
* inside a loop, this can result in incorrect matches. Enable this filter
* to call wp_reset_postdata() before evaluation.
*
* @since 0.4
*
* @param bool $reset Whether to reset post data. Default false for backwards compatibility.
*/
if ( apply_filters( 'acm_reset_postdata_before_match', false ) ) {
wp_reset_postdata();
}
// Run our ad codes through all of the conditionals to make sure we should
// be displaying it
$display_codes = array();
foreach ( (array) $this->ad_codes[ $tag_id ] as $ad_code ) {
/**
* Filters whether to display ad codes without conditionals.
*
* By default, ad codes without conditionals are not displayed.
* Enable this filter to show ad codes on all pages when they have
* no conditional restrictions.
*
* @since 0.1
*
* @param bool $display Whether to display ad codes without conditionals. Default false.
*/
// If the ad code doesn't have any conditionals
// we should add it to the display list
if ( empty( $ad_code['conditionals'] ) && apply_filters( 'acm_display_ad_codes_without_conditionals', false ) ) {
$display_codes[] = $ad_code;
continue;
}
// If the ad code doesn't have any conditionals
// and configuration filter acm_display_ad_codes_without_conditionals returns false
// We should should skip it
if ( empty( $ad_code['conditionals'] ) && ! apply_filters( 'acm_display_ad_codes_without_conditionals', false ) ) {
continue;
}
$include = true;
foreach ( $ad_code['conditionals'] as $conditional ) {
// If the conditional was passed as an array, then we have a complex rule.
// Otherwise, we have a function name and expect rule.
if ( is_array( $conditional ) ) {
$cond_func = $conditional['function'];
if ( ! empty( $conditional['arguments'] ) ) {
$cond_args = $conditional['arguments'];
} else {
$cond_args = array();
}
$cond_result = $conditional['result'] ?? true;
} else {
$cond_func = $conditional;
$cond_args = array();
$cond_result = true;
}
// Special trick: include 'not_' in front of the function name to reverse the result.
if ( 0 === strpos( $cond_func, 'not_' ) ) {
$cond_func = ltrim( $cond_func, 'not_' );
$cond_result = false;
}
// Don't run the conditional if the conditional function doesn't exist or isn't in our safelist.
if ( ! is_callable( $cond_func ) || ! in_array( $cond_func, $this->whitelisted_conditionals ) ) {
continue;
}
// Run our conditional and use any arguments that were passed
if ( ! empty( $cond_args ) ) {
/**
* Filters the arguments passed to conditional functions.
*
* For certain conditionals like has_tag or has_category, you might need
* to modify the arguments passed to the function.
*
* @since 0.1
*
* @param array $cond_args Array of arguments to pass to the conditional function.
* @param string $cond_func The conditional function name being called.
*/
$result = call_user_func_array( $cond_func, apply_filters( 'acm_conditional_args', $cond_args, $cond_func ) );
} else {
$result = $cond_func();
}
// If our results don't match what we need, don't include this ad code
if ( $cond_result !== $result ) {
$include = false;
} else {
$include = true;
}
// If we have matching conditional and $ad_code['operator'] equals OR just break from the loop and do not try to evaluate others
if ( $include && 'OR' === $ad_code['operator'] ) {
break;
}
// If $ad_code['operator'] equals AND and one conditional evaluates false, skip this ad code
if ( ! $include && 'AND' === $ad_code['operator'] ) {
break;
}
}
// If we're supposed to include the ad code even after we've run the conditionals,
// let's do it
if ( $include ) {
$display_codes[] = $ad_code;
}
}
// Don't do anything if we've ended up with no ad codes
if ( empty( $display_codes ) ) {
return;
}
// Prioritize the display of the ad codes based on
// the priority argument for the ad code
$prioritized_display_codes = array();
foreach ( $display_codes as $display_code ) {
$priority = $display_code['priority'];
$prioritized_display_codes[ $priority ][] = $display_code;
}
ksort( $prioritized_display_codes, SORT_NUMERIC );
$shifted_prioritized_display_codes = array_shift( $prioritized_display_codes );
$code_to_display = array_shift( $shifted_prioritized_display_codes );
if ( false === $cache_expiration ) {
$checked_ad_codes[ $cache_key ] = $code_to_display;
} else {
wp_cache_add( $cache_key, $code_to_display, 'acm', $cache_expiration );
}
return $code_to_display;
}
/**
* Filter the output tokens used in $this->action_acm_tag to include our URL vars
*
* @since 0.1
*
* @return array $output Placeholder tokens to be replaced with their values
*/
function filter_output_tokens( $output_tokens, $tag_id, $code_to_display ) {
if ( ! isset( $code_to_display['url_vars'] ) || ! is_array( $code_to_display['url_vars'] ) ) {
return $output_tokens;
}
foreach ( $code_to_display['url_vars'] as $url_var => $val ) {
$new_key = '%' . $url_var . '%';
$output_tokens[ $new_key ] = $val;
}
return $output_tokens;
}
/**
* Ensure the URL being used passes our whitelist check
*
* @since 0.1
* @see https://gist.github.com/1623788
*/
function validate_script_url( $url ) {
// If url is empty, there's nothing to validate
// Fixes issue with DFP JS
if ( empty( $url ) ) {
return true;
}
$domain = wp_parse_url( $url, PHP_URL_HOST );
// Check if we match the domain exactly
if ( in_array( $domain, $this->current_provider->whitelisted_script_urls, true ) ) {
return true;
}
$valid = false;
foreach ( $this->current_provider->whitelisted_script_urls as $whitelisted_domain ) {
$whitelisted_domain = '.' . $whitelisted_domain; // Prevent things like 'evilsitetime.com'
if ( strpos( $domain, $whitelisted_domain ) === ( strlen( $domain ) - strlen( $whitelisted_domain ) ) ) {
$valid = true;
break;
}
}
return $valid;
}
/**
* Shortcode function
*
* @since 0.2
*
* @return string HTML output. May be an empty string if no ad code is found.
*/
function shortcode( $atts ): string {
$atts = shortcode_atts(
array(
'id' => '',
),
$atts
);
$id = sanitize_text_field( $atts['id'] );
if ( empty( $id ) ) {
return '';
}
return $this->get_acm_tag( $id );
}
}