content = $content; $this->set_provider( $content, $url ); } /** * Get the content with an overlay. * * @param array $attributes Embed attributes * @param \epiphyt\Embed_Privacy\embed\Provider|null $provider Embed provider * @return string Content with embeds replaced by an overlay */ public function get( array $attributes = [], $provider = null ) { /** * Filter the content after it has been replaced with an overlay. * * @since 1.10.0 * * @param string $content Replaced content */ $content = (string) \apply_filters( 'embed_privacy_overlay_replaced_content', $this->content ); /** * If set to true, unknown providers are not handled via Embed Privacy. * * @since 1.5.0 * * @param bool $ignore_unknown Whether unknown providers should be ignored * @param string $content The original content */ $ignore_unknown_providers = (bool) \apply_filters( 'embed_privacy_ignore_unknown_providers', false, $content ); // get default external content // special case for youtube-nocookie.com as it is part of YouTube provider if ( ! $ignore_unknown_providers || \str_contains( $content, 'youtube-nocookie.com' ) ) { if ( $provider instanceof Provider ) { $this->provider = $provider; $content = $this->replace( $content, $attributes ); } else { foreach ( $this->get_providers() as $provider ) { $this->provider = $provider; $content = $this->replace( $content, $attributes ); } } $this->provider = null; } return $content; } /** * Get a list of characters to replace to prevent problems with DOMDocument. * * @return array List of character replacements */ private static function get_character_replacements() { $replacements = [ '%' => '@@epi_percentage', ' ' => ' data-epi-spacing ', '[' => '@@epi_square_bracket_start', ']' => '@@epi_square_bracket_end', '{' => '@@epi_curly_bracket_start', '}' => '@@epi_curly_bracket_end', ]; /** * Filter character replacements. * * @since 1.10.0 * * @param array $replacements Current replacements */ $replacements = (array) \apply_filters( 'embed_privacy_overlay_character_replacements', $replacements ); return $replacements; } /** * Get the current provider. * * @deprecated 1.10.4 * * @return \epiphyt\Embed_privacy\embed\Provider|null Provider object */ public function get_provider() { \_doing_it_wrong( __METHOD__, \esc_html__( 'This method is outdated and will be removed in the future.', 'embed-privacy' ), '1.10.4' ); return $this->provider; } /** * Get all providers to replace an embed of. * * @return \epiphyt\Embed_privacy\embed\Provider[] Provider object */ public function get_providers() { return $this->providers; } /** * Replace content with an overlay and print assets. * * @param string $content Content to replace embeds in * @param array $attributes Additional attributes * @return string Replaced content */ private function replace( $content, array $attributes ) { $new_content = $this->replace_content( $content, $attributes ); if ( $new_content !== $content ) { Embed_Privacy::get_instance()->has_embed = true; Embed_Privacy::get_instance()->frontend->print_assets(); $content = $new_content; } return $content; } /** * Replace embedded content with an overlay. * * @param string $content Content to replace embeds in * @param array $attributes Additional attributes * @return string Updated content */ private function replace_content( $content, array $attributes ) { if ( empty( $content ) ) { return $content; } if ( ! $this->provider instanceof Provider ) { return $content; } /** * Filter whether to ignore this embed. * * @since 1.9.0 * * @param bool $ignore_embed Whether to ignore this embed * @param string $content The original content * @param string $provider_title Embed provider title * @param string $provider_name Embed provider name * @param array $attributes Additional attributes */ $ignore_embed = (bool) \apply_filters( 'embed_privacy_ignore_embed', $this->provider->is_disabled(), $content, $this->provider->get_title(), $this->provider->get_name(), $attributes ); if ( $ignore_embed ) { return $content; } $attributes = \wp_parse_args( $attributes, [ 'additional_checks' => [], 'elements' => [ 'embed', 'iframe', 'object' ], 'element_attribute' => 'src', 'height' => 0, 'ignore_aspect_ratio' => false, 'is_oembed' => false, 'regex' => Replacer::extend_pattern( $this->provider->get_pattern(), $this->provider ), 'strip_newlines' => ! \has_blocks( $content ), 'width' => 0, ] ); if ( $attributes['is_oembed'] ) { return Template::get( $this->provider, $content, $attributes ); } \libxml_use_internal_errors( true ); $dom = new DOMDocument(); $character_replacements = self::get_character_replacements(); $dom->loadHTML( '' . \str_replace( \array_keys( $character_replacements ), \array_values( $character_replacements ), $content ) . '', \LIBXML_HTML_NOIMPLIED | \LIBXML_HTML_NODEFDTD ); $template_dom = new DOMDocument(); // detect domain if WordPress is installed on a sub domain $host = \wp_parse_url( \home_url(), \PHP_URL_HOST ); if ( ! \filter_var( $host, \FILTER_VALIDATE_IP ) ) { $host_array = \explode( '.', \str_replace( 'www.', '', $host ) ); $tld_count = \count( $host_array ); if ( $tld_count >= 3 && \strlen( $host_array[ $tld_count - 2 ] ) === 2 ) { $host = \implode( '.', \array_splice( $host_array, $tld_count - 3, 3 ) ); } else if ( $tld_count >= 2 ) { $host = \implode( '.', \array_splice( $host_array, $tld_count - 2, $tld_count ) ); } } foreach ( $attributes['elements'] as $tag ) { $replacements = []; if ( $tag === 'object' ) { $attributes['element_attribute'] = 'data'; } /** @var \DOMElement $element */ foreach ( $dom->getElementsByTagName( $tag ) as $element ) { if ( ! Embed_Privacy::get_instance()->run_checks( $attributes['additional_checks'], $element ) ) { continue; } // ignore embeds from the same (sub-)domain if ( \preg_match( '/https?:\/\/(.*\.)?' . \preg_quote( $host, '/' ) . '/', $element->getAttribute( $attributes['element_attribute'] ) ) ) { continue; } if ( $this->provider->is_unknown() ) { $embedded_host = \wp_parse_url( $element->getAttribute( $attributes['element_attribute'] ), \PHP_URL_HOST ); // embeds with relative paths have no host // and they are local by definition, so do nothing // see https://github.com/epiphyt/embed-privacy/issues/27 if ( empty( $embedded_host ) ) { return $content; } $this->provider->set_title( $embedded_host ); $this->provider->set_name( \sanitize_title( $embedded_host ) ); // check URL for available provider foreach ( Providers::get_instance()->get_list() as $provider ) { if ( $provider->is_matching( $element->getAttribute( $attributes['element_attribute'] ) ) && empty( $replacements ) ) { continue 2; } } } else { if ( empty( $attributes['regex'] ) || ! \preg_match_all( $this->provider->get_pattern(), $element->getAttribute( $attributes['element_attribute'] ), $matches ) ) { continue; } foreach ( $matches[0] as $matched_content ) { /** * Filter whether the replacement should take place for given matches. * * @since 1.10.9 * * @param bool $should_replace Whether the replacement should take place * @param string $matched_content Actual matched content * @param \epiphyt\Embed_privacy\embed\Provider $provider Provider object * @param string $content Current content * @param mixed[] $attributes Current attributes */ $should_replace = \apply_filters( 'embed_privacy_should_replace_match', true, $matched_content, $this->provider, $content, $attributes ); if ( ! $should_replace ) { continue 2; } } } /* translators: embed title */ $attributes['embed_title'] = $element->hasAttribute( 'title' ) ? $element->getAttribute( 'title' ) : ''; $attributes['embed_url'] = $element->getAttribute( $attributes['element_attribute'] ); $attributes['height'] = $element->hasAttribute( 'height' ) ? $element->getAttribute( 'height' ) : 0; $attributes['width'] = $element->hasAttribute( 'width' ) ? $element->getAttribute( 'width' ) : 0; // get overlay template as DOM element $template_dom->loadHTML( '' . \str_replace( '%', '%_epi_', Template::get( $this->provider, $dom->saveHTML( $element ), $attributes ) ) . '', \LIBXML_HTML_NOIMPLIED | \LIBXML_HTML_NODEFDTD ); $overlay = null; /** @var \DOMElement $div */ foreach ( $template_dom->getElementsByTagName( 'div' ) as $div ) { if ( \stripos( $div->getAttribute( 'class' ), 'embed-privacy-container' ) !== false ) { $overlay = $div; break; } } // store the elements to replace (see regressive loop down below) if ( $overlay instanceof DOMNode || $overlay instanceof DOMElement ) { $replacements[] = [ 'element' => $element, 'replace' => $dom->importNode( $overlay, true ), ]; } // reset embed provider name if ( $this->provider->is_unknown() ) { $this->provider->set_name( '' ); $this->provider->set_title( '' ); } } if ( ! empty( $replacements ) ) { $this->replacements = \array_merge( $this->replacements, $replacements ); Embed_Privacy::get_instance()->has_embed = true; $elements = $dom->getElementsByTagName( $tag ); $i = $elements->length - 1; // use regressive loop for replaceChild() // see: https://www.php.net/manual/en/domnode.replacechild.php#50500 while ( $i > -1 ) { $element = $elements->item( $i ); foreach ( $replacements as $replacement ) { if ( $replacement['element'] === $element ) { $element->parentNode->replaceChild( $replacement['replace'], $replacement['element'] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } } --$i; } $content = $dom->saveHTML( $dom->documentElement ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } } \libxml_use_internal_errors( false ); $i = -1; // embeds for other elements need to be handled manually if ( empty( $this->replacements ) && ! empty( $attributes['regex'] ) && ! $this->provider->is_unknown() && ! $this->provider->is_disabled() && \preg_match_all( $attributes['regex'], $content, $matches ) >= 1 ) { $ignored_attributes = [ 'data-*', 'href', ]; /** * Filter ignored attributes for matches. * * You can use * as a wildcard, e.g. data-* * * @since 1.10.11 * * @param string[] $ignored_attributes Current list of ignored attributes * @param string[] $matches List of matched content * @param \epiphyt\Embed_privacy\embed\Provider $provider Current provider */ $ignored_attributes = (array) \apply_filters( 'embed_privacy_ignored_match_attributes', $ignored_attributes, $matches[0], $this->provider ); $ignored_attributes = \implode( '|', \array_map( static function( $item ) { return \preg_quote( \trim( $item ), '/' ); }, $ignored_attributes ) ); // allow wildcard $ignored_attributes = \str_replace( '\*', '(.*)', $ignored_attributes ); foreach ( $matches[0] as $matched_content ) { ++$i; if ( \str_contains( $matched_content, 'embed-privacy-' ) ) { continue; } if ( isset( $matches['original_pattern'] ) ) { if ( empty( $matches['original_pattern'][ $i ] ) ) { continue; } // the original pattern must not be inside a href attribute if ( \preg_match( '/(' . $ignored_attributes . ')="([^"]*)' . \preg_quote( $matches['original_pattern'][ $i ], '/' ) . '/', $matched_content ) ) { continue; } } // if the content contains an embed wrapper class, that means that the // embed is broken if ( $this->provider->is_system() && \str_contains( $matched_content, 'class="wp-block-embed__wrapper' ) ) { return $content; } /** * Filter whether the replacement should take place for given matches. * * @since 1.10.9 * * @param bool $should_replace Whether the replacement should take place * @param string $matched_content Actual matched content * @param \epiphyt\Embed_privacy\embed\Provider $provider Provider object * @param string $content Current content * @param mixed[] $attributes Current attributes */ $should_replace = \apply_filters( 'embed_privacy_should_replace_match', true, $matched_content, $this->provider, $content, $attributes ); if ( $should_replace ) { $content = \str_replace( $matched_content, Template::get( $this->provider, $matched_content, $attributes ), $content ); } } } // decode to make sure there is nothing left encoded if replacements have been made // otherwise, content is untouched by DOMDocument, and we don't need a decoding // only required for WPBakery Page Builder if ( ! empty( $this->replacements ) && \str_contains( 'vc_row', $content ) ) { $content = \rawurldecode( $content ); } return \str_replace( \array_merge( [ '', '', '%20data-epi-spacing%20', '"data-epi-spacing%20', '%_epi_20data-epi-spacing%_epi_20', // % has been replaced with %_epi_ after replacing spaces ], \array_values( $character_replacements ) ), \array_merge( [ '', '', ' ', '" ', ' ', ], \array_keys( $character_replacements ) ), $content ); } /** * Set the provider for this overlay. * * @param string $content Content to get the provider from * @param string $url URL to the embedded content */ private function set_provider( $content, $url = '' ) { $current_provider = null; $providers = Providers::get_instance()->get_list(); foreach ( $providers as $provider ) { if ( ! $provider->is_matching( $content, Replacer::extend_pattern( $provider->get_pattern(), $provider ) ) && ( empty( $url ) || ! $provider->is_matching( $url ) ) ) { continue; } $current_provider = $provider; /** * Filter the overlay provider. * * @since 1.10.0 * * @param \epiphyt\Embed_Privacy\embed\Provider $provider Current provider * @param string $content Content to get the provider from * @param string $url URL to the embedded content */ $this->providers[] = \apply_filters( 'embed_privacy_overlay_provider', $current_provider, $content, $url ); } // support unknown oEmbed provider // see https://github.com/epiphyt/embed-privacy/issues/89 if ( $current_provider === null && ! empty( $url ) ) { $parsed_url = \wp_parse_url( $url ); $provider = isset( $parsed_url['host'] ) ? $parsed_url['host'] : ''; $current_provider = new Provider(); $current_provider->set_name( $provider ); $current_provider->set_title( $provider ); } // unknown embeds if ( $current_provider === null ) { $current_provider = new Provider(); $current_provider->set_pattern( \preg_quote( $url, '/' ) ); } /** * This filter is documented in inc/embed/class-replacement.php. */ $this->providers[] = \apply_filters( 'embed_privacy_overlay_provider', $current_provider, $content, $url ); } }