placement_repository = adpresso_get_placement_repository(); // TODO: Consider using a different priority by setting if conflicts arise with other content filters. add_filter( 'the_content', [ $this, 'inject_into_dom' ], 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 ); $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 { if ( ! Plugin::get()->context['is_singular'] || ! in_the_loop() || ! is_main_query() || ! 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']; $placement = $this->placement_repository->get( $placement_id ); if ( ! $placement ) { continue; } $content_to_inject = ''; if ( $data['output_mode'] === 'js' ) { $content_to_inject = $placement->get_placeholder_html(); } elseif ( $data['output_mode'] === 'ssr' ) { $content_to_inject = $renderer->render( $placement, $data['content'] ); } if ( empty( $content_to_inject ) ) { continue; } $this->_inject_fragment_into_html( $dom, $xpath, $content_to_inject, $rules, $frontend_prefix ); } // Extract the body content. $body = $dom->getElementsByTagName( 'body' )->item( 0 ); $output = ''; foreach ( $body->childNodes as $child ) { $output .= $dom->saveHTML( $child ); } return $output; } /** * Injects the fragment into the HTML DOM based on the provided rules. * * @param $dom * @param DOMXPath $xpath * @param string $content_to_inject * @param array $rules * @param string $frontend_prefix * * @return void */ private function _inject_fragment_into_html( $dom, DOMXPath $xpath, string $content_to_inject, array $rules, string $frontend_prefix ): void{ // find the target element (without nesting in the placement wrapper) $query = sprintf( '//%s[not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " %s-placement ")])]', $rules['target_element'], $frontend_prefix ); $elements = $xpath->query( $query ); if ( $elements instanceof \DOMNodeList && $elements->length >= $rules['position_after'] ) { $target_node = $elements->item( $rules['position_after'] - 1 ); $fragment = $this->create_fragment( $content_to_inject, $dom ); if ( $target_node instanceof DOMNode && $target_node->parentNode instanceof DOMNode && $fragment instanceof DOMNode ) { if ( $rules['placement_method'] === 'after' ) { // adds after the target node - use insertBefore with nextSibling $target_node->parentNode->insertBefore( $fragment, $target_node->nextSibling ); } elseif ( $rules['placement_method'] === 'before' ) { $target_node->parentNode->insertBefore( $fragment, $target_node ); } } } } /** * 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; } $fragment = $dom->createDocumentFragment(); @$fragment->appendXML( $html ); return $fragment; } }