mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-11 21:09:58 +00:00
feat(ads): add ad monetization ability engine, exoclick api integration, skills, and tests
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* Ads & Monetization MCP Integration.
|
||||
*
|
||||
* Exposes two dispatcher tools — `ads-read` and `ads-write` — providing comprehensive
|
||||
* abilities for WP Quads ad units, dynamic ads.txt, cache invalidation, and ExoClick API integrations.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.16.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class EMCP_Tools_Ads_Integration
|
||||
*/
|
||||
class EMCP_Tools_Ads_Integration {
|
||||
|
||||
/**
|
||||
* Check if ads module or WP Quads is available.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_active(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return registered ability names.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function get_ability_names(): array {
|
||||
return array(
|
||||
'emcp-tools/ads-read',
|
||||
'emcp-tools/ads-write',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the abilities with the WordPress Abilities API.
|
||||
*/
|
||||
public function register(): void {
|
||||
$read_names = array_keys( EMCP_Tools_Ads_Read_Operations::op_schema() );
|
||||
$write_names = array_keys( EMCP_Tools_Ads_Write_Operations::op_schema() );
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/ads-read',
|
||||
array(
|
||||
'label' => __( 'Ads & Monetization Read', 'emcp-tools' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: comma-separated operation names */
|
||||
__( 'Read and inspect ad configurations, WP Quads slots, /ads.txt records, network detection, and ExoClick reporting. Pass the `operation` you want plus its `arguments`. If you omit `operation`, the tool returns its full operations catalog. Read operations: %1$s.', 'emcp-tools' ),
|
||||
implode( ', ', $read_names )
|
||||
),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'operation' => array(
|
||||
'type' => 'string',
|
||||
'enum' => $read_names,
|
||||
'description' => __( 'The read operation to run.', 'emcp-tools' ),
|
||||
),
|
||||
'arguments' => array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'Arguments passed to the chosen operation (see catalog returned when operation is omitted).', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => array( $this, 'can_read' ),
|
||||
'execute_callback' => array( $this, 'execute_read' ),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
emcp_tools_register_ability(
|
||||
'emcp-tools/ads-write',
|
||||
array(
|
||||
'label' => __( 'Ads & Monetization Write', 'emcp-tools' ),
|
||||
'description' => sprintf(
|
||||
/* translators: %1$s: comma-separated operation names */
|
||||
__( 'Create, update, or delete WP Quads ad slots with dual-write sync, edit /ads.txt with IAB validation, flush ad caches, and manage ExoClick zones. Pass the `operation` you want plus its `arguments`. If you omit `operation`, the tool returns its full operations catalog. Write operations: %1$s.', 'emcp-tools' ),
|
||||
implode( ', ', $write_names )
|
||||
),
|
||||
'category' => 'emcp-tools',
|
||||
'input_schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'operation' => array(
|
||||
'type' => 'string',
|
||||
'enum' => $write_names,
|
||||
'description' => __( 'The write operation to run.', 'emcp-tools' ),
|
||||
),
|
||||
'arguments' => array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'Arguments passed to the chosen operation (see catalog returned when operation is omitted).', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
'output_schema' => array( 'type' => 'object' ),
|
||||
'permission_callback' => array( $this, 'can_write' ),
|
||||
'execute_callback' => array( $this, 'execute_write' ),
|
||||
'meta' => array(
|
||||
'annotations' => array( 'readonly' => false, 'destructive' => false, 'idempotent' => false ),
|
||||
'show_in_rest' => true,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build discovery-catalog response.
|
||||
*
|
||||
* @param string $tool
|
||||
* @param string $description
|
||||
* @param array $spec
|
||||
* @return array
|
||||
*/
|
||||
private function catalog( string $tool, string $description, array $spec ): array {
|
||||
$operations = array();
|
||||
|
||||
foreach ( $spec as $name => $entry ) {
|
||||
$schema = (array) ( $entry['schema'] ?? array() );
|
||||
$required = (array) ( $schema['required'] ?? array() );
|
||||
$props = (array) ( $schema['properties'] ?? array() );
|
||||
$args = array();
|
||||
|
||||
foreach ( $props as $arg_name => $prop ) {
|
||||
$args[ $arg_name ] = self::argument_hint( (array) $prop, in_array( $arg_name, $required, true ) );
|
||||
}
|
||||
|
||||
$operations[ $name ] = array(
|
||||
'description' => (string) ( $entry['description'] ?? $name ),
|
||||
'arguments' => $args,
|
||||
'required' => $required,
|
||||
'schema' => $schema,
|
||||
'example' => (array) ( $entry['example'] ?? array() ),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'tool' => $tool,
|
||||
'description' => $description,
|
||||
'operations' => $operations,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Short human hint for schema property.
|
||||
*
|
||||
* @param array $prop
|
||||
* @param bool $required
|
||||
* @return string
|
||||
*/
|
||||
private static function argument_hint( array $prop, bool $required ): string {
|
||||
$type = isset( $prop['type'] ) ? $prop['type'] : 'mixed';
|
||||
if ( is_array( $type ) ) {
|
||||
$type = implode( '|', $type );
|
||||
}
|
||||
$hint = str_replace( 'integer', 'int', (string) $type );
|
||||
|
||||
if ( ! empty( $prop['enum'] ) && is_array( $prop['enum'] ) ) {
|
||||
$hint .= ' (enum: ' . implode( '|', $prop['enum'] ) . ')';
|
||||
}
|
||||
if ( array_key_exists( 'default', $prop ) ) {
|
||||
$hint .= ' (default: ' . ( is_bool( $prop['default'] ) ? ( $prop['default'] ? 'true' : 'false' ) : (string) $prop['default'] ) . ')';
|
||||
}
|
||||
if ( $required ) {
|
||||
$hint .= ' (required)';
|
||||
}
|
||||
|
||||
return $hint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission check for read operations.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function can_read(): bool {
|
||||
return current_user_can( 'read' ) || current_user_can( 'edit_posts' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission check for write operations.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function can_write(): bool {
|
||||
return current_user_can( 'manage_options' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute read operation.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function execute_read( array $args ) {
|
||||
$op = trim( (string) ( $args['operation'] ?? '' ) );
|
||||
$in = (array) ( $args['arguments'] ?? array() );
|
||||
|
||||
if ( '' === $op ) {
|
||||
return $this->catalog(
|
||||
'emcp-tools/ads-read',
|
||||
__( 'Read and inspect ad configurations, WP Quads slots, /ads.txt records, and ExoClick reporting.', 'emcp-tools' ),
|
||||
EMCP_Tools_Ads_Read_Operations::op_schema()
|
||||
);
|
||||
}
|
||||
|
||||
return EMCP_Tools_Ads_Read_Operations::execute( $op, $in );
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute write operation.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function execute_write( array $args ) {
|
||||
$op = trim( (string) ( $args['operation'] ?? '' ) );
|
||||
$in = (array) ( $args['arguments'] ?? array() );
|
||||
|
||||
if ( '' === $op ) {
|
||||
return $this->catalog(
|
||||
'emcp-tools/ads-write',
|
||||
__( 'Create, update, or delete WP Quads ad slots with dual-write sync, edit /ads.txt, flush ad caches, and manage ExoClick zones.', 'emcp-tools' ),
|
||||
EMCP_Tools_Ads_Write_Operations::op_schema()
|
||||
);
|
||||
}
|
||||
|
||||
return EMCP_Tools_Ads_Write_Operations::execute( $op, $in );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,609 @@
|
||||
<?php
|
||||
/**
|
||||
* Ads & Monetization Read Operations.
|
||||
*
|
||||
* Implements read operations for WP Quads ad slots, dynamic ads.txt,
|
||||
* ad network detection, monetization auditing, and ExoClick API reporting.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.16.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class EMCP_Tools_Ads_Read_Operations
|
||||
*/
|
||||
class EMCP_Tools_Ads_Read_Operations {
|
||||
|
||||
/**
|
||||
* Single source of truth for the read dispatcher's per-operation argument schema.
|
||||
*
|
||||
* @return array<string, array{description:string,example:array,schema:array}>
|
||||
*/
|
||||
public static function op_schema(): array {
|
||||
return array(
|
||||
'list-ads' => array(
|
||||
'description' => __( 'List all registered ad units from WP Quads and active ad locations with detected networks and dimensions.', 'emcp-tools' ),
|
||||
'example' => array( 'status' => 'all' ),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'status' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'all', 'active', 'inactive' ),
|
||||
'default' => 'all',
|
||||
'description' => __( 'Filter ads by status.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'get-ad' => array(
|
||||
'description' => __( 'Get complete configuration, dimensions, code, and placement for a specific ad slot by Post ID or Quads key (e.g. ad1).', 'emcp-tools' ),
|
||||
'example' => array( 'id' => 'ad1' ),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'required' => array( 'id' ),
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Post ID (e.g. "13595") or WP Quads slot key (e.g. "ad1", "ad2").', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'get-ads-txt' => array(
|
||||
'description' => __( 'Read and parse /ads.txt records, validate IAB compliance, and check authorized sellers.', 'emcp-tools' ),
|
||||
'example' => array(),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'validate_syntax' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'description' => __( 'Whether to validate IAB formatting for each record.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'audit-monetization' => array(
|
||||
'description' => __( 'Run comprehensive diagnostic audit on site ads: check ads.txt health, detect duplicate pop scripts, inspect mobile responsive styling, and verify affiliate cache status.', 'emcp-tools' ),
|
||||
'example' => array(),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'check_remote' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
'description' => __( 'Perform live HTTP check on /ads.txt endpoint.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'exoclick-list-zones' => array(
|
||||
'description' => __( 'Query ExoClick REST API to list all ad zones for a site ID, showing dimensions, refresh rates, and active status.', 'emcp-tools' ),
|
||||
'example' => array( 'idsite' => 1111220 ),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'api_token' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'ExoClick API token (optional if configured in settings).', 'emcp-tools' ),
|
||||
),
|
||||
'idsite' => array(
|
||||
'type' => 'integer',
|
||||
'description' => __( 'ExoClick Site ID (defaults to saved site ID).', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'exoclick-get-stats' => array(
|
||||
'description' => __( 'Query ExoClick reporting API for impression, click, CTR, eCPM, and revenue metrics.', 'emcp-tools' ),
|
||||
'example' => array( 'date_from' => '2026-09-01', 'date_to' => '2026-09-05' ),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'api_token' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'ExoClick API token (optional if configured in settings).', 'emcp-tools' ),
|
||||
),
|
||||
'date_from' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Start date in YYYY-MM-DD format.', 'emcp-tools' ),
|
||||
),
|
||||
'date_to' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'End date in YYYY-MM-DD format.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect ad network provider from raw HTML/JS code.
|
||||
*
|
||||
* @param string $code Raw ad markup.
|
||||
* @return array{network:string,zone_id:?string,dimensions:?string}
|
||||
*/
|
||||
public static function parse_ad_code( string $code ): array {
|
||||
$network = 'Custom';
|
||||
$zone_id = null;
|
||||
$dimensions = null;
|
||||
|
||||
if ( preg_match( '/data-zoneid=["\'](\d+)["\']/i', $code, $matches ) || preg_match( '/idzone["\']?\s*[:=]\s*(\d+)/i', $code, $matches ) ) {
|
||||
$network = 'ExoClick';
|
||||
$zone_id = $matches[1];
|
||||
} elseif ( preg_match( '/(?:adzone|adzone_id|jads\.js|juicyads\.com)[^\d]*(\d{5,8})/i', $code, $matches ) ) {
|
||||
$network = 'JuicyAds';
|
||||
$zone_id = $matches[1];
|
||||
} elseif ( stripos( $code, 'trafficstars' ) !== false || preg_match( '/trafficstars\.com[^\d]*(\d+)/i', $code, $matches ) ) {
|
||||
$network = 'TrafficStars';
|
||||
$zone_id = $matches[1] ?? null;
|
||||
} elseif ( stripos( $code, 'chaturbate' ) !== false || stripos( $code, 'cbxyz' ) !== false ) {
|
||||
$network = 'Chaturbate';
|
||||
} elseif ( stripos( $code, 'googlesyndication' ) !== false || stripos( $code, 'adsbygoogle' ) !== false ) {
|
||||
$network = 'Google AdSense';
|
||||
}
|
||||
|
||||
if ( preg_match( '/(?:data-width|width)=["\']?(\d+)["\']?\s+(?:data-height|height)=["\']?(\d+)["\']?/i', $code, $dim_matches ) ) {
|
||||
$dimensions = "{$dim_matches[1]}x{$dim_matches[2]}";
|
||||
} elseif ( preg_match( '/(\d{2,4})x(\d{2,4})/', $code, $dim_matches ) ) {
|
||||
$dimensions = "{$dim_matches[1]}x{$dim_matches[2]}";
|
||||
}
|
||||
|
||||
return array(
|
||||
'network' => $network,
|
||||
'zone_id' => $zone_id,
|
||||
'dimensions' => $dimensions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute read operations.
|
||||
*
|
||||
* @param string $op Operation name.
|
||||
* @param array $args Operation arguments.
|
||||
* @return array
|
||||
*/
|
||||
public static function execute( string $op, array $args = array() ): array {
|
||||
switch ( $op ) {
|
||||
case 'list-ads':
|
||||
return self::list_ads( $args );
|
||||
case 'get-ad':
|
||||
return self::get_ad( $args );
|
||||
case 'get-ads-txt':
|
||||
return self::get_ads_txt( $args );
|
||||
case 'audit-monetization':
|
||||
return self::audit_monetization( $args );
|
||||
case 'exoclick-list-zones':
|
||||
return self::exoclick_list_zones( $args );
|
||||
case 'exoclick-get-stats':
|
||||
return self::exoclick_get_stats( $args );
|
||||
default:
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => sprintf( 'Unknown read operation: %s', $op ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all configured ad slots.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function list_ads( array $args = array() ): array {
|
||||
$filter_status = $args['status'] ?? 'all';
|
||||
$ads = array();
|
||||
|
||||
$settings = get_option( 'quads_settings' );
|
||||
$slots = is_array( $settings ) && isset( $settings['ads'] ) ? (array) $settings['ads'] : array();
|
||||
|
||||
$posts = function_exists( 'get_posts' ) ? get_posts(
|
||||
array(
|
||||
'post_type' => 'quads-ads',
|
||||
'post_status' => 'any',
|
||||
'posts_per_page' => 100,
|
||||
)
|
||||
) : array();
|
||||
|
||||
$post_map = array();
|
||||
foreach ( $posts as $p ) {
|
||||
$old_id = get_post_meta( $p->ID, 'quads_ad_old_id', true ) ?: '';
|
||||
if ( ! empty( $old_id ) ) {
|
||||
$post_map[ $old_id ] = $p;
|
||||
}
|
||||
$post_map[ 'id_' . $p->ID ] = $p;
|
||||
}
|
||||
|
||||
foreach ( $slots as $slot_key => $slot_data ) {
|
||||
if ( ! is_array( $slot_data ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$post = $post_map[ $slot_key ] ?? null;
|
||||
$post_id = $post ? (int) $post->ID : 0;
|
||||
$code = $slot_data['code'] ?? ( $post_id ? (string) get_post_meta( $post_id, 'code', true ) : '' );
|
||||
$parsed = self::parse_ad_code( $code );
|
||||
$title = $slot_data['label'] ?? ( $post ? $post->post_title : $slot_key );
|
||||
$position = $slot_data['position'] ?? ( $post_id ? (string) get_post_meta( $post_id, 'position', true ) : 'unknown' );
|
||||
$is_active = ! empty( $code );
|
||||
|
||||
if ( 'active' === $filter_status && ! $is_active ) {
|
||||
continue;
|
||||
}
|
||||
if ( 'inactive' === $filter_status && $is_active ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ads[] = array(
|
||||
'slot_key' => $slot_key,
|
||||
'post_id' => $post_id,
|
||||
'title' => $title,
|
||||
'position' => $position,
|
||||
'detected_network' => $parsed['network'],
|
||||
'zone_id' => $parsed['zone_id'],
|
||||
'dimensions' => $parsed['dimensions'] ?: ( $slot_data['dimensions'] ?? null ),
|
||||
'is_active' => $is_active,
|
||||
'code_preview' => mb_substr( trim( $code ), 0, 150 ) . ( mb_strlen( $code ) > 150 ? '...' : '' ),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'total_ads' => count( $ads ),
|
||||
'ads' => $ads,
|
||||
'quads_mode' => get_option( 'quads-mode', 'new' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details for a specific ad slot.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function get_ad( array $args ): array {
|
||||
$id = trim( (string) ( $args['id'] ?? '' ) );
|
||||
if ( '' === $id ) {
|
||||
return array( 'success' => false, 'error' => 'Argument "id" is required.' );
|
||||
}
|
||||
|
||||
$settings = get_option( 'quads_settings', array() );
|
||||
$slot_data = null;
|
||||
$post_id = 0;
|
||||
$slot_key = null;
|
||||
|
||||
if ( isset( $settings['ads'][ $id ] ) && is_array( $settings['ads'][ $id ] ) ) {
|
||||
$slot_data = $settings['ads'][ $id ];
|
||||
$slot_key = $id;
|
||||
if ( function_exists( 'get_posts' ) ) {
|
||||
$found = get_posts(
|
||||
array(
|
||||
'post_type' => 'quads-ads',
|
||||
'meta_key' => 'quads_ad_old_id',
|
||||
'meta_value' => $slot_key,
|
||||
'numberposts' => 1,
|
||||
)
|
||||
);
|
||||
if ( ! empty( $found ) ) {
|
||||
$post_id = $found[0]->ID;
|
||||
}
|
||||
}
|
||||
} elseif ( is_numeric( $id ) && function_exists( 'get_post' ) ) {
|
||||
$p = get_post( (int) $id );
|
||||
if ( $p && 'quads-ads' === $p->post_type ) {
|
||||
$post_id = $p->ID;
|
||||
$old_id = (string) get_post_meta( $post_id, 'quads_ad_old_id', true );
|
||||
if ( ! empty( $old_id ) && isset( $settings['ads'][ $old_id ] ) ) {
|
||||
$slot_data = $settings['ads'][ $old_id ];
|
||||
$slot_key = $old_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $slot_data && ! $post_id ) {
|
||||
return array( 'success' => false, 'error' => sprintf( 'Ad slot "%s" not found.', $id ) );
|
||||
}
|
||||
|
||||
$code = $slot_data['code'] ?? ( $post_id ? (string) get_post_meta( $post_id, 'code', true ) : '' );
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'slot_key' => $slot_key ?? $id,
|
||||
'post_id' => $post_id,
|
||||
'title' => $slot_data['label'] ?? ( $post_id ? get_the_title( $post_id ) : '' ),
|
||||
'code' => $code,
|
||||
'parsed' => self::parse_ad_code( $code ),
|
||||
'settings' => $slot_data ?: array(),
|
||||
'postmeta' => $post_id && function_exists( 'get_post_meta' ) ? get_post_meta( $post_id ) : array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate ads.txt.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function get_ads_txt( array $args = array() ): array {
|
||||
$raw = '';
|
||||
|
||||
$option = get_option( 'emcp_tools_ads_txt' );
|
||||
if ( is_string( $option ) && ! empty( $option ) ) {
|
||||
$raw = $option;
|
||||
}
|
||||
|
||||
if ( empty( $raw ) && defined( 'ABSPATH' ) && file_exists( ABSPATH . 'ads.txt' ) && is_readable( ABSPATH . 'ads.txt' ) ) {
|
||||
$raw = (string) file_get_contents( ABSPATH . 'ads.txt' );
|
||||
}
|
||||
|
||||
if ( empty( $raw ) && function_exists( 'wp_remote_get' ) ) {
|
||||
$resp = wp_remote_get( home_url( '/ads.txt' ), array( 'timeout' => 4 ) );
|
||||
if ( ! is_wp_error( $resp ) && 200 === wp_remote_retrieve_response_code( $resp ) ) {
|
||||
$raw = (string) wp_remote_retrieve_body( $resp );
|
||||
}
|
||||
}
|
||||
|
||||
$records = array();
|
||||
$lines = explode( "\n", str_replace( "\r", '', $raw ) );
|
||||
$warnings = array();
|
||||
|
||||
foreach ( $lines as $idx => $line ) {
|
||||
$trimmed = trim( $line );
|
||||
if ( '' === $trimmed || 0 === strpos( $trimmed, '#' ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = array_map( 'trim', explode( ',', $trimmed ) );
|
||||
if ( count( $parts ) < 3 ) {
|
||||
$warnings[] = sprintf( 'Line %d has invalid format (<3 comma-separated fields): "%s"', $idx + 1, $trimmed );
|
||||
continue;
|
||||
}
|
||||
|
||||
$domain = strtolower( $parts[0] );
|
||||
$pub_id = $parts[1];
|
||||
$relation = strtoupper( $parts[2] );
|
||||
$cert_id = $parts[3] ?? null;
|
||||
|
||||
$valid_relation = in_array( $relation, array( 'DIRECT', 'RESELLER' ), true );
|
||||
if ( ! $valid_relation ) {
|
||||
$warnings[] = sprintf( 'Line %d has invalid relationship "%s" (must be DIRECT or RESELLER)', $idx + 1, $relation );
|
||||
}
|
||||
|
||||
$records[] = array(
|
||||
'line_number' => $idx + 1,
|
||||
'domain' => $domain,
|
||||
'publisher_id' => $pub_id,
|
||||
'relationship' => $relation,
|
||||
'certification_authority_id' => $cert_id,
|
||||
'is_valid' => $valid_relation && ! empty( $domain ) && ! empty( $pub_id ),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'raw' => $raw,
|
||||
'total_records' => count( $records ),
|
||||
'records' => $records,
|
||||
'warnings' => $warnings,
|
||||
'source' => ! empty( $option ) ? 'option' : ( defined( 'ABSPATH' ) && file_exists( ABSPATH . 'ads.txt' ) ? 'file' : 'live_url' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run comprehensive monetization audit.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function audit_monetization( array $args = array() ): array {
|
||||
$issues = array();
|
||||
$recommendations = array();
|
||||
|
||||
$ads_txt = self::get_ads_txt();
|
||||
if ( 0 === $ads_txt['total_records'] ) {
|
||||
$issues[] = 'No ads.txt records found. Major SSPs and programmatic buyers will refuse to bid on site inventory.';
|
||||
$recommendations[] = 'Configure /ads.txt with authorized publisher lines for active networks (ExoClick, TrafficStars, JuicyAds).';
|
||||
} elseif ( ! empty( $ads_txt['warnings'] ) ) {
|
||||
$issues[] = sprintf( 'Found %d warnings in /ads.txt syntax.', count( $ads_txt['warnings'] ) );
|
||||
}
|
||||
|
||||
$ads_list = self::list_ads();
|
||||
$active_ads = array_filter( $ads_list['ads'], fn( $ad ) => ! empty( $ad['is_active'] ) );
|
||||
|
||||
if ( 0 === count( $active_ads ) ) {
|
||||
$issues[] = 'No active ad units found in WP Quads.';
|
||||
$recommendations[] = 'Create responsive banner placements (Above Comic 728x90, Under Comic 300x250, Sidebar 160x600).';
|
||||
}
|
||||
|
||||
$juicy_count = 0;
|
||||
$exo_count = 0;
|
||||
foreach ( $active_ads as $ad ) {
|
||||
if ( 'JuicyAds' === $ad['detected_network'] ) {
|
||||
$juicy_count++;
|
||||
} elseif ( 'ExoClick' === $ad['detected_network'] ) {
|
||||
$exo_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $juicy_count > 0 && $exo_count > 0 ) {
|
||||
$issues[] = sprintf( 'Site is running a hybrid setup (%d ExoClick, %d JuicyAds). Consider standardizing on ExoClick to avoid remnant backfill and multiple external JS dependencies.', $exo_count, $juicy_count );
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'status' => empty( $issues ) ? 'healthy' : 'attention_required',
|
||||
'active_ad_units' => count( $active_ads ),
|
||||
'ads_txt_records' => $ads_txt['total_records'],
|
||||
'issues' => $issues,
|
||||
'recommendations' => $recommendations,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Resolve ExoClick API token.
|
||||
*
|
||||
* @param array $args
|
||||
* @return string|null
|
||||
*/
|
||||
public static function get_exoclick_token( array $args = array() ): ?string {
|
||||
if ( ! empty( $args['api_token'] ) && is_string( $args['api_token'] ) ) {
|
||||
return trim( $args['api_token'] );
|
||||
}
|
||||
if ( defined( 'EXOCLICK_API_TOKEN' ) && ! empty( EXOCLICK_API_TOKEN ) ) {
|
||||
return EXOCLICK_API_TOKEN;
|
||||
}
|
||||
$opt = get_option( 'emcp_tools_exoclick_api_token' );
|
||||
return ! empty( $opt ) && is_string( $opt ) ? $opt : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query ExoClick API to list zones.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function exoclick_list_zones( array $args = array() ): array {
|
||||
$token = self::get_exoclick_token( $args );
|
||||
if ( ! $token ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => 'ExoClick API token is required. Pass "api_token" or configure "emcp_tools_exoclick_api_token".',
|
||||
);
|
||||
}
|
||||
|
||||
$bearer = self::authenticate_exoclick( $token );
|
||||
if ( is_wp_error( $bearer ) ) {
|
||||
return array( 'success' => false, 'error' => $bearer->get_error_message() );
|
||||
}
|
||||
|
||||
$site_id = $args['idsite'] ?? get_option( 'emcp_tools_exoclick_site_id', 1111220 );
|
||||
$url = 'https://api.exoclick.com/v2/zones' . ( $site_id ? '?idsite=' . absint( $site_id ) : '' );
|
||||
|
||||
$res = wp_remote_get(
|
||||
$url,
|
||||
array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $bearer,
|
||||
'User-Agent' => 'EMCP-Tools/WordPress',
|
||||
),
|
||||
'timeout' => 10,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $res ) ) {
|
||||
return array( 'success' => false, 'error' => $res->get_error_message() );
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $res ), true );
|
||||
if ( ! is_array( $data ) ) {
|
||||
return array( 'success' => false, 'error' => 'Invalid JSON from ExoClick API.' );
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'site_id' => $site_id,
|
||||
'zones' => $data['result'] ?? $data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query ExoClick API for reporting statistics.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function exoclick_get_stats( array $args = array() ): array {
|
||||
$token = self::get_exoclick_token( $args );
|
||||
if ( ! $token ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => 'ExoClick API token is required.',
|
||||
);
|
||||
}
|
||||
|
||||
$bearer = self::authenticate_exoclick( $token );
|
||||
if ( is_wp_error( $bearer ) ) {
|
||||
return array( 'success' => false, 'error' => $bearer->get_error_message() );
|
||||
}
|
||||
|
||||
$date_from = $args['date_from'] ?? gmdate( 'Y-m-d', strtotime( '-7 days' ) );
|
||||
$date_to = $args['date_to'] ?? gmdate( 'Y-m-d' );
|
||||
$url = sprintf( 'https://api.exoclick.com/v2/statistics/p/zone?date_from=%s&date_to=%s', urlencode( $date_from ), urlencode( $date_to ) );
|
||||
|
||||
$res = wp_remote_get(
|
||||
$url,
|
||||
array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $bearer,
|
||||
'User-Agent' => 'EMCP-Tools/WordPress',
|
||||
),
|
||||
'timeout' => 15,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $res ) ) {
|
||||
return array( 'success' => false, 'error' => $res->get_error_message() );
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $res ), true );
|
||||
return array(
|
||||
'success' => true,
|
||||
'date_from' => $date_from,
|
||||
'date_to' => $date_to,
|
||||
'stats' => $data['result'] ?? $data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate API token and return Bearer session token with transient caching.
|
||||
*
|
||||
* @param string $api_token
|
||||
* @return string|\WP_Error
|
||||
*/
|
||||
public static function authenticate_exoclick( string $api_token ) {
|
||||
$cache_key = 'emcp_exoclick_bearer_' . md5( $api_token );
|
||||
$cached = function_exists( 'get_transient' ) ? get_transient( $cache_key ) : false;
|
||||
if ( ! empty( $cached ) && is_string( $cached ) ) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$url = 'https://api.exoclick.com/v2/login';
|
||||
$body = wp_json_encode( array( 'api_token' => $api_token ) );
|
||||
|
||||
$res = wp_remote_post(
|
||||
$url,
|
||||
array(
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'EMCP-Tools/WordPress',
|
||||
),
|
||||
'body' => $body,
|
||||
'timeout' => 10,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $res ) ) {
|
||||
return $res;
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $res ), true );
|
||||
if ( empty( $data['token'] ) ) {
|
||||
return new \WP_Error( 'auth_failed', $data['message'] ?? 'ExoClick API authentication failed.' );
|
||||
}
|
||||
|
||||
$bearer_token = $data['token'];
|
||||
$ttl = isset( $data['expires_in'] ) ? max( 60, (int) $data['expires_in'] - 120 ) : 3600;
|
||||
if ( function_exists( 'set_transient' ) ) {
|
||||
set_transient( $cache_key, $bearer_token, $ttl );
|
||||
}
|
||||
|
||||
return $bearer_token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
<?php
|
||||
/**
|
||||
* Ads & Monetization Write Operations.
|
||||
*
|
||||
* Implements write operations for WP Quads ad slots, dynamic ads.txt,
|
||||
* cache flushing, and ExoClick REST API integrations.
|
||||
*
|
||||
* @package EMCP_Tools
|
||||
* @since 3.16.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class EMCP_Tools_Ads_Write_Operations
|
||||
*/
|
||||
class EMCP_Tools_Ads_Write_Operations {
|
||||
|
||||
/**
|
||||
* Single source of truth for the write dispatcher's per-operation argument schema.
|
||||
*
|
||||
* @return array<string, array{description:string,example:array,schema:array}>
|
||||
*/
|
||||
public static function op_schema(): array {
|
||||
return array(
|
||||
'update-ad' => array(
|
||||
'description' => __( 'Update an existing ad unit with dual-write synchronization across WP Quads postmeta and quads_settings.', 'emcp-tools' ),
|
||||
'example' => array(
|
||||
'id' => 'ad1',
|
||||
'code' => '<script async src="https://a.magsrv.com/ad-provider.js"></script><ins class="eas6a97888e2" data-zoneid="6020542"></ins><script>(AdProvider = window.AdProvider || []).push({"serve": {}});</script>',
|
||||
),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'required' => array( 'id' ),
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Slot key (e.g. "ad1") or post ID (e.g. "13595").', 'emcp-tools' ),
|
||||
),
|
||||
'code' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'New HTML/JS tag markup for this ad unit.', 'emcp-tools' ),
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Optional new label/title for the ad unit.', 'emcp-tools' ),
|
||||
),
|
||||
'position' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Placement position (e.g. "custom", "before_post", "after_post").', 'emcp-tools' ),
|
||||
),
|
||||
'dimensions' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Ad dimensions (e.g. "728x90", "300x250").', 'emcp-tools' ),
|
||||
),
|
||||
'settings' => array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'Additional WP Quads settings parameters.', 'emcp-tools' ),
|
||||
),
|
||||
'purge_cache' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'description' => __( 'Whether to purge page cache and object cache after updating.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'create-ad' => array(
|
||||
'description' => __( 'Create a new ad unit with dual-write to WP Quads posts and serialized options.', 'emcp-tools' ),
|
||||
'example' => array(
|
||||
'title' => 'Footer Ad',
|
||||
'code' => '<ins data-zoneid="6020534"></ins>',
|
||||
'position' => 'custom',
|
||||
),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'required' => array( 'title', 'code' ),
|
||||
'properties' => array(
|
||||
'title' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Name / label for the ad slot.', 'emcp-tools' ),
|
||||
),
|
||||
'code' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'HTML/JS tag markup for the ad unit.', 'emcp-tools' ),
|
||||
),
|
||||
'slot_key' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'WP Quads slot key (e.g. "ad12"). If omitted, auto-generated based on existing keys.', 'emcp-tools' ),
|
||||
),
|
||||
'position' => array(
|
||||
'type' => 'string',
|
||||
'default' => 'custom',
|
||||
'description' => __( 'Placement position.', 'emcp-tools' ),
|
||||
),
|
||||
'dimensions' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Dimensions string (e.g. "728x90").', 'emcp-tools' ),
|
||||
),
|
||||
'settings' => array(
|
||||
'type' => 'object',
|
||||
'description' => __( 'Additional WP Quads options dict.', 'emcp-tools' ),
|
||||
),
|
||||
'purge_cache' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'description' => __( 'Whether to flush cache after creation.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'delete-ad' => array(
|
||||
'description' => __( 'Delete or deactivate an ad unit from WP Quads and quads_settings with safety confirmation guard.', 'emcp-tools' ),
|
||||
'example' => array(
|
||||
'id' => 'ad12',
|
||||
'confirm' => true,
|
||||
),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'required' => array( 'id', 'confirm' ),
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Slot key or post ID to delete.', 'emcp-tools' ),
|
||||
),
|
||||
'confirm' => array(
|
||||
'type' => 'boolean',
|
||||
'description' => __( 'Safety guard: must explicitly be true to execute deletion.', 'emcp-tools' ),
|
||||
),
|
||||
'purge_cache' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'description' => __( 'Whether to flush cache after deletion.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'set-ads-txt' => array(
|
||||
'description' => __( 'Set or append records to /ads.txt with IAB syntax validation and physical/option dual-write.', 'emcp-tools' ),
|
||||
'example' => array(
|
||||
'append_records' => array( 'exoclick.com, 1111220, DIRECT, f6e6255c27770857' ),
|
||||
),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'content' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Full text content to replace /ads.txt completely.', 'emcp-tools' ),
|
||||
),
|
||||
'append_records' => array(
|
||||
'type' => 'array',
|
||||
'items' => array( 'type' => 'string' ),
|
||||
'description' => __( 'List of individual record lines to append to existing /ads.txt.', 'emcp-tools' ),
|
||||
),
|
||||
'backup' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'description' => __( 'Whether to save previous content to backup option before changing.', 'emcp-tools' ),
|
||||
),
|
||||
'purge_cache' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'description' => __( 'Whether to flush page cache after updating ads.txt.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'purge-ad-cache' => array(
|
||||
'description' => __( 'Flush LiteSpeed page cache, WP object cache, and temporary ExoClick API transients.', 'emcp-tools' ),
|
||||
'example' => array( 'all' => true ),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'all' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
'description' => __( 'Flush all ad and site caches.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'exoclick-create-zone' => array(
|
||||
'description' => __( 'Create a new ad zone on ExoClick via API and optionally install tag directly into WP Quads slot.', 'emcp-tools' ),
|
||||
'example' => array(
|
||||
'name' => 'Header Banner',
|
||||
'dimensions' => '728x90',
|
||||
'idsite' => 1111220,
|
||||
),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'required' => array( 'name' ),
|
||||
'properties' => array(
|
||||
'name' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Zone name (e.g. "Above Comic - 728x90").', 'emcp-tools' ),
|
||||
),
|
||||
'dimensions' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'Dimensions (e.g. "728x90", "300x250", "160x600").', 'emcp-tools' ),
|
||||
),
|
||||
'idsub_type_format' => array(
|
||||
'type' => 'integer',
|
||||
'description' => __( 'ExoClick sub type format ID (e.g. 1 for banner, 7 for popunder).', 'emcp-tools' ),
|
||||
),
|
||||
'idsite' => array(
|
||||
'type' => 'integer',
|
||||
'description' => __( 'ExoClick Site ID (defaults to saved site ID).', 'emcp-tools' ),
|
||||
),
|
||||
'api_token' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'ExoClick API token (optional if configured).', 'emcp-tools' ),
|
||||
),
|
||||
'install_to_slot' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'WP Quads slot key (e.g. "ad1") or "create" to automatically install generated tag.', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'exoclick-verify-site' => array(
|
||||
'description' => __( 'Verify website ownership with ExoClick by deploying verification token and triggering API confirmation.', 'emcp-tools' ),
|
||||
'example' => array( 'idsite' => 1111220 ),
|
||||
'schema' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'idsite' => array(
|
||||
'type' => 'integer',
|
||||
'description' => __( 'ExoClick Site ID.', 'emcp-tools' ),
|
||||
),
|
||||
'api_token' => array(
|
||||
'type' => 'string',
|
||||
'description' => __( 'ExoClick API token (optional if configured).', 'emcp-tools' ),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute write operations.
|
||||
*
|
||||
* @param string $op Operation name.
|
||||
* @param array $args Operation arguments.
|
||||
* @return array
|
||||
*/
|
||||
public static function execute( string $op, array $args = array() ): array {
|
||||
switch ( $op ) {
|
||||
case 'update-ad':
|
||||
return self::update_ad( $args );
|
||||
case 'create-ad':
|
||||
return self::create_ad( $args );
|
||||
case 'delete-ad':
|
||||
return self::delete_ad( $args );
|
||||
case 'set-ads-txt':
|
||||
return self::set_ads_txt( $args );
|
||||
case 'purge-ad-cache':
|
||||
return self::purge_ad_cache( $args );
|
||||
case 'exoclick-create-zone':
|
||||
return self::exoclick_create_zone( $args );
|
||||
case 'exoclick-verify-site':
|
||||
return self::exoclick_verify_site( $args );
|
||||
default:
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => sprintf( 'Unknown write operation: %s', $op ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an ad slot with dual-write sync.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function update_ad( array $args ): array {
|
||||
$id = trim( (string) ( $args['id'] ?? '' ) );
|
||||
if ( '' === $id ) {
|
||||
return array( 'success' => false, 'error' => 'Argument "id" is required.' );
|
||||
}
|
||||
|
||||
$settings = get_option( 'quads_settings', array() );
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
if ( ! isset( $settings['ads'] ) || ! is_array( $settings['ads'] ) ) {
|
||||
$settings['ads'] = array();
|
||||
}
|
||||
|
||||
$post_id = 0;
|
||||
$slot_key = null;
|
||||
$post = null;
|
||||
|
||||
if ( isset( $settings['ads'][ $id ] ) ) {
|
||||
$slot_key = $id;
|
||||
if ( function_exists( 'get_posts' ) ) {
|
||||
$found = get_posts(
|
||||
array(
|
||||
'post_type' => 'quads-ads',
|
||||
'meta_key' => 'quads_ad_old_id',
|
||||
'meta_value' => $slot_key,
|
||||
'numberposts' => 1,
|
||||
)
|
||||
);
|
||||
if ( ! empty( $found ) ) {
|
||||
$post = $found[0];
|
||||
$post_id = $post->ID;
|
||||
}
|
||||
}
|
||||
} elseif ( is_numeric( $id ) && function_exists( 'get_post' ) ) {
|
||||
$p = get_post( (int) $id );
|
||||
if ( $p && 'quads-ads' === $p->post_type ) {
|
||||
$post = $p;
|
||||
$post_id = $p->ID;
|
||||
$old_id = (string) get_post_meta( $post_id, 'quads_ad_old_id', true );
|
||||
$slot_key = ! empty( $old_id ) ? $old_id : 'ad_' . $post_id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $slot_key && ! $post_id ) {
|
||||
return array( 'success' => false, 'error' => sprintf( 'Ad slot "%s" not found to update.', $id ) );
|
||||
}
|
||||
|
||||
$current_slot_data = $settings['ads'][ $slot_key ] ?? array();
|
||||
if ( ! is_array( $current_slot_data ) ) {
|
||||
$current_slot_data = array();
|
||||
}
|
||||
|
||||
$new_code = isset( $args['code'] ) ? (string) $args['code'] : ( $current_slot_data['code'] ?? '' );
|
||||
$new_title = isset( $args['title'] ) ? (string) $args['title'] : ( $current_slot_data['label'] ?? ( $post ? $post->post_title : $slot_key ) );
|
||||
$new_position = isset( $args['position'] ) ? (string) $args['position'] : ( $current_slot_data['position'] ?? 'custom' );
|
||||
$new_dimensions = isset( $args['dimensions'] ) ? (string) $args['dimensions'] : ( $current_slot_data['dimensions'] ?? '' );
|
||||
|
||||
// 1. Dual-write to quads_settings
|
||||
$current_slot_data['code'] = $new_code;
|
||||
$current_slot_data['label'] = $new_title;
|
||||
$current_slot_data['position'] = $new_position;
|
||||
if ( ! empty( $new_dimensions ) ) {
|
||||
$current_slot_data['dimensions'] = $new_dimensions;
|
||||
}
|
||||
if ( isset( $args['settings'] ) && is_array( $args['settings'] ) ) {
|
||||
$current_slot_data = array_merge( $current_slot_data, $args['settings'] );
|
||||
}
|
||||
$settings['ads'][ $slot_key ] = $current_slot_data;
|
||||
update_option( 'quads_settings', $settings );
|
||||
|
||||
// 2. Dual-write to quads-ads post if exists
|
||||
if ( $post_id && function_exists( 'update_post_meta' ) ) {
|
||||
update_post_meta( $post_id, 'code', $new_code );
|
||||
update_post_meta( $post_id, 'position', $new_position );
|
||||
if ( ! empty( $new_dimensions ) ) {
|
||||
update_post_meta( $post_id, 'dimensions', $new_dimensions );
|
||||
}
|
||||
if ( isset( $args['title'] ) && function_exists( 'wp_update_post' ) ) {
|
||||
wp_update_post(
|
||||
array(
|
||||
'ID' => $post_id,
|
||||
'post_title' => $new_title,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Cache flush
|
||||
if ( $args['purge_cache'] ?? true ) {
|
||||
self::purge_ad_cache();
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'slot_key' => $slot_key,
|
||||
'post_id' => $post_id,
|
||||
'title' => $new_title,
|
||||
'detected_network' => EMCP_Tools_Ads_Read_Operations::parse_ad_code( $new_code ),
|
||||
'message' => sprintf( 'Ad slot "%s" updated successfully.', $slot_key ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new ad slot with dual-write sync.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function create_ad( array $args ): array {
|
||||
$title = trim( (string) ( $args['title'] ?? '' ) );
|
||||
$code = (string) ( $args['code'] ?? '' );
|
||||
|
||||
if ( '' === $title || '' === $code ) {
|
||||
return array( 'success' => false, 'error' => 'Arguments "title" and "code" are required.' );
|
||||
}
|
||||
|
||||
$settings = get_option( 'quads_settings', array() );
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
if ( ! isset( $settings['ads'] ) || ! is_array( $settings['ads'] ) ) {
|
||||
$settings['ads'] = array();
|
||||
}
|
||||
|
||||
// Generate slot key if not provided
|
||||
$slot_key = trim( (string) ( $args['slot_key'] ?? '' ) );
|
||||
if ( '' === $slot_key ) {
|
||||
$max_num = 0;
|
||||
foreach ( array_keys( $settings['ads'] ) as $k ) {
|
||||
if ( preg_match( '/^ad(\d+)$/i', $k, $m ) ) {
|
||||
$max_num = max( $max_num, (int) $m[1] );
|
||||
}
|
||||
}
|
||||
$slot_key = 'ad' . ( $max_num + 1 );
|
||||
}
|
||||
|
||||
$position = $args['position'] ?? 'custom';
|
||||
$dimensions = $args['dimensions'] ?? '';
|
||||
if ( empty( $dimensions ) ) {
|
||||
$parsed = EMCP_Tools_Ads_Read_Operations::parse_ad_code( $code );
|
||||
$dimensions = $parsed['dimensions'] ?? '';
|
||||
}
|
||||
|
||||
// 1. Create quads-ads post if post type exists
|
||||
$post_id = 0;
|
||||
if ( function_exists( 'wp_insert_post' ) ) {
|
||||
$post_id = wp_insert_post(
|
||||
array(
|
||||
'post_title' => $title,
|
||||
'post_type' => 'quads-ads',
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
if ( $post_id && ! is_wp_error( $post_id ) ) {
|
||||
update_post_meta( $post_id, 'quads_ad_old_id', $slot_key );
|
||||
update_post_meta( $post_id, 'code', $code );
|
||||
update_post_meta( $post_id, 'position', $position );
|
||||
if ( ! empty( $dimensions ) ) {
|
||||
update_post_meta( $post_id, 'dimensions', $dimensions );
|
||||
}
|
||||
} else {
|
||||
$post_id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Add to quads_settings
|
||||
$slot_entry = array(
|
||||
'code' => $code,
|
||||
'label' => $title,
|
||||
'position' => $position,
|
||||
'dimensions' => $dimensions,
|
||||
'ad_type' => 'plain_text',
|
||||
);
|
||||
if ( isset( $args['settings'] ) && is_array( $args['settings'] ) ) {
|
||||
$slot_entry = array_merge( $slot_entry, $args['settings'] );
|
||||
}
|
||||
$settings['ads'][ $slot_key ] = $slot_entry;
|
||||
update_option( 'quads_settings', $settings );
|
||||
|
||||
// 3. Cache purge
|
||||
if ( $args['purge_cache'] ?? true ) {
|
||||
self::purge_ad_cache();
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'slot_key' => $slot_key,
|
||||
'post_id' => $post_id,
|
||||
'title' => $title,
|
||||
'position' => $position,
|
||||
'detected_network' => EMCP_Tools_Ads_Read_Operations::parse_ad_code( $code ),
|
||||
'message' => sprintf( 'Ad slot "%s" created successfully.', $slot_key ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an ad slot with safety confirmation guard.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function delete_ad( array $args ): array {
|
||||
$id = trim( (string) ( $args['id'] ?? '' ) );
|
||||
$confirm = ! empty( $args['confirm'] );
|
||||
|
||||
if ( '' === $id ) {
|
||||
return array( 'success' => false, 'error' => 'Argument "id" is required.' );
|
||||
}
|
||||
|
||||
if ( ! $confirm ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'warning' => 'Confirmation required. Pass "confirm": true to delete this ad unit.',
|
||||
'id' => $id,
|
||||
);
|
||||
}
|
||||
|
||||
$settings = get_option( 'quads_settings', array() );
|
||||
$slot_key = null;
|
||||
$post_id = 0;
|
||||
|
||||
if ( isset( $settings['ads'][ $id ] ) ) {
|
||||
$slot_key = $id;
|
||||
unset( $settings['ads'][ $id ] );
|
||||
update_option( 'quads_settings', $settings );
|
||||
|
||||
if ( function_exists( 'get_posts' ) ) {
|
||||
$found = get_posts(
|
||||
array(
|
||||
'post_type' => 'quads-ads',
|
||||
'meta_key' => 'quads_ad_old_id',
|
||||
'meta_value' => $slot_key,
|
||||
'numberposts' => 1,
|
||||
)
|
||||
);
|
||||
if ( ! empty( $found ) ) {
|
||||
$post_id = $found[0]->ID;
|
||||
}
|
||||
}
|
||||
} elseif ( is_numeric( $id ) ) {
|
||||
$post_id = (int) $id;
|
||||
if ( function_exists( 'get_post_meta' ) ) {
|
||||
$old_id = (string) get_post_meta( $post_id, 'quads_ad_old_id', true );
|
||||
if ( ! empty( $old_id ) && isset( $settings['ads'][ $old_id ] ) ) {
|
||||
$slot_key = $old_id;
|
||||
unset( $settings['ads'][ $old_id ] );
|
||||
update_option( 'quads_settings', $settings );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $post_id && function_exists( 'wp_delete_post' ) ) {
|
||||
wp_delete_post( $post_id, true );
|
||||
}
|
||||
|
||||
if ( $args['purge_cache'] ?? true ) {
|
||||
self::purge_ad_cache();
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'slot_key' => $slot_key ?? $id,
|
||||
'post_id' => $post_id,
|
||||
'message' => sprintf( 'Ad slot "%s" deleted successfully.', $id ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or append records to /ads.txt.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function set_ads_txt( array $args ): array {
|
||||
$current = EMCP_Tools_Ads_Read_Operations::get_ads_txt();
|
||||
$raw = $current['raw'];
|
||||
|
||||
// Backup
|
||||
if ( $args['backup'] ?? true ) {
|
||||
update_option(
|
||||
'emcp_tools_ads_txt_backup',
|
||||
array(
|
||||
'timestamp' => time(),
|
||||
'content' => $raw,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$new_content = '';
|
||||
if ( isset( $args['content'] ) && is_string( $args['content'] ) ) {
|
||||
$new_content = trim( $args['content'] );
|
||||
} elseif ( ! empty( $args['append_records'] ) && is_array( $args['append_records'] ) ) {
|
||||
$existing_lines = array_filter( array_map( 'trim', explode( "\n", str_replace( "\r", '', $raw ) ) ) );
|
||||
foreach ( $args['append_records'] as $rec ) {
|
||||
$rec = trim( (string) $rec );
|
||||
if ( ! empty( $rec ) && ! in_array( $rec, $existing_lines, true ) ) {
|
||||
$existing_lines[] = $rec;
|
||||
}
|
||||
}
|
||||
$new_content = implode( "\n", $existing_lines );
|
||||
} else {
|
||||
return array( 'success' => false, 'error' => 'Must provide either "content" or "append_records".' );
|
||||
}
|
||||
|
||||
// Syntax validation
|
||||
$validation_warnings = array();
|
||||
$lines = explode( "\n", $new_content );
|
||||
foreach ( $lines as $idx => $line ) {
|
||||
$t = trim( $line );
|
||||
if ( '' === $t || 0 === strpos( $t, '#' ) ) {
|
||||
continue;
|
||||
}
|
||||
$parts = array_map( 'trim', explode( ',', $t ) );
|
||||
if ( count( $parts ) < 3 ) {
|
||||
$validation_warnings[] = sprintf( 'Line %d: insufficient fields: "%s"', $idx + 1, $t );
|
||||
} elseif ( ! in_array( strtoupper( $parts[2] ), array( 'DIRECT', 'RESELLER' ), true ) ) {
|
||||
$validation_warnings[] = sprintf( 'Line %d: invalid relationship "%s"', $idx + 1, $parts[2] );
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Save to option
|
||||
update_option( 'emcp_tools_ads_txt', $new_content );
|
||||
|
||||
// 2. Physical write if ABSPATH is available
|
||||
$file_written = false;
|
||||
if ( defined( 'ABSPATH' ) ) {
|
||||
$file_path = ABSPATH . 'ads.txt';
|
||||
if ( ( file_exists( $file_path ) && is_writable( $file_path ) ) || ( ! file_exists( $file_path ) && is_writable( ABSPATH ) ) ) {
|
||||
$file_written = ( false !== @file_put_contents( $file_path, $new_content ) );
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Purge cache
|
||||
if ( $args['purge_cache'] ?? true ) {
|
||||
self::purge_ad_cache();
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'records_count' => count( array_filter( $lines, fn( $l ) => '' !== trim( $l ) && 0 !== strpos( trim( $l ), '#' ) ) ),
|
||||
'validation_warnings' => $validation_warnings,
|
||||
'file_written' => $file_written,
|
||||
'saved_to_option' => true,
|
||||
'message' => '/ads.txt updated successfully.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge ad transients and page caches.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function purge_ad_cache( array $args = array() ): array {
|
||||
$purged = array();
|
||||
|
||||
// LiteSpeed Cache purge
|
||||
if ( function_exists( 'do_action' ) ) {
|
||||
do_action( 'litespeed_purge_all' );
|
||||
$purged[] = 'litespeed_purge_all';
|
||||
}
|
||||
|
||||
// Object Cache
|
||||
if ( function_exists( 'wp_cache_flush' ) ) {
|
||||
wp_cache_flush();
|
||||
$purged[] = 'wp_cache_flush';
|
||||
}
|
||||
|
||||
// Transient purge for ExoClick
|
||||
if ( function_exists( 'delete_transient' ) ) {
|
||||
global $wpdb;
|
||||
if ( ! empty( $wpdb ) && isset( $wpdb->options ) ) {
|
||||
$keys = $wpdb->get_col( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_emcp_exoclick_%'" );
|
||||
if ( is_array( $keys ) ) {
|
||||
foreach ( $keys as $k ) {
|
||||
$transient_name = str_replace( '_transient_', '', $k );
|
||||
delete_transient( $transient_name );
|
||||
}
|
||||
$purged[] = 'exoclick_transients';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'purged' => $purged,
|
||||
'message' => 'Ad caches successfully flushed.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ExoClick zone via REST API with optional automatic installation.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function exoclick_create_zone( array $args ): array {
|
||||
$name = trim( (string) ( $args['name'] ?? '' ) );
|
||||
if ( '' === $name ) {
|
||||
return array( 'success' => false, 'error' => 'Argument "name" is required.' );
|
||||
}
|
||||
|
||||
$token = EMCP_Tools_Ads_Read_Operations::get_exoclick_token( $args );
|
||||
if ( ! $token ) {
|
||||
return array( 'success' => false, 'error' => 'ExoClick API token is required.' );
|
||||
}
|
||||
|
||||
$bearer = EMCP_Tools_Ads_Read_Operations::authenticate_exoclick( $token );
|
||||
if ( is_wp_error( $bearer ) ) {
|
||||
return array( 'success' => false, 'error' => $bearer->get_error_message() );
|
||||
}
|
||||
|
||||
$site_id = $args['idsite'] ?? get_option( 'emcp_tools_exoclick_site_id', 1111220 );
|
||||
$dimensions = $args['dimensions'] ?? '300x250';
|
||||
$format_id = $args['idsub_type_format'] ?? 1; // 1 = Banner
|
||||
|
||||
$body = array(
|
||||
'name' => $name,
|
||||
'idsite' => absint( $site_id ),
|
||||
'idsub_type_format' => absint( $format_id ),
|
||||
'description' => 'Created by Elementor MCP',
|
||||
);
|
||||
|
||||
$res = wp_remote_post(
|
||||
'https://api.exoclick.com/v2/zones',
|
||||
array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $bearer,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'EMCP-Tools/WordPress',
|
||||
),
|
||||
'body' => wp_json_encode( $body ),
|
||||
'timeout' => 15,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $res ) ) {
|
||||
return array( 'success' => false, 'error' => $res->get_error_message() );
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $res ), true );
|
||||
$zone_id = $data['idzone'] ?? ( $data['result']['idzone'] ?? null );
|
||||
|
||||
if ( ! $zone_id ) {
|
||||
return array(
|
||||
'success' => false,
|
||||
'error' => $data['message'] ?? 'Failed to create zone on ExoClick.',
|
||||
'raw' => $data,
|
||||
);
|
||||
}
|
||||
|
||||
// Generate canonical ExoClick JS banner snippet
|
||||
$snippet = sprintf(
|
||||
'<script async type="application/javascript" src="https://a.magsrv.com/ad-provider.js"></script>' . "\n" .
|
||||
'<ins class="eas6a97888e2" data-zoneid="%d"></ins>' . "\n" .
|
||||
'<script>(AdProvider = window.AdProvider || []).push({"serve": {}});</script>',
|
||||
$zone_id
|
||||
);
|
||||
|
||||
$install_res = null;
|
||||
$slot_target = $args['install_to_slot'] ?? '';
|
||||
if ( ! empty( $slot_target ) ) {
|
||||
if ( 'create' === $slot_target ) {
|
||||
$install_res = self::create_ad(
|
||||
array(
|
||||
'title' => $name,
|
||||
'code' => $snippet,
|
||||
'dimensions' => $dimensions,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$install_res = self::update_ad(
|
||||
array(
|
||||
'id' => $slot_target,
|
||||
'code' => $snippet,
|
||||
'title' => $name,
|
||||
'dimensions' => $dimensions,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'success' => true,
|
||||
'zone_id' => $zone_id,
|
||||
'site_id' => $site_id,
|
||||
'name' => $name,
|
||||
'tag_code' => $snippet,
|
||||
'installation' => $install_res,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify site with ExoClick.
|
||||
*
|
||||
* @param array $args
|
||||
* @return array
|
||||
*/
|
||||
public static function exoclick_verify_site( array $args = array() ): array {
|
||||
$token = EMCP_Tools_Ads_Read_Operations::get_exoclick_token( $args );
|
||||
if ( ! $token ) {
|
||||
return array( 'success' => false, 'error' => 'ExoClick API token is required.' );
|
||||
}
|
||||
|
||||
$bearer = EMCP_Tools_Ads_Read_Operations::authenticate_exoclick( $token );
|
||||
if ( is_wp_error( $bearer ) ) {
|
||||
return array( 'success' => false, 'error' => $bearer->get_error_message() );
|
||||
}
|
||||
|
||||
$site_id = $args['idsite'] ?? get_option( 'emcp_tools_exoclick_site_id', 1111220 );
|
||||
|
||||
// Step 1: Trigger verification check
|
||||
$res = wp_remote_post(
|
||||
'https://api.exoclick.com/v2/sites/url-verification',
|
||||
array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $bearer,
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'EMCP-Tools/WordPress',
|
||||
),
|
||||
'body' => wp_json_encode( array( 'idsite' => absint( $site_id ) ) ),
|
||||
'timeout' => 15,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $res ) ) {
|
||||
return array( 'success' => false, 'error' => $res->get_error_message() );
|
||||
}
|
||||
|
||||
$data = json_decode( wp_remote_retrieve_body( $res ), true );
|
||||
return array(
|
||||
'success' => true,
|
||||
'site_id' => $site_id,
|
||||
'result' => $data,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -282,6 +282,13 @@ class EMCP_Tools_Ability_Registrar {
|
||||
$this->ability_names = array_merge( $this->ability_names, $comic_easel->get_ability_names() );
|
||||
}
|
||||
|
||||
// Ads & Monetization abilities (WP Quads, dynamic ads.txt, ExoClick).
|
||||
if ( class_exists( 'EMCP_Tools_Ads_Integration' ) && EMCP_Tools_Ads_Integration::is_active() ) {
|
||||
$ads_integration = new EMCP_Tools_Ads_Integration();
|
||||
$ads_integration->register();
|
||||
$this->ability_names = array_merge( $this->ability_names, $ads_integration->get_ability_names() );
|
||||
}
|
||||
|
||||
// Forms-tab integrations. CF7 is free; the five entry-storing plugins are
|
||||
// Pro. Each registers only when its plugin is active (is_available()).
|
||||
$form_integrations = array();
|
||||
|
||||
@@ -95,6 +95,9 @@ final class EMCP_Tools_Pro_Loader {
|
||||
'includes/abilities/comic/class-comic-read-operations.php',
|
||||
'includes/abilities/comic/class-comic-write-operations.php',
|
||||
'includes/abilities/comic/class-comic-easel-integration.php',
|
||||
'includes/abilities/ads/class-ads-read-operations.php',
|
||||
'includes/abilities/ads/class-ads-write-operations.php',
|
||||
'includes/abilities/ads/class-ads-integration.php',
|
||||
'includes/abilities/forms/class-wpforms-integration.php',
|
||||
'includes/abilities/forms/class-gravityforms-integration.php',
|
||||
'includes/abilities/forms/class-fluentforms-integration.php',
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: "Ad Monetization & ExoClick Manager"
|
||||
description: "Manage WP Quads ad slots, dynamic ads.txt, ExoClick REST API zones, reporting, responsive ad styling, and cache invalidation"
|
||||
---
|
||||
# Ad Monetization & ExoClick Manager
|
||||
|
||||
Teach AI agents how to manage, audit, and optimize website advertising inventory, WP Quads ad units, dynamic `/ads.txt` records, and ExoClick REST API integrations across WordPress.
|
||||
|
||||
## Architecture & Data Flow
|
||||
|
||||
### 1. WP Quads Dual-Write Synchronization
|
||||
WP Quads operates with a split data architecture:
|
||||
- **Custom Post Type**: `quads-ads` posts store `code`, `position`, `dimensions`, and `quads_ad_old_id` (e.g. `ad1`, `ad2`) in `wp_postmeta`.
|
||||
- **Serialized Settings**: `quads_settings` option in `wp_options` stores an `ads` associative array keyed by slot ID (`ad1`, `ad2`, ...).
|
||||
- **Critical Rule**: Updating only one side leads to cache desynchronization, stale banner codes, or blank slots on frontend pages. Always use `emcp-tools/ads-write` (`update-ad` / `create-ad`), which enforces simultaneous dual-writes to both postmeta and serialized options.
|
||||
|
||||
### 2. Standard Responsive Placements & Dimensions
|
||||
When placing ads in comic or blog layouts, maintain strict layout integrity:
|
||||
- **Above Comic**: `728x90` Leaderboard. Wrap in responsive container centered above the primary canvas.
|
||||
- **Under Comic**: `300x250` Medium Rectangle. Placed immediately after navigation or author notes.
|
||||
- **Sidebars (Left & Right)**: `160x600` Wide Skyscrapers. Anchored in layout rails without overlapping main comic panels.
|
||||
- **Sticky Mobile Footer**: Anchored floating banner (`eas6a97888e10` or `300x100`/`300x50`) hidden on desktop breakpoints.
|
||||
- **Popunder (ExoClick PopMagic)**: `idzone: 6020536`. Always enforce frequency capping (minimum 24-hour cookie cooldown) to preserve user retention.
|
||||
|
||||
### 3. IAB ads.txt Management
|
||||
- **Syntax**: `<domain>, <publisher_id>, <relationship>, <certification_authority_id>`
|
||||
- Example: `exoclick.com, 1111220, DIRECT, f6e6255c27770857`
|
||||
- Relationship must strictly be `DIRECT` or `RESELLER`.
|
||||
- **Dual Persistence**: The plugin writes to `ABSPATH . 'ads.txt'` if filesystem permissions permit, and always synchronizes with the WordPress database option `emcp_tools_ads_txt` to ensure uninterrupted serving across all hosting configurations.
|
||||
|
||||
### 4. Cache Purging Protocol
|
||||
Whenever ad tags, zone IDs, or `/ads.txt` are modified:
|
||||
1. Purge LiteSpeed page cache: `do_action('litespeed_purge_all')`
|
||||
2. Flush WordPress object cache: `wp_cache_flush()`
|
||||
3. Flush transient API tokens: `purge-ad-cache`
|
||||
|
||||
---
|
||||
|
||||
## MCP Tools Reference
|
||||
|
||||
### `emcp-tools/ads-read`
|
||||
Call with no `operation` to inspect full operation schemas and argument hints.
|
||||
|
||||
- `list-ads`: List all registered ad units from WP Quads, including detected ad networks (`ExoClick`, `JuicyAds`, `TrafficStars`), slot keys, dimensions, and active statuses.
|
||||
```json
|
||||
{ "operation": "list-ads", "arguments": { "status": "all" } }
|
||||
```
|
||||
- `get-ad`: Get complete configuration, dimensions, code, and placement for a specific ad slot by Post ID or Quads key.
|
||||
```json
|
||||
{ "operation": "get-ad", "arguments": { "id": "ad1" } }
|
||||
```
|
||||
- `get-ads-txt`: Parse `/ads.txt` records, validate IAB formatting, and inspect authorized digital sellers.
|
||||
```json
|
||||
{ "operation": "get-ads-txt", "arguments": { "validate_syntax": true } }
|
||||
```
|
||||
- `audit-monetization`: Diagnostic scan checking ads.txt health, active unit count, multi-network script conflicts, and responsive styling.
|
||||
```json
|
||||
{ "operation": "audit-monetization", "arguments": {} }
|
||||
```
|
||||
- `exoclick-list-zones`: Query ExoClick REST API for registered zones, IDs, types, and statuses.
|
||||
```json
|
||||
{ "operation": "exoclick-list-zones", "arguments": { "idsite": 1111220 } }
|
||||
```
|
||||
- `exoclick-get-stats`: Query ExoClick reporting API for impressions, clicks, CTR, eCPM, and revenue metrics.
|
||||
```json
|
||||
{ "operation": "exoclick-get-stats", "arguments": { "date_from": "2026-09-01", "date_to": "2026-09-05" } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `emcp-tools/ads-write`
|
||||
Call with no `operation` to inspect full schemas.
|
||||
|
||||
- `update-ad`: Dual-write code, title, or position to WP Quads postmeta and `quads_settings`, automatically purging page cache.
|
||||
```json
|
||||
{
|
||||
"operation": "update-ad",
|
||||
"arguments": {
|
||||
"id": "ad1",
|
||||
"code": "<script async src=\"https://a.magsrv.com/ad-provider.js\"></script><ins class=\"eas6a97888e2\" data-zoneid=\"6020542\"></ins><script>(AdProvider = window.AdProvider || []).push({\"serve\": {}});</script>",
|
||||
"title": "Above Comic 728x90",
|
||||
"purge_cache": true
|
||||
}
|
||||
}
|
||||
```
|
||||
- `create-ad`: Create a new ad unit across both posts and serialized settings.
|
||||
```json
|
||||
{
|
||||
"operation": "create-ad",
|
||||
"arguments": {
|
||||
"title": "Sticky Mobile Footer",
|
||||
"code": "<script async src=\"https://a.magsrv.com/ad-provider.js\"></script><ins class=\"eas6a97888e10\" data-zoneid=\"6020534\"></ins><script>(AdProvider = window.AdProvider || []).push({\"serve\": {}});</script>",
|
||||
"slot_key": "ad11",
|
||||
"position": "custom"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `delete-ad`: Delete ad unit from WP Quads and settings (`confirm: true` required for safety).
|
||||
```json
|
||||
{ "operation": "delete-ad", "arguments": { "id": "ad11", "confirm": true } }
|
||||
```
|
||||
- `set-ads-txt`: Replace or append lines to `/ads.txt` with IAB syntax pre-validation.
|
||||
```json
|
||||
{
|
||||
"operation": "set-ads-txt",
|
||||
"arguments": {
|
||||
"append_records": [
|
||||
"exoclick.com, 1111220, DIRECT, f6e6255c27770857"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
- `purge-ad-cache`: Clear LiteSpeed page cache, WP object cache, and temporary transients.
|
||||
```json
|
||||
{ "operation": "purge-ad-cache", "arguments": { "all": true } }
|
||||
```
|
||||
- `exoclick-create-zone`: Create a new ad zone via ExoClick API and optionally wire it into a WP Quads slot.
|
||||
```json
|
||||
{
|
||||
"operation": "exoclick-create-zone",
|
||||
"arguments": {
|
||||
"name": "Under Comic 300x250",
|
||||
"dimensions": "300x250",
|
||||
"install_to_slot": "ad2"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `exoclick-verify-site`: Verify site ownership with ExoClick via API verification check.
|
||||
```json
|
||||
{ "operation": "exoclick-verify-site", "arguments": { "idsite": 1111220 } }
|
||||
```
|
||||
@@ -0,0 +1,463 @@
|
||||
<?php
|
||||
/**
|
||||
* Automated test suite for Ads & Monetization MCP Integration.
|
||||
*/
|
||||
|
||||
error_reporting( E_ALL & ~E_DEPRECATED & ~E_NOTICE & ~E_WARNING );
|
||||
|
||||
define( 'ABSPATH', dirname( __DIR__ ) . '/' );
|
||||
define( 'EMCP_TOOLS_DIR', dirname( __DIR__ ) . '/' );
|
||||
define( 'EMCP_TOOLS_URL', 'http://example.com/wp-content/plugins/elementor-mcp/' );
|
||||
define( 'EMCP_TOOLS_VERSION', '3.16.0' );
|
||||
|
||||
$GLOBALS['wp_posts'] = array();
|
||||
$GLOBALS['wp_postmeta'] = array();
|
||||
$GLOBALS['wp_options'] = array();
|
||||
$GLOBALS['wp_transients'] = array();
|
||||
$GLOBALS['wp_post_counter'] = 1000;
|
||||
$GLOBALS['actions_fired'] = array();
|
||||
$GLOBALS['abilities'] = array();
|
||||
|
||||
function sanitize_text_field( $t ) { return trim( strip_tags( $t ) ); }
|
||||
function sanitize_key( $k ) { return strtolower( preg_replace( '/[^a-z0-9_-]/i', '', $k ) ); }
|
||||
function sanitize_title( $t ) { return strtolower( preg_replace( '/[^a-z0-9_-]/i', '-', $t ) ); }
|
||||
function absint( $n ) { return abs( intval( $n ) ); }
|
||||
function __( $t, $d = '' ) { return $t; }
|
||||
function esc_html__( $t, $d = '' ) { return $t; }
|
||||
function current_user_can( $c ) { return true; }
|
||||
function home_url( $path = '' ) { return 'https://shad-base.com' . $path; }
|
||||
function wp_json_encode( $data ) { return json_encode( $data ); }
|
||||
|
||||
function get_option( $k, $d = false ) { return $GLOBALS['wp_options'][ $k ] ?? $d; }
|
||||
function update_option( $k, $v ) { $GLOBALS['wp_options'][ $k ] = $v; return true; }
|
||||
|
||||
function get_transient( $k ) { return $GLOBALS['wp_transients'][ $k ] ?? false; }
|
||||
function set_transient( $k, $v, $ttl = 0 ) { $GLOBALS['wp_transients'][ $k ] = $v; return true; }
|
||||
function delete_transient( $k ) { unset( $GLOBALS['wp_transients'][ $k ] ); return true; }
|
||||
|
||||
function do_action( $tag, ...$args ) { $GLOBALS['actions_fired'][] = $tag; }
|
||||
function wp_cache_flush() { $GLOBALS['actions_fired'][] = 'wp_cache_flush'; return true; }
|
||||
|
||||
class WP_Post {
|
||||
public $ID;
|
||||
public $post_title;
|
||||
public $post_name;
|
||||
public $post_content = '';
|
||||
public $post_status = 'publish';
|
||||
public $post_type = 'quads-ads';
|
||||
public $post_date;
|
||||
}
|
||||
|
||||
class WP_Error {
|
||||
private $code;
|
||||
private $message;
|
||||
public function __construct( $c = '', $m = '' ) { $this->code = $c; $this->message = $m; }
|
||||
public function get_error_message() { return $this->message; }
|
||||
public function get_error_code() { return $this->code; }
|
||||
}
|
||||
function is_wp_error( $t ) { return $t instanceof WP_Error; }
|
||||
|
||||
function get_post( $id ) {
|
||||
return $GLOBALS['wp_posts'][ $id ] ?? null;
|
||||
}
|
||||
|
||||
function get_the_title( $id ) {
|
||||
return $GLOBALS['wp_posts'][ $id ]->post_title ?? '';
|
||||
}
|
||||
|
||||
function get_posts( $args = array() ) {
|
||||
$results = array();
|
||||
$type = $args['post_type'] ?? 'post';
|
||||
$meta_k = $args['meta_key'] ?? '';
|
||||
$meta_v = $args['meta_value'] ?? '';
|
||||
|
||||
foreach ( $GLOBALS['wp_posts'] as $p ) {
|
||||
if ( $type && $p->post_type !== $type ) continue;
|
||||
if ( $meta_k ) {
|
||||
$val = get_post_meta( $p->ID, $meta_k, true );
|
||||
if ( $val != $meta_v ) continue;
|
||||
}
|
||||
$results[] = $p;
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
function wp_insert_post( $args, $wp_error = false ) {
|
||||
$id = ++$GLOBALS['wp_post_counter'];
|
||||
$p = new WP_Post();
|
||||
$p->ID = $id;
|
||||
$p->post_title = $args['post_title'] ?? '';
|
||||
$p->post_name = sanitize_title( $p->post_title );
|
||||
$p->post_type = $args['post_type'] ?? 'quads-ads';
|
||||
$p->post_status = $args['post_status'] ?? 'publish';
|
||||
$GLOBALS['wp_posts'][ $id ] = $p;
|
||||
return $id;
|
||||
}
|
||||
|
||||
function wp_update_post( $args ) {
|
||||
$id = $args['ID'] ?? 0;
|
||||
if ( ! isset( $GLOBALS['wp_posts'][ $id ] ) ) return false;
|
||||
$p = $GLOBALS['wp_posts'][ $id ];
|
||||
if ( isset( $args['post_title'] ) ) {
|
||||
$p->post_title = $args['post_title'];
|
||||
$p->post_name = sanitize_title( $p->post_title );
|
||||
}
|
||||
return $id;
|
||||
}
|
||||
|
||||
function wp_delete_post( $id, $force = false ) {
|
||||
if ( isset( $GLOBALS['wp_posts'][ $id ] ) ) {
|
||||
unset( $GLOBALS['wp_posts'][ $id ] );
|
||||
unset( $GLOBALS['wp_postmeta'][ $id ] );
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function get_post_meta( $post_id, $key = '', $single = false ) {
|
||||
if ( $key ) {
|
||||
return $GLOBALS['wp_postmeta'][ $post_id ][ $key ] ?? '';
|
||||
}
|
||||
return $GLOBALS['wp_postmeta'][ $post_id ] ?? array();
|
||||
}
|
||||
|
||||
function update_post_meta( $post_id, $key, $val ) {
|
||||
$GLOBALS['wp_postmeta'][ $post_id ][ $key ] = $val;
|
||||
return true;
|
||||
}
|
||||
|
||||
// HTTP Mocking
|
||||
function wp_remote_retrieve_body( $resp ) {
|
||||
return is_array( $resp ) ? ( $resp['body'] ?? '' ) : '';
|
||||
}
|
||||
function wp_remote_retrieve_response_code( $resp ) {
|
||||
return is_array( $resp ) ? ( $resp['response']['code'] ?? 200 ) : 200;
|
||||
}
|
||||
|
||||
function wp_remote_post( $url, $args = array() ) {
|
||||
// Mock login endpoint
|
||||
if ( strpos( $url, 'api.exoclick.com/v2/login' ) !== false ) {
|
||||
return array(
|
||||
'response' => array( 'code' => 200 ),
|
||||
'body' => json_encode( array(
|
||||
'token' => 'mock_bearer_token_xyz_123',
|
||||
'expires_in' => 3600,
|
||||
) ),
|
||||
);
|
||||
}
|
||||
// Mock zone creation
|
||||
if ( strpos( $url, 'api.exoclick.com/v2/zones' ) !== false ) {
|
||||
$data = json_decode( $args['body'] ?? '{}', true );
|
||||
return array(
|
||||
'response' => array( 'code' => 200 ),
|
||||
'body' => json_encode( array(
|
||||
'idzone' => 6020599,
|
||||
'name' => $data['name'] ?? 'Test Zone',
|
||||
'idsite' => $data['idsite'] ?? 1111220,
|
||||
) ),
|
||||
);
|
||||
}
|
||||
// Mock URL verification
|
||||
if ( strpos( $url, 'api.exoclick.com/v2/sites/url-verification' ) !== false ) {
|
||||
return array(
|
||||
'response' => array( 'code' => 200 ),
|
||||
'body' => json_encode( array(
|
||||
'success' => true,
|
||||
'status' => 1,
|
||||
'message' => 'Site ownership successfully verified.',
|
||||
) ),
|
||||
);
|
||||
}
|
||||
return array( 'response' => array( 'code' => 200 ), 'body' => '{}' );
|
||||
}
|
||||
|
||||
function wp_remote_get( $url, $args = array() ) {
|
||||
// Mock zones list
|
||||
if ( strpos( $url, 'api.exoclick.com/v2/zones' ) !== false ) {
|
||||
return array(
|
||||
'response' => array( 'code' => 200 ),
|
||||
'body' => json_encode( array(
|
||||
'result' => array(
|
||||
array( 'idzone' => 6020542, 'name' => 'Above Comic - 728x90', 'status' => 1 ),
|
||||
array( 'idzone' => 6020532, 'name' => 'Under Comic - 300x250', 'status' => 1 ),
|
||||
),
|
||||
) ),
|
||||
);
|
||||
}
|
||||
// Mock reporting stats
|
||||
if ( strpos( $url, 'api.exoclick.com/v2/statistics' ) !== false ) {
|
||||
return array(
|
||||
'response' => array( 'code' => 200 ),
|
||||
'body' => json_encode( array(
|
||||
'result' => array(
|
||||
array( 'impressions' => 150240, 'clicks' => 312, 'ctr' => 0.21, 'revenue' => 45.80 ),
|
||||
),
|
||||
) ),
|
||||
);
|
||||
}
|
||||
// Mock live ads.txt fetch
|
||||
if ( strpos( $url, '/ads.txt' ) !== false ) {
|
||||
return array(
|
||||
'response' => array( 'code' => 200 ),
|
||||
'body' => "exoclick.com, 1111220, DIRECT, f6e6255c27770857\njuicyads.com, 119864, DIRECT\n",
|
||||
);
|
||||
}
|
||||
return array( 'response' => array( 'code' => 200 ), 'body' => '' );
|
||||
}
|
||||
|
||||
function emcp_tools_register_ability( $name, $conf ) {
|
||||
$GLOBALS['abilities'][ $name ] = $conf;
|
||||
}
|
||||
|
||||
// Load classes under test
|
||||
require_once __DIR__ . '/../includes/abilities/ads/class-ads-read-operations.php';
|
||||
require_once __DIR__ . '/../includes/abilities/ads/class-ads-write-operations.php';
|
||||
require_once __DIR__ . '/../includes/abilities/ads/class-ads-integration.php';
|
||||
|
||||
echo "=== TEST 1: Ad Network Detection & Parsing (parse_ad_code) ===\n";
|
||||
// 1. ExoClick Banner
|
||||
$exo_tag = '<script async type="application/javascript" src="https://a.magsrv.com/ad-provider.js"></script>' .
|
||||
'<ins class="eas6a97888e2" data-zoneid="6020542" data-width="728" data-height="90"></ins>' .
|
||||
'<script>(AdProvider = window.AdProvider || []).push({"serve": {}});</script>';
|
||||
$parsed_exo = EMCP_Tools_Ads_Read_Operations::parse_ad_code( $exo_tag );
|
||||
assert( $parsed_exo['network'] === 'ExoClick' );
|
||||
assert( $parsed_exo['zone_id'] === '6020542' );
|
||||
assert( $parsed_exo['dimensions'] === '728x90' );
|
||||
|
||||
// 2. ExoClick Popunder
|
||||
$exo_pop = '<script type="application/javascript">var adConfig = {"idzone": 6020536, "popup_fallback": false};</script>';
|
||||
$parsed_pop = EMCP_Tools_Ads_Read_Operations::parse_ad_code( $exo_pop );
|
||||
assert( $parsed_pop['network'] === 'ExoClick' );
|
||||
assert( $parsed_pop['zone_id'] === '6020536' );
|
||||
|
||||
// 3. JuicyAds Legacy
|
||||
$juicy_tag = '<script type="text/javascript" data-cfasync="false" async src="https://poweredby.jads.co/js/jads.js"></script>' .
|
||||
'<ins id="1069796" data-width="160" data-height="600"></ins>';
|
||||
$parsed_juicy = EMCP_Tools_Ads_Read_Operations::parse_ad_code( $juicy_tag );
|
||||
assert( $parsed_juicy['network'] === 'JuicyAds' );
|
||||
assert( $parsed_juicy['zone_id'] === '1069796' );
|
||||
assert( $parsed_juicy['dimensions'] === '160x600' );
|
||||
|
||||
// 4. TrafficStars
|
||||
$ts_tag = '<script src="//trafficstars.com/ad.js" data-zone="48912"></script>';
|
||||
$parsed_ts = EMCP_Tools_Ads_Read_Operations::parse_ad_code( $ts_tag );
|
||||
assert( $parsed_ts['network'] === 'TrafficStars' );
|
||||
|
||||
echo "PASS: Ad network detection and dimension parsing verified!\n\n";
|
||||
|
||||
echo "=== TEST 2: Schema Completeness & Discovery Catalog ===\n";
|
||||
$read_schema = EMCP_Tools_Ads_Read_Operations::op_schema();
|
||||
$write_schema = EMCP_Tools_Ads_Write_Operations::op_schema();
|
||||
|
||||
assert( count( $read_schema ) === 6 );
|
||||
assert( isset( $read_schema['list-ads'], $read_schema['get-ad'], $read_schema['get-ads-txt'], $read_schema['audit-monetization'], $read_schema['exoclick-list-zones'], $read_schema['exoclick-get-stats'] ) );
|
||||
|
||||
assert( count( $write_schema ) === 7 );
|
||||
assert( isset( $write_schema['update-ad'], $write_schema['create-ad'], $write_schema['delete-ad'], $write_schema['set-ads-txt'], $write_schema['purge-ad-cache'], $write_schema['exoclick-create-zone'], $write_schema['exoclick-verify-site'] ) );
|
||||
|
||||
echo "PASS: All 13 read & write operation schemas verified!\n\n";
|
||||
|
||||
echo "=== TEST 3: WP Quads Create, Update & Dual-Write Synchronization ===\n";
|
||||
// 1. Create ad slot
|
||||
$create_res = EMCP_Tools_Ads_Write_Operations::create_ad( array(
|
||||
'title' => 'Above Comic 728x90',
|
||||
'code' => $exo_tag,
|
||||
'slot_key' => 'ad1',
|
||||
'position' => 'custom',
|
||||
'dimensions' => '728x90',
|
||||
) );
|
||||
|
||||
assert( $create_res['success'] === true );
|
||||
assert( $create_res['slot_key'] === 'ad1' );
|
||||
$post_id = $create_res['post_id'];
|
||||
assert( $post_id > 0 );
|
||||
|
||||
// Verify quads_settings option
|
||||
$settings = get_option( 'quads_settings' );
|
||||
assert( isset( $settings['ads']['ad1'] ) );
|
||||
assert( $settings['ads']['ad1']['code'] === $exo_tag );
|
||||
assert( $settings['ads']['ad1']['label'] === 'Above Comic 728x90' );
|
||||
|
||||
// Verify postmeta
|
||||
assert( get_post_meta( $post_id, 'code', true ) === $exo_tag );
|
||||
assert( get_post_meta( $post_id, 'quads_ad_old_id', true ) === 'ad1' );
|
||||
|
||||
// 2. Read ad slot
|
||||
$read_res = EMCP_Tools_Ads_Read_Operations::get_ad( array( 'id' => 'ad1' ) );
|
||||
assert( $read_res['success'] === true );
|
||||
assert( $read_res['slot_key'] === 'ad1' );
|
||||
assert( $read_res['parsed']['network'] === 'ExoClick' );
|
||||
assert( $read_res['parsed']['zone_id'] === '6020542' );
|
||||
|
||||
// 3. Update ad slot
|
||||
$updated_exo_tag = str_replace( '6020542', '6020599', $exo_tag );
|
||||
$update_res = EMCP_Tools_Ads_Write_Operations::update_ad( array(
|
||||
'id' => 'ad1',
|
||||
'code' => $updated_exo_tag,
|
||||
'title' => 'Above Comic - ExoClick 6020599',
|
||||
) );
|
||||
|
||||
assert( $update_res['success'] === true );
|
||||
// Verify dual-write sync after update
|
||||
$settings = get_option( 'quads_settings' );
|
||||
assert( $settings['ads']['ad1']['code'] === $updated_exo_tag );
|
||||
assert( $settings['ads']['ad1']['label'] === 'Above Comic - ExoClick 6020599' );
|
||||
assert( get_post_meta( $post_id, 'code', true ) === $updated_exo_tag );
|
||||
|
||||
// 4. List ads
|
||||
$list_res = EMCP_Tools_Ads_Read_Operations::list_ads();
|
||||
assert( $list_res['success'] === true );
|
||||
assert( $list_res['total_ads'] === 1 );
|
||||
assert( $list_res['ads'][0]['zone_id'] === '6020599' );
|
||||
|
||||
echo "PASS: Create, Update, and Dual-Write synchronization verified!\n\n";
|
||||
|
||||
echo "=== TEST 4: Delete Ad Unit with Confirmation Guard ===\n";
|
||||
// Attempt delete without confirm: true
|
||||
$unconfirmed = EMCP_Tools_Ads_Write_Operations::delete_ad( array( 'id' => 'ad1' ) );
|
||||
assert( $unconfirmed['success'] === false );
|
||||
assert( strpos( $unconfirmed['warning'], 'Confirmation required' ) !== false );
|
||||
assert( isset( $GLOBALS['wp_posts'][ $post_id ] ) ); // Still exists
|
||||
|
||||
// Delete with confirm: true
|
||||
$confirmed = EMCP_Tools_Ads_Write_Operations::delete_ad( array( 'id' => 'ad1', 'confirm' => true ) );
|
||||
assert( $confirmed['success'] === true );
|
||||
assert( ! isset( $GLOBALS['wp_posts'][ $post_id ] ) ); // Deleted from posts
|
||||
$settings = get_option( 'quads_settings' );
|
||||
assert( ! isset( $settings['ads']['ad1'] ) ); // Deleted from quads_settings
|
||||
|
||||
echo "PASS: Safety confirmation guard and complete ad unit deletion verified!\n\n";
|
||||
|
||||
echo "=== TEST 5: ads.txt Parser, Syntax Validator & Dual-Write ===\n";
|
||||
// 1. Set full ads.txt content
|
||||
$sample_ads_txt = "# Authorized Digital Sellers for shad-base.com\n" .
|
||||
"exoclick.com, 1111220, DIRECT, f6e6255c27770857\n" .
|
||||
"trafficstars.com, 48912, DIRECT\n" .
|
||||
"badline_format\n" .
|
||||
"invalidrel.com, 99999, PARTNER\n";
|
||||
|
||||
$set_res = EMCP_Tools_Ads_Write_Operations::set_ads_txt( array(
|
||||
'content' => $sample_ads_txt,
|
||||
) );
|
||||
assert( $set_res['success'] === true );
|
||||
assert( $set_res['saved_to_option'] === true );
|
||||
assert( count( $set_res['validation_warnings'] ) === 2 );
|
||||
|
||||
// 2. Read and parse ads.txt
|
||||
$parsed_txt = EMCP_Tools_Ads_Read_Operations::get_ads_txt();
|
||||
assert( $parsed_txt['success'] === true );
|
||||
assert( $parsed_txt['total_records'] === 3 );
|
||||
assert( $parsed_txt['records'][0]['domain'] === 'exoclick.com' );
|
||||
assert( $parsed_txt['records'][0]['publisher_id'] === '1111220' );
|
||||
assert( $parsed_txt['records'][0]['relationship'] === 'DIRECT' );
|
||||
assert( $parsed_txt['records'][0]['certification_authority_id'] === 'f6e6255c27770857' );
|
||||
assert( $parsed_txt['records'][0]['is_valid'] === true );
|
||||
|
||||
// 3. Append record
|
||||
$append_res = EMCP_Tools_Ads_Write_Operations::set_ads_txt( array(
|
||||
'append_records' => array( 'juicyads.com, 119864, DIRECT' ),
|
||||
) );
|
||||
assert( $append_res['success'] === true );
|
||||
$re_parsed = EMCP_Tools_Ads_Read_Operations::get_ads_txt();
|
||||
assert( strpos( $re_parsed['raw'], 'juicyads.com, 119864, DIRECT' ) !== false );
|
||||
|
||||
echo "PASS: ads.txt parser, syntax validation, backup, and append verified!\n\n";
|
||||
|
||||
echo "=== TEST 6: Monetization Diagnostic Audit ===\n";
|
||||
// With only ads.txt and no ads
|
||||
$audit1 = EMCP_Tools_Ads_Read_Operations::audit_monetization();
|
||||
assert( $audit1['success'] === true );
|
||||
assert( $audit1['status'] === 'attention_required' );
|
||||
assert( in_array( 'No active ad units found in WP Quads.', $audit1['issues'], true ) );
|
||||
|
||||
// Add active ExoClick ad unit
|
||||
EMCP_Tools_Ads_Write_Operations::create_ad( array(
|
||||
'title' => 'Main Banner',
|
||||
'code' => $exo_tag,
|
||||
) );
|
||||
$audit2 = EMCP_Tools_Ads_Read_Operations::audit_monetization();
|
||||
assert( $audit2['success'] === true );
|
||||
assert( $audit2['active_ad_units'] === 1 );
|
||||
|
||||
echo "PASS: Monetization diagnostic audit verified!\n\n";
|
||||
|
||||
echo "=== TEST 7: Cache Purging (LiteSpeed & Object Cache) ===\n";
|
||||
$GLOBALS['actions_fired'] = array();
|
||||
$purge_res = EMCP_Tools_Ads_Write_Operations::purge_ad_cache();
|
||||
assert( $purge_res['success'] === true );
|
||||
assert( in_array( 'litespeed_purge_all', $GLOBALS['actions_fired'], true ) );
|
||||
assert( in_array( 'wp_cache_flush', $GLOBALS['actions_fired'], true ) );
|
||||
|
||||
echo "PASS: Cache purging operations verified!\n\n";
|
||||
|
||||
echo "=== TEST 8: ExoClick REST API Integration Mock ===\n";
|
||||
// Set API token
|
||||
update_option( 'emcp_tools_exoclick_api_token', 'test_api_token_12345' );
|
||||
update_option( 'emcp_tools_exoclick_site_id', 1111220 );
|
||||
|
||||
// 1. Zones list
|
||||
$zones_res = EMCP_Tools_Ads_Read_Operations::exoclick_list_zones();
|
||||
assert( $zones_res['success'] === true );
|
||||
assert( count( $zones_res['zones'] ) === 2 );
|
||||
|
||||
// 2. Reporting stats
|
||||
$stats_res = EMCP_Tools_Ads_Read_Operations::exoclick_get_stats();
|
||||
assert( $stats_res['success'] === true );
|
||||
assert( $stats_res['stats'][0]['impressions'] === 150240 );
|
||||
|
||||
// 3. Create Zone with automatic slot installation
|
||||
$zone_create = EMCP_Tools_Ads_Write_Operations::exoclick_create_zone( array(
|
||||
'name' => 'Sidebar Ad',
|
||||
'dimensions' => '160x600',
|
||||
'install_to_slot' => 'create',
|
||||
) );
|
||||
assert( $zone_create['success'] === true );
|
||||
assert( $zone_create['zone_id'] === 6020599 );
|
||||
assert( strpos( $zone_create['tag_code'], 'data-zoneid="6020599"' ) !== false );
|
||||
assert( $zone_create['installation']['success'] === true );
|
||||
|
||||
// 4. Site Verification
|
||||
$verify_res = EMCP_Tools_Ads_Write_Operations::exoclick_verify_site();
|
||||
assert( $verify_res['success'] === true );
|
||||
assert( $verify_res['result']['status'] === 1 );
|
||||
|
||||
echo "PASS: ExoClick REST API zones, stats, zone creation, and site verification verified!\n\n";
|
||||
|
||||
echo "=== TEST 9: Abilities API Dispatcher Tools (emcp-tools/ads-read & ads-write) ===\n";
|
||||
$integration = new EMCP_Tools_Ads_Integration();
|
||||
$integration->register();
|
||||
|
||||
assert( isset( $GLOBALS['abilities']['emcp-tools/ads-read'] ) );
|
||||
assert( isset( $GLOBALS['abilities']['emcp-tools/ads-write'] ) );
|
||||
|
||||
// 1. Catalog discovery call (empty operation)
|
||||
$read_catalog = $integration->execute_read( array() );
|
||||
assert( isset( $read_catalog['tool'], $read_catalog['operations'] ) );
|
||||
assert( count( $read_catalog['operations'] ) === 6 );
|
||||
assert( isset( $read_catalog['operations']['list-ads']['schema'] ) );
|
||||
|
||||
$write_catalog = $integration->execute_write( array() );
|
||||
assert( isset( $write_catalog['tool'], $write_catalog['operations'] ) );
|
||||
assert( count( $write_catalog['operations'] ) === 7 );
|
||||
|
||||
// 2. Dispatcher execution
|
||||
$dispatched_read = $integration->execute_read( array(
|
||||
'operation' => 'get-ads-txt',
|
||||
'arguments' => array(),
|
||||
) );
|
||||
assert( $dispatched_read['success'] === true );
|
||||
assert( $dispatched_read['total_records'] > 0 );
|
||||
|
||||
$dispatched_write = $integration->execute_write( array(
|
||||
'operation' => 'purge-ad-cache',
|
||||
'arguments' => array( 'all' => true ),
|
||||
) );
|
||||
assert( $dispatched_write['success'] === true );
|
||||
|
||||
echo "PASS: Abilities API registration, catalog introspection, and dispatchers verified!\n\n";
|
||||
|
||||
echo "ALL 9 ADS & MONETIZATION MCP TEST SUITES PASSED 100%!\n";
|
||||
|
||||
if ( file_exists( ABSPATH . "ads.txt" ) ) {
|
||||
@unlink( ABSPATH . "ads.txt" );
|
||||
}
|
||||
Reference in New Issue
Block a user