*/ private const EXCLUDED_PAGES = ['options-general.php']; /** * Lifetime of the "which rules did not match" hint for the CSS generator. */ private const FAILED_RULES_TTL = 300; private XPathEngine $xpath_engine; private OptionsRepository $options_repository; private CapabilitiesService $capabilities_service; private Logger $logger; public function __construct( XPathEngine $xpath_engine, OptionsRepository $options_repository, CapabilitiesService $capabilities_service, Logger $logger ) { $this->xpath_engine = $xpath_engine; $this->options_repository = $options_repository; $this->capabilities_service = $capabilities_service; $this->logger = $logger; } /** * Start output buffering when the current request should be processed. * * @since 1.0.0 */ public function start_output_buffering(): void { if (!$this->should_process_output()) { return; } ob_start([$this, 'process_output']); } /** * Process the output buffer content. * * This callback is automatically called when the output buffer is flushed. * It must always return a string to ensure the buffer is properly closed. * * @param string $content Buffered output. * @return string Processed output. */ public function process_output(string $content): string { if (!$this->is_html_document($content)) { return $content; } try { return (string) $this->xpath_engine->process_html( $content, $this->options_repository->get_active_rules(), $this->is_selection_mode_active() ); } catch (\Throwable $e) { $this->logger->error('Error processing output', ['message' => $e->getMessage()]); return $content; } } /** * Check if output processing should be enabled. * * @return bool */ private function should_process_output(): bool { if (!is_admin() || wp_doing_ajax() || wp_doing_cron()) { return false; } if (defined('REST_REQUEST') && REST_REQUEST) { return false; } if (defined('WP_CLI') && WP_CLI) { return false; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return false; } if (!$this->capabilities_service->can_use_selector()) { return false; } if (empty($this->options_repository->get_active_rules())) { return false; } if ($this->is_excluded_page()) { $this->mark_all_rules_as_failed_for_frontend(); return false; } return true; } /** * Hand every active rule over to the client-side runtime. * * Used for pages excluded from server-side HTML processing: the inline CSS * generator and the JavaScript runtime take over from there. * * @since 1.0.0 */ private function mark_all_rules_as_failed_for_frontend(): void { $rule_ids = array_keys($this->options_repository->get_active_rules()); if (empty($rule_ids)) { return; } PageContext::set_tracked_transient( 'adsd_failed_rules_' . PageContext::get_page_cache_key(), $rule_ids, self::FAILED_RULES_TTL ); } /** * Check if the current page is excluded from processing. * * @since 1.0.0 * @return bool True if current page should be excluded from processing. */ private function is_excluded_page(): bool { $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : ''; foreach (self::EXCLUDED_PAGES as $page) { if (strpos($request_uri, $page) !== false) { return true; } } return false; } /** * Check that the buffered content is an HTML document. * * Feeds, file downloads and JSON responses must be passed through untouched. * * @param string $content Buffered output. * @return bool True when the content looks like an HTML page. */ private function is_html_document(string $content): bool { $head = strtolower(substr(ltrim($content), 0, 64)); return strpos($head, '