is_demo = $is_demo; } public function isDemo(): bool { return $this->is_demo; } public function setBait($bait) { $this->_bait = $bait; } public function getBait() { return $this->_bait; } public function getDataJson() { return $this->dataJson; } public function setDataJson($json) { $this->dataJson = $json; } public function getThemeCodePath() { return $this->themeCodePath; } public function getThemeJson() { return $this->themeJson; } public function setThemeJson($json) { $this->themeJson = $json; } public function getCurrentTheme() { return $this->currentTheme; } public function setCurrentTheme($theme) { $this->currentTheme = $theme; } public function setDebug($debug) { $this->debug = $debug; } public function setUseLoader($useLoader) { $this->useLoader = $useLoader; } public function setUsePacker($usePacker) { $this->usePacker = $usePacker; } public function setUseObsf($useObsf) { $this->useObsf = $useObsf; } public function setUseMinify($useMinify) { $this->useMinify = $useMinify; } public function isUseLoader() { return $this->useLoader; } public function isUsePacker() { return $this->usePacker; } public function setLoaderVersion($loaderVersion) { $this->loaderVersion = $loaderVersion; } public function getLoaderVersion() { return $this->loaderVersion; } public function __construct($debug = false) { $this->debug = $debug; } /** * Graceful JSON loader. * @return array|\WP_Error */ protected function loadJsonData($json) { if ($json === null) { $json = ''; } elseif (!is_string($json)) { $type = gettype($json); /* translators: %s: PHP data type name. */ $msg = sprintf(__('Expected JSON string, got %s.', 'ad-block-guard'), esc_html($type)); PluginLogger::log('error', $msg, [ 'context' => 'loadJsonData', 'received_type' => $type, ]); return new \WP_Error('abg_json_type', $msg); } // Strip UTF-8 BOM $json = preg_replace('/^\xEF\xBB\xBF/', '', $json); if (trim($json) === '') { // Empty JSON treated as empty object return []; } $decoded = json_decode($json, true, 512, 0); if (!is_array($decoded)) { $errCode = json_last_error(); $errMsg = json_last_error_msg(); // Truncate preview to keep logs sane $preview = function_exists('mb_substr') ? mb_substr($json, 0, 400) : substr($json, 0, 400); if ((function_exists('mb_strlen') ? mb_strlen($json) : strlen($json)) > 400) { $preview .= '…'; } $error_message = __('Invalid JSON data encountered.', 'ad-block-guard'); PluginLogger::log('error', $error_message, [ 'context' => 'loadJsonData', 'json_error' => $errMsg, 'json_code' => $errCode, 'json_preview' => $preview, 'json_length' => function_exists('mb_strlen') ? mb_strlen($json) : strlen($json), ]); return new \WP_Error('abg_json_invalid', sprintf('%s (%s)', $error_message, $errMsg)); } return $decoded; } /** * Replace {{placeholders}} in a template. Never throws. * Non-string values are stringified; we log and continue. */ protected function replacePlaceholders($template, $replacements) { foreach ($replacements as $key => $value) { if (!is_string($value)) { $stringified = is_scalar($value) ? (string) $value : wp_json_encode($value, JSON_UNESCAPED_SLASHES); PluginLogger::log('warning', sprintf( /* translators: %1$s: Replacement key name. */ esc_html__('Non-string value for key {%1$s}, stringifying.', 'ad-block-guard'), esc_html($key) ), ['key' => $key, 'value_type' => gettype($value)] ); $value = (string) $stringified; } $template = str_replace('{{' . $key . '}}', $value, $template); } return $template; } protected function isJson($string) { json_decode($string); return json_last_error() === JSON_ERROR_NONE; } /** * Perform JSON data replacements. Logs & returns original template on error. */ public function jsonDataReplacements($template) { if (empty($this->dataJson)) { $error_message = esc_html__('Data JSON is not set.', 'ad-block-guard'); PluginLogger::log('error', $error_message, ['context' => 'jsonDataReplacements']); return $template; // soft-fail } $jsonArray = $this->loadJsonData($this->dataJson); if (is_wp_error($jsonArray)) { PluginLogger::log('error', esc_html__('Failed to load JSON data.', 'ad-block-guard'), ['context' => 'jsonDataReplacements', 'error' => $jsonArray->get_error_message()] ); return $template; } if (empty($jsonArray) || !is_array($jsonArray)) { PluginLogger::log('error', esc_html__('Loaded JSON data is empty or invalid.', 'ad-block-guard'), ['context' => 'jsonDataReplacements'] ); return $template; } return $this->replacePlaceholders($template, $jsonArray); } /** * Perform theme replacements. Logs & returns original template on error. */ public function jsonThemeReplacements($template) { if (empty($this->themeJson)) { $error_message = esc_html__('Theme JSON is not set.', 'ad-block-guard'); PluginLogger::log('error', $error_message, ['context' => 'jsonThemeReplacements']); return $template; // soft-fail } $theme = $this->getCurrentTheme(); $jsonArray = $this->loadJsonData($this->themeJson); if (is_wp_error($jsonArray)) { PluginLogger::log('error', esc_html__('Failed to load theme JSON data.', 'ad-block-guard'), ['context' => 'jsonThemeReplacements', 'error' => $jsonArray->get_error_message()] ); return $template; } if (!isset($jsonArray[$theme]) || !is_array($jsonArray[$theme])) { $error_message = sprintf( /* translators: %1$s: Requested theme key. */ esc_html__('Invalid theme [%1$s] data or theme not found.', 'ad-block-guard'), esc_html($theme) ); PluginLogger::log('error', $error_message, [ 'context' => 'jsonThemeReplacements', 'theme' => $theme ]); return $template; } $replacements = $jsonArray[$theme]; // JSON template case if ($this->isJson($template)) { $templateArray = json_decode($template, true); if (is_array($templateArray)) { foreach ($templateArray as $key => $value) { if (isset($replacements[$key])) { $templateArray[$key] = $replacements[$key]; } } return wp_json_encode($templateArray); } } return $this->replacePlaceholders($template, $replacements); } private function jsRandomizer($length, $characters) { $charactersLength = strlen($characters); $randomString = ''; for ($i = 0; $i < $length; $i++) { $randomString .= $characters[wp_rand(0, $charactersLength - 1)]; } return $randomString; } public function toCharCode($str) { return array_map('ord', str_split($str)); } public function fromCharCode($codes) { return implode('', array_map('chr', $codes)); } private function customObfuscate($template, $baseKey) { $names_to_obfuscate = [ 'css-class-ad-block-overlay-content-inner', 'css-class-ad-block-notice-content-inner', 'js_const_admin_notice_header_element', 'css-id-ad-block-overlay-content', 'js_func_delayed_detect_ad_block', 'js_var_check_overlay_visibility', 'js_const_notice_header_element', 'js_func_run_ad_block_detection', 'js_const_notice_style_element', 'js_var_trans_overlays_visible', 'js_var_content_overlay_check', 'js_var_calculated_checksum', 'js_var_css_element_removal', 'js_var_css_element_removal', 'js_var_notice_css_location', 'css-id-ad-block-container', 'js_func_verify_and_decode', 'js_var_redraw_frame_count', 'css-class-body-no-scroll', 'js_var_check_frame_count', 'js_func_check_animation', 'js_var_animation_fired', 'js_var_check_intervals', 'js_var_selected_position', 'js_var_resolve_method', 'js_var_reject_method', 'js_var_obfuscated_js', 'js_var_checks_completed', 'js_func_detect_ad_block', 'css-id-ad-block-overlay', 'css-id-ad-block-button', 'js_var_content_overlay', 'js_var_overlay_observer', 'js_var_max_redraw_frames', 'js_var_init_frame_count', 'js_var_max_init_frames', 'js_var_trans_overlays', 'js_var_positions', 'js_var_bg_overlay_check', 'js_var_loader_overlay', 'js_var_method_custom_js', 'js_var_notice_element', 'js_var_notice_css_location', 'js_var_const_style', 'js_var_const_style', 'js_const_notice_html', 'css-class-close-button', 'css-id-overlay-decoy-1', 'css-id-overlay-decoy-2', 'css-id-overlay-decoy-3', 'css-id-overlay-decoy-4', 'css-id-overlay-decoy-5', 'js_var_max_check_frames', 'js_var_trans_overlay', 'js_var_content_overlay', 'js_var_bg_overlay', 'js_var_method_css_ani', 'js_var_check_count', 'js_var_frame_count', 'js_var_total_checks', 'js_func_check_time', 'js_var_current_time', 'js_var_start_time', 'js_var_remote_url', 'js_var_adBaitAnimation', 'js_var_init_display', 'js_func_show_overlay', 'js_func_not_detected', 'js_var_overlay_shown', 'js_func_detected', 'js_var_detected', 'js_func_nextFrame', 'js_func_finalCheck', 'js_func_checkBait', 'js_var_script', 'js_var_xhr', 'js_const_overlay', 'js_const_notice', 'js_var_method_1', 'js_var_method_2', 'js_var_method_3', 'js_var_decoded_js', 'js_var_decoded', 'js_var_checksum', 'js_var_encoded', 'js_var_element', 'js_var_element', 'js_var_style_el', 'js_var_is_closing', 'js_var_nothing', 'js_var_nadda', 'js_var_redraw', 'js_var_sum', 'js_var_current_time', 'js_var_demo', 'js_var_str', 'js_var_char', 'js_var_bait' ]; $obfuscated_names = []; foreach ($names_to_obfuscate as $name) { do { $obfuscated_name = $this->jsRandomizer(wp_rand($this->minLength, $this->maxLength), $baseKey); } while (in_array($obfuscated_name, $obfuscated_names, true)); $obfuscated_names[$name] = $obfuscated_name; } if ($this->useObsf) { $template = $this->getReplacementsWithoutAlteringExistingJsonVariables($template, $obfuscated_names); } // Replace bait classes $template = str_replace( "pub_300x250 pub_300x250m text-ad textAd text_ad adBanner ad-banner ad_box adbox", $this->_bait, $template ); // Remove log messages and #log related styles if debug is false if (!$this->debug) { $template = preg_replace('/function\s+logMessage\(message\)\s*\{.*?\}\s*/s', '', $template); $template = preg_replace('/logMessage\(.*?\);/s', '', $template); $template = preg_replace('/#log\s*\{.*?\}/s', '', $template); $template = str_replace('
', '', $template); } // remove demo-only css if (!$this->isDemo()) { $template = preg_replace('/body\.admin-bar\s+#\w+\s*\{.*?\}\s*/s', '', $template); $template = preg_replace('/@media\s+screen\s+and\s+\(max-width:\s*782px\)\s*\{\s*(?:body\.admin-bar\s+#\w+\s*\{.*?\}\s*)?\}\s*/s', '', $template); } return [$template, $obfuscated_names]; } /** * Read, transform, obfuscate, minify JS. Soft-fails with WP_Error on IO issues. * @return array{0:string,1:int,2:array}|\WP_Error */ public function processScript($javascriptCodePath = null) { $javascriptCodePath = $javascriptCodePath ?: $this->javascriptCodePath; $path = wp_normalize_path($javascriptCodePath); if (empty($path) || ! file_exists($path)) { $error_message = sprintf( /* translators: %s: Absolute path to JavaScript file. */ __('JavaScript file missing or not readable: %s', 'ad-block-guard'), esc_html($path) ); PluginLogger::log('error', $error_message, [ 'file' => $path, 'context' => 'processScript' ]); return new \WP_Error('abg_js_missing', $error_message); } $js_content = file_get_contents($path); if ($js_content === false) { $error_message = sprintf( /* translators: %s: Absolute path to JavaScript file. */ __('Failed to read the JavaScript file: %s', 'ad-block-guard'), esc_html($path) ); PluginLogger::log('error', $error_message, [ 'file' => $path, 'context' => 'processScript' ]); return new \WP_Error('abg_js_unreadable', $error_message); } // Strip comments + empty lines $js_content = preg_replace('!/\*.*?\*/!s', '', $js_content); $js_content = preg_replace('/\n\s*\n/', "\n", $js_content); // Apply replacements (soft-fail internally) $js_content = $this->jsonDataReplacements($js_content); $js_content = $this->jsonThemeReplacements($js_content); // Obfuscate + minify list($obfuscated_js, $obfuscated_names) = $this->customObfuscate($js_content, $this->baseKey); if ($this->useMinify) { try { $minifier = new Minify\JS(); $minifier->add($obfuscated_js); $obfuscated_js = $minifier->minify(); } catch (\Throwable $t) { PluginLogger::log('error', 'Minify failed; continuing unminified.', [ 'context' => 'processScript', 'error' => $t->getMessage() ]); } } // Add checksum $checksum = $this->sum_utf16_code_units($obfuscated_js); return [$obfuscated_js, $checksum, $obfuscated_names]; } private function sum_utf16_code_units($utf8_str) { $utf16 = mb_convert_encoding($utf8_str, 'UTF-16LE', 'UTF-8'); $sum = 0; $length = strlen($utf16); for ($i = 0; $i < $length; $i += 2) { $low = ord($utf16[$i]); $high = ord($utf16[$i + 1]); $code_unit = $low | ($high << 8); $sum += $code_unit; } return $sum % 256; } public function getFinalJavaScript() { $javascript = $this->isUseLoader() ? $this->getJavaScriptWithLoader() : $this->getJavaScriptWithoutLoader(); // If loader path failed, it may return WP_Error if (is_wp_error($javascript)) { PluginLogger::log('error', 'Final JS generation failed.', [ 'context' => 'getFinalJavaScript', 'error' => $javascript->get_error_message() ]); return ''; // Soft-fail: empty script } // Apply packer if enabled if ($this->isUsePacker()) { try { $javascript = $this->pack($javascript); } catch (\Throwable $t) { PluginLogger::log('error', 'Packer failed; returning un-packed JS.', [ 'context' => 'getFinalJavaScript', 'error' => $t->getMessage() ]); } } return trim($javascript); } private function pack($plain_javascript) { $packer = new Packer($plain_javascript, 'Normal', true, false, true); return $packer->pack(); } private function getJavaScriptWithoutLoader() { $result = $this->processScript($this->javascriptCodePath); if (is_wp_error($result)) { return $result; // bubble up } list($obfuscated_js) = $result; return $obfuscated_js; } private function getJavaScriptWithLoader() { $result = $this->processScript($this->javascriptCodePath); if (is_wp_error($result)) { return $result; // bubble up } list($obfuscated_js, $checksum, $obfuscated_names) = $result; // Path to the appropriate loader file based on version $loader_suffix = ($this->getLoaderVersion() === 'v1') ? '' : '-v2'; $js_file_path = defined('ADBLOCKGUARD_PLUGIN_DIR') ? ADBLOCKGUARD_PLUGIN_DIR . 'src/script/loader' . $loader_suffix . '.js' : __DIR__ . '/script/loader' . $loader_suffix . '.js'; $path = wp_normalize_path($js_file_path); if (empty($path) || !file_exists($path)) { /* translators: %s: Absolute path to loader JavaScript file. */ $msg = sprintf(__('Loader file missing: %s', 'ad-block-guard'), esc_html($path)); PluginLogger::log('error', $msg, [ 'context' => 'getJavaScriptWithLoader', 'file' => $path ]); return new \WP_Error('abg_loader_missing', $msg); } $js_loader_template = file_get_contents($path); if ($js_loader_template === false) { /* translators: %s: Absolute path to loader JavaScript file. */ $msg = sprintf(__('Failed to read loader file: %s', 'ad-block-guard'), esc_html($path)); PluginLogger::log('error', $msg, [ 'context' => 'getJavaScriptWithLoader', 'file' => $path ]); return new \WP_Error('abg_loader_unreadable', $msg); } // Replace variable names with obfuscated ones $js_loader_template = $this->javascriptVariableReplacements($js_loader_template, $obfuscated_names, $obfuscated_js, $checksum); // Base64 encode the obfuscated JS $encoded = base64_encode($obfuscated_js); // Replace placeholders $js_loader_template = str_replace('OBFUSCATED_JAVASCRIPT_ADBLOCK_SCRIPT', $encoded, $js_loader_template); $js_loader_template = str_replace('JS_OBFUSCATION_CHECKSUM', $checksum, $js_loader_template); // Minify the complete loader template if ($this->useMinify) { try { $minifier = new Minify\JS(); $minifier->add($js_loader_template); $js_loader_template = $minifier->minify(); } catch (\Throwable $t) { PluginLogger::log('error', 'Minify (loader) failed; continuing unminified.', [ 'context' => 'getJavaScriptWithLoader', 'error' => $t->getMessage() ]); } } return $js_loader_template; } public function isDebug() { return $this->debug; } public function setScriptPath($javascriptCodePath) { $this->javascriptCodePath = $javascriptCodePath; } public function setBaitClasses($baitClasses) { $this->_bait = implode(' ', $baitClasses); } public function getBaitClasses() { $baitArray = explode(' ', $this->_bait); shuffle($baitArray); return implode(' ', $baitArray); } private function javascriptVariableReplacements($template, $obfuscatedNames, $obfuscatedJs, $checksum) { return $this->getReplacementsWithoutAlteringExistingJsonVariables($template, $obfuscatedNames); } private function getReplacementsWithoutAlteringExistingJsonVariables($template, $obfuscatedNames) { return preg_replace_callback('/(.*?)({{.*?}}|$)/s', function ($matches) use ($obfuscatedNames) { $part = $matches[1]; $placeholder = $matches[2]; foreach ($obfuscatedNames as $original => $obfuscated) { $part = str_replace($original, $obfuscated, $part); } return $part . $placeholder; }, $template); } public function iso8859_1_to_utf8($s) { $s .= $s; $len = strlen($s); for ($i = $len >> 1, $j = 0; $i < $len; ++$i, ++$j) { switch (true) { case $s[$i] < "\x80": $s[$j] = $s[$i]; break; case $s[$i] < "\xC0": $s[$j] = "\xC2"; $s[++$j] = $s[$i]; break; default: $s[$j] = "\xC3"; $s[++$j] = \chr(\ord($s[$i]) - 64); break; } } return substr($s, 0, $j); } }