shortcode_handler = new AdGhost_Shortcode_Handler(); $this->shortcode_handler->register(); if (class_exists('AdGhost_Ad_Block')) { (new AdGhost_Ad_Block($this->shortcode_handler))->init(); } (new AdGhost_Sidebar_Handler())->init(); (new AdGhost_Zones(function ($unit, $zone, $index) { return $this->get_ad_container_html($unit, $zone, $index); }))->init(); add_filter('the_content', [$this, 'insert_ads'], 15); add_filter('the_content', [$this, 'insert_content_edges'], 20); add_filter('acf/format_value/type=wysiwyg', [$this, 'insert_ads'], 998); add_filter('render_block', [$this, 'process_block_content'], 15, 2); add_action('template_redirect', [$this, 'start_output_buffer'], 1); add_action('wp_body_open', [$this, 'render_header_ads'], 10); add_action('wp_footer', [$this, 'render_footer_ads'], 9999999); add_action('shutdown', [$this, 'process_output_buffer'], 0); } public function start_output_buffer() { if (self::$buffer_started || is_admin() || defined('REST_REQUEST') || is_feed()) { return; } $units = AdGhost_Helper::units(); if (!$this->find_units_by_position($units, 'header') && !(self::is_article_page() && $this->find_units_by_position($units, 'incontent'))) { return; } ob_start([$this, 'filter_output_buffer']); self::$buffer_started = true; self::$buffer_level = ob_get_level(); } public function filter_output_buffer($html) { if (empty($html)) { return $html; } $html = preg_replace_callback( '//', function($matches) { $index = intval($matches[2]); $unit = $this->find_unit_by_key(AdGhost_Helper::units(), $matches[1]); if ($unit) { return $this->get_ad_container_html($unit, 'incontent', $index); } return ''; }, $html ); if (!self::$header_rendered && strpos($html, '
]*>/i', function ($m) use ($header_html) { return $m[0] . $header_html; }, $html, 1); } } return $html; } public function process_output_buffer() { if (self::$buffer_started && ob_get_level() >= self::$buffer_level && self::$buffer_level > 0) { while (ob_get_level() > self::$buffer_level) { ob_end_flush(); } ob_end_flush(); self::$buffer_started = false; } } public function process_block_content($block_content, $block) { $block_name = $block['blockName'] ?? ''; if ($block_name !== 'core/post-content') { return $block_content; } if (!self::is_article_page()) { return $block_content; } $post_id = (int) ($block['attrs']['postId'] ?? 0); if ($post_id === 0 && isset($GLOBALS['post']->ID)) { $post_id = (int) $GLOBALS['post']->ID; } if ($post_id !== 0 && $post_id !== (int) get_queried_object_id()) { return $block_content; } if ((new AdGhost_Paragraph_Rules())->content_gate($block_content) !== null) { return $block_content; } if (strpos($block_content, '') !== false) { return $block_content; } return $this->insert_ads($block_content); } public function insert_ads($content) { if (!is_string($content) || $content === '') { return $content; } if (doing_action('wp_head')) { return $content; } if (in_the_loop() && is_singular() && (int) get_the_ID() !== (int) get_queried_object_id()) { return $content; } $paragraph_rules = new AdGhost_Paragraph_Rules(); $ineligible = $paragraph_rules->content_gate($content); if ($ineligible !== null) { self::record_decision([], [], $ineligible . '.'); return $content; } if (strpos($content, '') !== false) { return $content; } $content_hash = md5($content); if (AdGhost_Placement_State::is_content_processed($content_hash)) { return AdGhost_Placement_State::get_processed_content($content_hash); } if (!self::is_article_page()) { AdGhost_Placement_State::mark_content_processed($content_hash, $content); return $content; } $excluded = AdGhost_Targeting::page_exclusion_reason(); if ($excluded !== null) { self::record_decision([], [], $excluded . '.'); AdGhost_Placement_State::mark_content_processed($content_hash, $content); return $content; } if (class_exists('AdGhost_IVT_Manager') && !AdGhost_IVT_Manager::ads_allowed()) { self::record_decision([], [], 'This session was judged invalid traffic; no ads were requested.'); AdGhost_Placement_State::mark_content_processed($content_hash, $content); return $content; } if (is_admin() || is_feed() || defined('REST_REQUEST')) { AdGhost_Placement_State::mark_content_processed($content_hash, $content); return $content; } if (!AdGhost_Helper::ads_live()) { AdGhost_Placement_State::mark_content_processed($content_hash, $content); return $content; } if (!get_option('adghost_auto_incontent', true)) { self::record_decision([], [], 'Automatic in-content placement is switched off in settings.'); AdGhost_Placement_State::mark_content_processed($content_hash, $content); return $content; } $ad_units = AdGhost_Helper::units(); $incontent_unit = $this->find_incontent_unit($ad_units); if (!$incontent_unit) { self::record_decision([], [], 'No ad unit resolves to the in-content position.'); AdGhost_Placement_State::mark_content_processed($content_hash, $content); return $content; } $max_ads = $this->remaining_page_budget() - $this->pending_zone_reservation($ad_units, 'header'); if ($max_ads <= 0) { self::record_decision([], [], 'The page ad limit was reached before the content was processed.'); AdGhost_Placement_State::mark_content_processed($content_hash, $content); return $content; } $content = $this->insert_placeholders_at_paragraphs($content, $ad_units, $max_ads); $content .= ''; AdGhost_Placement_State::mark_content_processed($content_hash, $content); $this->content_processed = true; return $content; } private function insert_placeholders_at_paragraphs($content, $ad_units, $max_ads) { $insertion_engine = new AdGhost_Insertion_Engine(); $points = $insertion_engine->detect_points($content); if (empty($points)) { self::record_decision([], [], 'No insertion points detected in this content.'); return $content; } if (self::placement_v3_enabled()) { $decision = $this->plan_incontent_v3($points, $ad_units, $max_ads, $content); } else { $decision = $this->plan_incontent_legacy($points, $ad_units, $max_ads); } $plan = $decision['plan']; $skipped = $decision['skipped']; self::record_decision($plan, $skipped, empty($plan) ? 'No point passed the selector — see the reason on each marker.' : null); if (empty($plan)) { return $content; } usort($plan, function ($a, $b) { return $b['offset'] - $a['offset']; }); foreach ($plan as $item) { $slot_id = $item['slot_id']; if (AdGhost_Placement_State::is_block_inserted($slot_id, 'content')) { continue; } $placeholder = self::$buffer_started ? "" : $this->placeholder_unit_html($item, $ad_units); $content = substr($content, 0, $item['offset']) . $placeholder . substr($content, $item['offset']); AdGhost_Placement_State::mark_block_inserted($slot_id, 'content', [ 'unit_name' => $item['unit_name'], 'paragraph' => $item['paragraph'], 'rule' => $item['rule'], 'score' => $item['score'], ]); } return $content; } private function placeholder_unit_html(array $item, array $ad_units) { $unit = $this->find_unit_by_key($ad_units, $item['unit_key']); return $unit ? $this->get_ad_container_html($unit, 'incontent', $item['index']) : ''; } private function plan_incontent_v3($points, $ad_units, $max_ads, $content = '') { $frequency = absint(get_option('adghost_incontent_frequency', 3)); if ($frequency < 1) { $frequency = 3; } $budget = $max_ads > 0 ? $max_ads : PHP_INT_MAX; $skipped = []; $analyzer = new AdGhost_Content_Analyzer($content); $points = $analyzer->filter_points($points, $skipped); if (!empty($points)) { $proximity = new AdGhost_Proximity($content); $points = $proximity->filter($points, $skipped); } if (!empty($points)) { $points = (new AdGhost_Paragraph_Rules())->filter_points($points, $content, $skipped); } if (empty($points)) { return ['plan' => [], 'skipped' => $skipped]; } $units = array_values(array_filter($ad_units, function ($u) { if (($u['placementMode'] ?? 'auto') === 'manual') { return false; } return AdGhost_Helper::get_unit_position($u) === 'incontent'; })); if (empty($units)) { return ['plan' => [], 'skipped' => $skipped]; } $order = ['after_n' => 0, 'before_n' => 0, 'before_last' => 1, 'every_n' => 2, 'auto' => 3]; $rank = array_map(function ($u) use ($order) { return $order[$u['insertionRule'] ?? 'every_n'] ?? 2; }, $units); array_multisort($rank, SORT_ASC, array_keys($units), SORT_ASC, $units); $min_words = self::min_words_between(); $plan = []; $global_index = AdGhost_Placement_State::get_zone_count('content'); $paragraph_total = (new AdGhost_Paragraph_Rules())->count_paragraphs($content); $claimed = []; foreach ($units as $unit) { $unit_name = sanitize_title($unit['name'] ?? 'incontent'); $unit_id = $unit['id'] ?? $unit_name; if ($budget <= 0) { $skipped['unit:' . $unit_id] = sprintf( '%s placed nothing: the page ad limit was already reached', $unit['name'] ?? $unit_name ); continue; } $device_block = AdGhost_Targeting::unit_exclusion_reason($unit); if ($device_block !== null) { $skipped['unit:' . $unit_id] = $device_block; continue; } $rule = $unit['insertionRule'] ?? 'every_n'; $pinned = in_array($rule, ['after_n', 'before_n'], true); $every = absint($unit['insertionValue'] ?? 0); $spacing = absint($unit['minSpacing'] ?? 0); $min_gap = $spacing > 0 ? $spacing : ($pinned ? 1 : max(1, $rule === 'every_n' && $every > 0 ? $every : $frequency)); $gap_words = $pinned ? 0 : $min_words; $unit_points = array_values(array_filter($points, function ($p) use ($claimed, $plan, $gap_words, &$skipped) { if (isset($claimed[$p['paragraph_number']])) { return false; } if ($gap_words > 0 && isset($p['words_before'])) { foreach ($plan as $placed) { if (isset($placed['words_before']) && abs($p['words_before'] - $placed['words_before']) < $gap_words) { $skipped[$p['id']] = sprintf('Fewer than %d words from another ad', $gap_words); return false; } } } return true; })); $sizes = (string) ($unit['sizes'] ?? ''); if ($sizes !== '') { $validator = new AdGhost_Safety_Validator(); $unit_points = array_values(array_filter($unit_points, function ($p) use ($validator, $sizes, $unit, $unit_name, &$skipped) { $why = $validator->creative_fit((int) ($p['validation']['width_px'] ?? 0), $sizes); if ($why === null) { return true; } $skipped[$p['id']] = sprintf('%s: %s', $unit['name'] ?? $unit_name, $why); return false; })); } $unit_points = AdGhost_Rule_Selector::filter_by_text($unit_points, $unit, $skipped); if (!empty($unit['excludeSelectors'])) { $selectors = array_map('trim', explode(',', (string) $unit['excludeSelectors'])); $unit_points = $analyzer->strip_excluded($unit_points, $content, $selectors); } $unit_cap = absint($unit['insertionLimit'] ?? 0); $selected = AdGhost_Rule_Selector::select($unit_points, $unit, [ 'last_paragraph' => $paragraph_total, 'frequency' => $frequency, 'max' => $unit_cap > 0 ? min($unit_cap, $budget) : $budget, 'min_gap' => $min_gap, 'min_words' => $gap_words, 'placed' => array_map(function ($item) { return [$item['paragraph'], $item['words_before']]; }, $plan), ]); foreach ($selected as $point) { if ($budget <= 0) { break; } $paragraph = $point['paragraph_number'] ?? 0; $plan[] = [ 'offset' => $point['offset'], 'point_id' => $point['id'] ?? '', 'index' => $global_index, 'unit_key' => self::unit_key($unit), 'unit_name' => $unit_name, 'slot_id' => "{$unit_id}_{$global_index}", 'paragraph' => $paragraph, 'words_before' => $point['words_before'] ?? null, 'rule' => $rule, 'score' => $point['score'] ?? [], ]; for ($n = $paragraph - $min_gap + 1; $n <= $paragraph + $min_gap - 1; $n++) { if ($n > 0 && !isset($claimed[$n])) { $claimed[$n] = ($n === $paragraph) ? sprintf('Taken by %s', $unit['name'] ?? $unit_name) : sprintf('Within %d paragraphs of %s', $min_gap, $unit['name'] ?? $unit_name); } } $global_index++; $budget--; } } $placed = array_column($plan, 'paragraph'); foreach ($points as $p) { $paragraph = $p['paragraph_number'] ?? 0; if (in_array($paragraph, $placed, true) || isset($skipped[$p['id']])) { continue; } $skipped[$p['id']] = $claimed[$paragraph] ?? 'No unit rule selected this point'; } return ['plan' => $plan, 'skipped' => $skipped]; } private function plan_incontent_legacy($points, $ad_units, $max_ads) { $paragraph_points = array_filter($points, function ($p) { return $p['type'] === 'paragraph' && $p['validation']['is_safe']; }); if (empty($paragraph_points)) { return ['plan' => [], 'skipped' => []]; } usort($paragraph_points, function ($a, $b) { $sa = $a['score']['ux'] + $a['score']['performance']; $sb = $b['score']['ux'] + $b['score']['performance']; return $sb - $sa; }); $selected = array_slice($paragraph_points, 0, $max_ads); $incontent_unit = $this->find_incontent_unit($ad_units); $unit_name = sanitize_title($incontent_unit['name'] ?? 'incontent'); $plan = []; $i = 0; foreach ($selected as $point) { $plan[] = [ 'offset' => $point['offset'], 'index' => $i, 'unit_key' => self::unit_key($incontent_unit), 'unit_name' => $unit_name, 'slot_id' => "incontent_{$i}", 'paragraph' => $point['paragraph_number'] ?? 0, 'rule' => 'legacy', 'score' => $point['score'] ?? [], ]; $i++; } return ['plan' => $plan, 'skipped' => []]; } private static $last_decision = ['plan' => [], 'skipped' => [], 'notice' => null]; private static function record_decision(array $plan, array $skipped, $notice = null) { if (empty($plan) && !empty(self::$last_decision['plan'])) { return; } self::$last_decision = [ 'plan' => $plan, 'skipped' => $skipped, 'notice' => $notice, ]; } public static function get_last_decision() { return self::$last_decision; } public static function min_words_between() { return (int) apply_filters('adghost_min_words_between', absint(get_option('adghost_min_words_between', 100))); } public static function is_article_page() { return is_singular() && !is_attachment() && !is_singular(['elementor_library', 'fl-builder-template', 'et_pb_layout', 'bricks_template', 'ct_template', 'wp_block']); } public static function placement_v3_enabled() { if (defined('ADGHOST_PLACEMENT_V3')) { return (bool) ADGHOST_PLACEMENT_V3; } return true; } private function page_allows_ads() { if (!AdGhost_Helper::ads_live() || is_admin() || is_feed() || (defined('REST_REQUEST') && REST_REQUEST)) { return false; } if (class_exists('AdGhost_Targeting') && AdGhost_Targeting::page_exclusion_reason() !== null) { return false; } if (class_exists('AdGhost_IVT_Manager') && !AdGhost_IVT_Manager::ads_allowed()) { return false; } return true; } public function insert_content_edges($content) { if (!is_singular() || !in_the_loop() || !is_main_query() || !$this->page_allows_ads()) { return $content; } $units = AdGhost_Helper::units(); foreach (['before_content' => 'prepend', 'after_content' => 'append'] as $position => $where) { foreach ($this->find_units_by_position($units, $position) as $index => $unit) { $slot = $position . '_' . $index; if (!$this->page_budget_remaining() || AdGhost_Placement_State::is_block_inserted($slot, 'content_edge')) { continue; } AdGhost_Placement_State::mark_block_inserted($slot, 'content_edge', ['unit_name' => $unit['name'] ?? '']); $html = $this->get_ad_container_html($unit, $position, $index); $content = $where === 'prepend' ? $html . $content : $content . $html; } } return $content; } public function render_header_ads() { if (self::$header_rendered) { return; } echo wp_kses($this->get_header_ads_html(), AdGhost_Helper::allowed_ads_html()); self::$header_rendered = true; } private function get_header_ads_html() { if (!$this->page_allows_ads()) { return ''; } $ad_units = AdGhost_Helper::units(); $header_units = $this->find_units_by_position($ad_units, 'header'); if (empty($header_units)) { return ''; } $html = ''; foreach ($header_units as $index => $header_unit) { if (!$this->page_budget_remaining()) { break; } if (AdGhost_Placement_State::is_block_inserted('header_' . $index, 'header')) { continue; } AdGhost_Placement_State::mark_block_inserted('header_' . $index, 'header', [ 'unit_name' => $header_unit['name'], ]); $ad_html = $this->get_ad_container_html($header_unit, 'header', $index); $html .= '