placement_repository = adpresso_get_placement_repository(); add_filter( 'the_content', [ $this, 'inject_into_dom' ], apply_filters( 'adpresso_content_filter_priority', 100 ) ); } /** * Prepare placements for injection. * * @return array */ public function prepare_placements(): array { $processed_data = Runtime::get_processed_data(); // Filter for placements that have injection rules, otherwise it's not a content placement. return array_filter( $processed_data, function( $data ) { if ( empty( $data['injection_rules'] ) ) { return false; } return true; }, ARRAY_FILTER_USE_BOTH ); } /** * Prepare the DOMDocument from the HTML content. * * @param $html * * @return DOMDocument|null */ private function prepare_dom( $html ): ?DOMDocument { $charset = get_bloginfo( 'charset' ); if ( empty( $charset ) ) { $charset = 'UTF-8'; } $dom = new DOMDocument( '1.0', $charset ); libxml_use_internal_errors( true ); $html_wrapper = '
' . $html . ''; $success = $dom->loadHTML( $html_wrapper ); // $success = $dom->loadHTML( '' . $html . '' ); libxml_clear_errors(); if ( ! $success ) { return null; } return $dom; } /** * Inject all content placements into the DOM. * * @param string $html * * @return string */ public function inject_into_dom( string $html ): string { $context = Plugin::get()->context; if ( isset( $context['is_singular'] ) && ! $context['is_singular'] || ! in_the_loop() || ! extension_loaded( 'dom' ) || trim( $html ) === '' ) { return $html; } $placements = $this->prepare_placements(); if ( empty( $placements ) ) { return $html; } $dom = $this->prepare_dom( $html ); if ( ! $dom ) { return $html; } $xpath = new DOMXPath( $dom ); $renderer = PlacementRenderer::get_instance(); $frontend_prefix = Settings::get_setting( 'frontend_prefix', Constants::DEFAULT_FRONTEND_PREFIX ); foreach ( $placements as $placement_id => $data ) { $rules = $data['injection_rules']; if ( ! $rules ) { continue; } $placement = $this->placement_repository->get( $placement_id ); if ( ! $placement ) { continue; } $allow_in_secondary_queries = $placement->get_meta( 'inject_in_secondary_queries', false ); // If we are not in the main query, and secondary queries are not allowed, skip this placement. if ( ! is_main_query() && ! $allow_in_secondary_queries ) { continue; } // Fetch the content of the placement (could be one or more ads) $ads_to_inject = $this->get_content_for_placement( $data, $placement, $renderer ); if ( empty( $ads_to_inject ) ) { continue; } $depth_mode = $placement->get_meta( 'automatic_depth_search', 'automatic' ); // Start with "all_levels" $query = sprintf( '//%s[not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " %s-wrap ")])]', $rules['target_element'], $frontend_prefix ); if ( $depth_mode === 'top_level' ) { // Strict top level: Forces XPath, only search direct children of . // works with pure the_content, but fails often with Page-Builders $query = sprintf( '/html/body/%s[not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " %s-wrap ")])]', $rules['target_element'], $frontend_prefix ); } $target_nodes = $xpath->query( $query ); if ( ! $target_nodes instanceof DOMNodeList || $target_nodes->length === 0 ) { // TODO: Implement a message for the user that no target elements were found. continue; } // converts DOMNodeList to a pure array that we can pass to the engine $targets_array = iterator_to_array( $target_nodes ); // Smart depth search filtering if ( $depth_mode === 'automatic' && count( $targets_array ) > 0 ) { $node_depths = array_map( function( $node ) { return substr_count( $node->getNodePath(), '/' ); }, $targets_array ); // Finds the lowest depth level where elements were found, called baseline $min_depth = min( $node_depths ); // Filters the array: Keep only the elements that are exactly on the baseline level! $targets_array = array_filter( $targets_array, function( $node ) use ( $min_depth ) { return substr_count( $node->getNodePath(), '/' ) === $min_depth; } ); // numerate array-keys (important for RepeatStrategy!) $targets_array = array_values( $targets_array ); } $targets_html = array_map( fn( $node ) => $dom->saveHTML( $node ), $targets_array ); // The "ARCHITECT": Asks the engine for the indices where the ads should be injected. $strategy_config = $rules['repeat_config'] ?? []; $strategy_config['from_bottom'] = ! empty( $rules['from_bottom'] ); $strategy = new RepeatStrategy(); $indices_to_inject = $strategy->calculate_injection_indices( $targets_html, $strategy_config ); // THE "MONTEUR": Perform the injection at the calculated indices $ad_count = count( $ads_to_inject ); $injected_count = 0; foreach ( $indices_to_inject as $index ) { $target_node = $targets_array[ $index ] ?? null; if ( ! $target_node ) { continue; } // Fetch the next ad (rotation) $ad_html = $ads_to_inject[ $injected_count % $ad_count ]; $fragment = $this->create_fragment( $ad_html, $dom ); $this->_inject_fragment_into_html( $target_node, $fragment, $rules['placement_method'] ); $injected_count++; } } // Extract the body content. $body = $dom->getElementsByTagName( 'body' )->item( 0 ); $output = ''; foreach ( $body->childNodes as $child ) { $output .= $dom->saveHTML( $child ); } return $output; } /** * Helper function to get the content of the placement. * * @param array $data * @param Placement $placement * @param PlacementRenderer $renderer * * @return array */ private function get_content_for_placement( array $data, Placement $placement, PlacementRenderer $renderer ): array { $content = []; if ( $data['output_mode'] === 'js' ) { $content[] = $placement->get_placeholder_html(); } elseif ( $data['output_mode'] === 'ssr' ) { $content[] = $renderer->render( $placement, $data['content'] ); } return $content; } /** * Injects the fragment into the HTML DOM based on the provided rules. * * @param DOMNode $target_node * @param DOMNode $fragment * @param string $method * * @return void */ private function _inject_fragment_into_html( DOMNode $target_node, DOMNode $fragment, string $method ): void { // 'before', 'after', und 'replace' need a parentNode. if ( ! $target_node->parentNode instanceof DOMNode ) { if ( in_array( $method, [ 'after', 'before', 'replace' ], true ) ) { // Cannot inject relative to a node without a parent (e.g., the root node) return; } } switch ( $method ) { case 'after': $target_node->parentNode->insertBefore( $fragment, $target_node->nextSibling ); break; case 'before': $target_node->parentNode->insertBefore( $fragment, $target_node ); break; case 'replace': $target_node->parentNode->replaceChild( $fragment, $target_node ); break; case 'inside_last': // or append $target_node->appendChild( $fragment ); break; case 'inside_first': // or prepend $target_node->insertBefore( $fragment, $target_node->firstChild ); break; default: RuntimeDebugger::add_doing_it_wrong_entry( 'Injector: Unknown injection method: ' . $method ); break; } } /** * Creates a DOMFragment, @ suppresses parsing warnings for e.g. script tags. */ public function create_fragment( string $html, DOMDocument $dom ): ?DOMNode { if ( trim( $html ) === '' ) { return null; } $charset = $dom->encoding ?: 'UTF-8'; $tmp = new DOMDocument( $dom->xmlVersion ?: '1.0', $charset ); libxml_use_internal_errors( true ); $html_wrapper = '' . $html . ''; $success = $tmp->loadHTML( $html_wrapper, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); libxml_clear_errors(); if ( ! $success ) { return null; } $fragment = $dom->createDocumentFragment(); $body = $tmp->getElementsByTagName( 'body' )->item( 0 ); if ( ! $body ) { return null; } // Alle Kinder aus dem ins Fragment importieren foreach ( iterator_to_array( $body->childNodes ) as $child ) { $fragment->appendChild( $dom->importNode( $child, true ) ); } return $fragment; } }