70, 'main' => 70, 'aside' => 30, ]; const CLASS_WIDTHS = [ 'elementor-col-20' => 20, 'elementor-col-25' => 25, 'elementor-col-33' => 33, 'elementor-col-50' => 50, 'elementor-col-66' => 66, 'wp-block-column' => 30, 'e-child' => 50, 'elementor-inner-column' => 50, 'widget-area' => 30, 'wp-sidebar' => 30, 'side-panel' => 30, 'sidebar' => 30, 'secondary' => 30, 'widget_' => 30, 'col-sm-3' => 25, 'col-md-3' => 25, 'col-lg-3' => 25, 'col-sm-4' => 33, 'col-md-4' => 33, 'col-lg-4' => 33, 'col-sm-6' => 50, 'col-md-6' => 50, 'col-lg-6' => 50, 'col-xl-6' => 50, 'col-6' => 50, 'col-sm-7' => 58, 'col-md-7' => 58, 'col-lg-7' => 58, 'col-7' => 58, 'col-md-8' => 66, 'col-lg-8' => 66, 'col-md-9' => 75, 'col-lg-9' => 75, 'col-md-10' => 83, 'entry-content' => 100, 'post-content' => 100, 'article-content' => 100, 'main-content' => 100, 'page-content' => 100, 'site-content' => 100, 'content-area' => 100, 'primary-content' => 100, ]; private $threshold; private $width_threshold; public function __construct($threshold = null, $width_threshold = null) { $this->threshold = (int) ($threshold ?? apply_filters('adghost_safety_threshold', self::DEFAULT_THRESHOLD)); $this->width_threshold = (int) ($width_threshold ?? apply_filters('adghost_width_threshold', self::DEFAULT_WIDTH_THRESHOLD)); } public function validate(array $point, $content, $sizes = '') { $offset = (int) ($point['offset'] ?? 0); $before = substr($content, 0, $offset); $after = substr($content, $offset); $state = [ 'score' => 100, 'checks' => [], 'reasons' => [], ]; $this->check_structural($before, $state); $width = $this->check_width($before, $sizes, $state); $this->check_context($before, $after, $state); $this->check_nesting($before, $state); $this->check_grid($before, $state); $score = max(0, min(100, $state['score'])); return [ 'passed' => $score >= $this->threshold && empty($state['reasons']), 'score' => $score, 'checks' => $state['checks'], 'reasons' => $state['reasons'], 'width_percent' => $width['percent'], 'width_px' => $width['px'], ]; } private function check_structural($before, array &$state) { $penalties = [ 'nav' => 50, 'footer' => 50, 'table' => 40, 'form' => 40, 'aside' => 40, 'iframe' => 40, 'script' => 50, 'style' => 50, ]; $passed = true; foreach (AdGhost_Protected_Elements::TAGS as $tag) { if ($tag === 'header' || !$this->is_open($before, $tag)) { continue; } $state['score'] -= ($penalties[$tag] ?? 30); $state['reasons'][] = sprintf('Inside a <%s> element', $tag); $passed = false; } if ($this->is_open($before, 'header') && !preg_match('/<(main|article|div[^>]*class="[^"]*(?:content|entry|post)[^"]*")[^>]*>/i', $before)) { $state['score'] -= 50; $state['reasons'][] = 'Inside a
element'; $passed = false; } $state['checks']['structural'] = $passed; } private function check_width($before, $sizes, array &$state) { $container = $this->detect_container_width($before); $percent = $container['percent']; $px = (int) round((self::DESKTOP_VIEWPORT * $percent) / 100); $passed = $percent >= $this->width_threshold; if (!$passed) { $state['score'] -= 40; $state['reasons'][] = sprintf( 'Container is only %d%% wide (%s)', $percent, $container['source'] ); } if ($sizes !== '') { $widest = $this->widest_size($sizes); if ($widest > 0 && $widest > $px) { $state['score'] -= 40; $state['reasons'][] = sprintf( 'Creative is %dpx wide but the container is about %dpx (%d%%, %s)', $widest, $px, $percent, $container['source'] ); $passed = false; } } $state['checks']['width'] = $passed; return ['percent' => $percent, 'px' => $px]; } /* Portions derived from NoAdCode (https://noadcode.com), GPL v2 or later. */ public function detect_container_width($before) { $stack = $this->parse_parent_containers($before); if (empty($stack)) { return ['percent' => 100, 'source' => 'no container detected']; } $immediate = end($stack); return ['percent' => $immediate['width'], 'source' => $immediate['source']]; } private function parse_parent_containers($before) { $containers = []; foreach ($this->open_elements($before) as $el) { $el['width'] = $this->width_of($el['tag'], $el['attributes']); if ($el['width'] !== null) { $containers[] = [ 'tag' => $el['tag'], 'width' => $el['width']['percent'], 'source' => $el['width']['source'], ]; } } return $containers; } private function open_elements($before) { $before = preg_replace_callback('//s', function ($c) { return str_repeat(' ', strlen($c[0])); }, $before); if (!preg_match_all('/<(\/?)([a-z][a-z0-9]*)\b([^>]*)>/i', $before, $m)) { return []; } $open = []; $void = ['img', 'br', 'hr', 'input', 'meta', 'link', 'source', 'area', 'col', 'embed', 'track', 'wbr']; foreach ($m[0] as $i => $unused) { $tag = strtolower($m[2][$i]); $attributes = $m[3][$i]; if (in_array($tag, $void, true) || substr(rtrim($attributes), -1) === '/') { continue; } if ($m[1][$i] === '/') { for ($s = count($open) - 1; $s >= 0; $s--) { if ($open[$s]['tag'] === $tag) { array_splice($open, $s); break; } } continue; } $open[] = ['tag' => $tag, 'attributes' => $attributes]; } return $open; } private function width_of($tag, $attributes) { if (preg_match('/style=["\']([^"\']*)["\']/i', $attributes, $style_match)) { $style = $style_match[1]; if (preg_match('/flex:\s*\d+\s+\d+\s+(\d+)%/i', $style, $m)) { return ['percent' => (int) $m[1], 'source' => 'inline flex-basis']; } if (preg_match('/max-width:\s*(\d+)%/i', $style, $m)) { return ['percent' => (int) $m[1], 'source' => 'inline max-width']; } if (preg_match('/(? (int) $m[1], 'source' => 'inline width']; } if (preg_match('/(? min(100, $percent), 'source' => 'inline width ' . $m[1] . 'px']; } } if (isset(self::SEMANTIC_WIDTHS[$tag])) { return ['percent' => self::SEMANTIC_WIDTHS[$tag], 'source' => '<' . $tag . '>']; } if (preg_match('/class=["\']([^"\']+)["\']/i', $attributes, $class_match)) { $classes = $class_match[1]; foreach (self::CLASS_WIDTHS as $pattern => $width) { if (stripos($classes, $pattern) !== false) { return ['percent' => $width, 'source' => 'class "' . $pattern . '"']; } } } return null; } public function creative_fit($width_px, $sizes_str) { if ($width_px <= 0 || trim((string) $sizes_str) === '') { return null; } $widest = $this->widest_size($sizes_str); if ($widest > 0 && $widest > $width_px) { return sprintf( 'Creative is %dpx wide but this container is about %dpx', $widest, $width_px ); } return null; } private function widest_size($sizes_str) { $widest = 0; foreach (preg_split('/[;,]/', $sizes_str) as $size) { if (preg_match('/(\d+)\s*x\s*\d+/i', trim($size), $m)) { $widest = max($widest, (int) $m[1]); } } return $widest; } private function check_context($before, $after, array &$state) { $chars_before = strlen(trim(wp_strip_all_tags($before))); $chars_after = strlen(trim(wp_strip_all_tags($after))); $min = 50; $ideal = 150; $passed = true; if ($chars_before < 10) { $state['score'] -= 2; } if ($chars_after < $min) { $state['score'] -= ($chars_after < 20) ? 15 : 10; $state['reasons'][] = sprintf('Only %d characters of content follow this point', $chars_after); $passed = false; } if ($chars_before >= $ideal && $chars_after >= $ideal) { $state['score'] += 15; } elseif ($chars_before >= $min && $chars_after >= $min) { $state['score'] += 10; } $state['checks']['context'] = $passed; } private function check_nesting($before, array &$state) { $depth = preg_match_all('//i', $before); $passed = true; if ($depth > 10) { $state['score'] -= 20; $state['reasons'][] = sprintf('Nested %d levels deep (likely a widget)', $depth); $passed = false; } elseif ($depth < 5) { $state['score'] += 10; } $state['checks']['nesting'] = $passed; $state['nesting_depth'] = $depth; } private function check_grid($before, array &$state) { $passed = true; $type = null; foreach (array_reverse($this->open_elements($before)) as $el) { $attributes = $el['attributes']; $style = preg_match('/style\s*=\s*("([^"]*)"|\'([^\']*)\')/i', $attributes, $sm) ? ($sm[2] !== '' ? $sm[2] : ($sm[3] ?? '')) : ''; $class = preg_match('/class\s*=\s*("([^"]*)"|\'([^\']*)\')/i', $attributes, $cm) ? ($cm[2] !== '' ? $cm[2] : ($cm[3] ?? '')) : ''; if (preg_match('/(?]*)?>/i', $before); $closed = preg_match_all('/<\/' . $tag . '>/i', $before); return $opened > $closed; } }