PluginProbe
Embed Privacy / 1.10.7
Embed Privacy v1.10.7
1.14.0 1.13.0 trunk 0.1 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.1.3 1.10.0 1.10.1 1.10.10 1.10.2 1.10.3 1.10.4 1.10.5 1.10.6 1.10.7 1.10.8 1.10.9 1.11.0 1.11.1 1.11.2 All 68 releases
embed-privacy / inc / embed / class-replacement.php

class-replacement.php in Embed Privacy 1.10.7, at inc/embed/class-replacement.php

490 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace epiphyt\Embed_Privacy\embed;
3
4 use DOMDocument;
5 use DOMElement;
6 use DOMNode;
7 use epiphyt\Embed_Privacy\data\Providers;
8 use epiphyt\Embed_Privacy\data\Replacer;
9 use epiphyt\Embed_Privacy\Embed_Privacy;
10
11 /**
12 * Embed replacement representation.
13 *
14 * @author Epiphyt
15 * @license GPL2
16 * @package epiphyt\Embed_Privacy
17 * @since 1.10.0
18 */
19 final class Replacement {
20 /**
21 * @var string Original content
22 */
23 private $content = '';
24
25 /**
26 * @var \epiphyt\Embed_privacy\embed\Provider|null Current processed provider for this replacement
27 */
28 private $provider;
29
30 /**
31 * @var \epiphyt\Embed_privacy\embed\Provider[] List of matching providers for this replacement
32 */
33 private $providers = [];
34
35 /**
36 * @var array List of replacements
37 */
38 private $replacements = [];
39
40 /**
41 * Replacement constructor
42 *
43 * @param string $content Original embedded content
44 * @param string $url Embedded content URL
45 */
46 public function __construct( $content, $url = '' ) {
47 $this->content = $content;
48 $this->set_provider( $content, $url );
49 }
50
51 /**
52 * Get the content with an overlay.
53 *
54 * @param array $attributes Embed attributes
55 * @param \epiphyt\Embed_Privacy\embed\Provider|null $provider Embed provider
56 * @return string Content with embeds replaced by an overlay
57 */
58 public function get( array $attributes = [], $provider = null ) {
59 /**
60 * Filter the content after it has been replaced with an overlay.
61 *
62 * @since 1.10.0
63 *
64 * @param string $content Replaced content
65 */
66 $content = (string) \apply_filters( 'embed_privacy_overlay_replaced_content', $this->content );
67
68 /**
69 * If set to true, unknown providers are not handled via Embed Privacy.
70 *
71 * @since 1.5.0
72 *
73 * @param bool $ignore_unknown Whether unknown providers should be ignored
74 * @param string $content The original content
75 */
76 $ignore_unknown_providers = (bool) \apply_filters( 'embed_privacy_ignore_unknown_providers', false, $content );
77
78 // get default external content
79 // special case for youtube-nocookie.com as it is part of YouTube provider
80 if ( ! $ignore_unknown_providers || \str_contains( $content, 'youtube-nocookie.com' ) ) {
81 if ( $provider instanceof Provider ) {
82 $this->provider = $provider;
83 $content = $this->replace( $content, $attributes );
84 }
85 else {
86 foreach ( $this->get_providers() as $provider ) {
87 $this->provider = $provider;
88 $content = $this->replace( $content, $attributes );
89 }
90 }
91
92 $this->provider = null;
93 }
94
95 return $content;
96 }
97
98 /**
99 * Get a list of characters to replace to prevent problems with DOMDocument.
100 *
101 * @return array List of character replacements
102 */
103 private static function get_character_replacements() {
104 $replacements = [
105 '%' => '@@epi_percentage',
106 ' ' => ' data-epi-spacing ',
107 '[' => '@@epi_square_bracket_start',
108 ']' => '@@epi_square_bracket_end',
109 '{' => '@@epi_curly_bracket_start',
110 '}' => '@@epi_curly_bracket_end',
111 ];
112
113 /**
114 * Filter character replacements.
115 *
116 * @since 1.10.0
117 *
118 * @param array $replacements Current replacements
119 */
120 $replacements = (array) \apply_filters( 'embed_privacy_overlay_character_replacements', $replacements );
121
122 return $replacements;
123 }
124
125 /**
126 * Get the current provider.
127 *
128 * @deprecated 1.10.4
129 *
130 * @return \epiphyt\Embed_privacy\embed\Provider|null Provider object
131 */
132 public function get_provider() {
133 \_doing_it_wrong(
134 __METHOD__,
135 \esc_html__( 'This method is outdated and will be removed in the future.', 'embed-privacy' ),
136 '1.10.4'
137 );
138
139 return $this->provider;
140 }
141
142 /**
143 * Get all providers to replace an embed of.
144 *
145 * @return \epiphyt\Embed_privacy\embed\Provider[] Provider object
146 */
147 public function get_providers() {
148 return $this->providers;
149 }
150
151 /**
152 * Replace content with an overlay and print assets.
153 *
154 * @param string $content Content to replace embeds in
155 * @param array $attributes Additional attributes
156 * @return string Replaced content
157 */
158 private function replace( $content, array $attributes ) {
159 $new_content = $this->replace_content( $content, $attributes );
160
161 if ( $new_content !== $content ) {
162 Embed_Privacy::get_instance()->has_embed = true;
163 Embed_Privacy::get_instance()->frontend->print_assets();
164 $content = $new_content;
165 }
166
167 return $content;
168 }
169
170 /**
171 * Replace embedded content with an overlay.
172 *
173 * @param string $content Content to replace embeds in
174 * @param array $attributes Additional attributes
175 * @return string Updated content
176 */
177 private function replace_content( $content, array $attributes ) {
178 if ( empty( $content ) ) {
179 return $content;
180 }
181
182 if ( ! $this->provider instanceof Provider ) {
183 return $content;
184 }
185
186 /**
187 * Filter whether to ignore this embed.
188 *
189 * @since 1.9.0
190 *
191 * @param bool $ignore_embed Whether to ignore this embed
192 * @param string $content The original content
193 * @param string $provider_title Embed provider title
194 * @param string $provider_name Embed provider name
195 * @param array $attributes Additional attributes
196 */
197 $ignore_embed = (bool) \apply_filters( 'embed_privacy_ignore_embed', false, $content, $this->provider->get_title(), $this->provider->get_name(), $attributes );
198
199 if ( $ignore_embed ) {
200 return $content;
201 }
202
203 $attributes = \wp_parse_args( $attributes, [
204 'additional_checks' => [],
205 'elements' => [ 'embed', 'iframe', 'object' ],
206 'element_attribute' => 'src',
207 'height' => 0,
208 'ignore_aspect_ratio' => false,
209 'is_oembed' => false,
210 'regex' => Replacer::extend_pattern( $this->provider->get_pattern(), $this->provider ),
211 'strip_newlines' => ! \has_blocks( $content ),
212 'width' => 0,
213 ] );
214
215 if ( $attributes['is_oembed'] ) {
216 return Template::get( $this->provider, $content, $attributes );
217 }
218
219 \libxml_use_internal_errors( true );
220 $dom = new DOMDocument();
221 $character_replacements = self::get_character_replacements();
222 $dom->loadHTML(
223 '<html><meta charset="utf-8">' . \str_replace(
224 \array_keys( $character_replacements ),
225 \array_values( $character_replacements ),
226 $content
227 ) . '</html>',
228 \LIBXML_HTML_NOIMPLIED | \LIBXML_HTML_NODEFDTD
229 );
230 $template_dom = new DOMDocument();
231 // detect domain if WordPress is installed on a sub domain
232 $host = \wp_parse_url( \home_url(), \PHP_URL_HOST );
233
234 if ( ! \filter_var( $host, \FILTER_VALIDATE_IP ) ) {
235 $host_array = \explode( '.', \str_replace( 'www.', '', $host ) );
236 $tld_count = \count( $host_array );
237
238 if ( $tld_count >= 3 && \strlen( $host_array[ $tld_count - 2 ] ) === 2 ) {
239 $host = \implode( '.', \array_splice( $host_array, $tld_count - 3, 3 ) );
240 }
241 else if ( $tld_count >= 2 ) {
242 $host = \implode( '.', \array_splice( $host_array, $tld_count - 2, $tld_count ) );
243 }
244 }
245
246 foreach ( $attributes['elements'] as $tag ) {
247 $replacements = [];
248
249 if ( $tag === 'object' ) {
250 $attributes['element_attribute'] = 'data';
251 }
252
253 /** @var \DOMElement $element */
254 foreach ( $dom->getElementsByTagName( $tag ) as $element ) {
255 if ( ! Embed_Privacy::get_instance()->run_checks( $attributes['additional_checks'], $element ) ) {
256 continue;
257 }
258
259 // ignore embeds from the same (sub-)domain
260 if ( \preg_match( '/https?:\/\/(.*\.)?' . \preg_quote( $host, '/' ) . '/', $element->getAttribute( $attributes['element_attribute'] ) ) ) {
261 continue;
262 }
263
264 if (
265 ! empty( $attributes['regex'] )
266 && ! \preg_match( $attributes['regex'], $element->getAttribute( $attributes['element_attribute'] ) )
267 ) {
268 continue;
269 }
270
271 if ( $this->provider->is_unknown() ) {
272 $embedded_host = \wp_parse_url( $element->getAttribute( $attributes['element_attribute'] ), \PHP_URL_HOST );
273
274 // embeds with relative paths have no host
275 // and they are local by definition, so do nothing
276 // see https://github.com/epiphyt/embed-privacy/issues/27
277 if ( empty( $embedded_host ) ) {
278 return $content;
279 }
280
281 $this->provider->set_title( $embedded_host );
282 $this->provider->set_name( \sanitize_title( $embedded_host ) );
283
284 // check URL for available provider
285 foreach ( Providers::get_instance()->get_list() as $provider ) {
286 if (
287 $provider->is_matching( $element->getAttribute( $attributes['element_attribute'] ) )
288 && empty( $replacements )
289 ) {
290 continue 2;
291 }
292 }
293 }
294
295 /* translators: embed title */
296 $attributes['embed_title'] = $element->hasAttribute( 'title' ) ? $element->getAttribute( 'title' ) : '';
297 $attributes['embed_url'] = $element->getAttribute( $attributes['element_attribute'] );
298 $attributes['height'] = $element->hasAttribute( 'height' ) ? $element->getAttribute( 'height' ) : 0;
299 $attributes['width'] = $element->hasAttribute( 'width' ) ? $element->getAttribute( 'width' ) : 0;
300
301 // get overlay template as DOM element
302 $template_dom->loadHTML(
303 '<html><meta charset="utf-8">' . \str_replace( '%', '%_epi_', Template::get( $this->provider, $dom->saveHTML( $element ), $attributes ) ) . '</html>',
304 \LIBXML_HTML_NOIMPLIED | \LIBXML_HTML_NODEFDTD
305 );
306 $overlay = null;
307
308 /** @var \DOMElement $div */
309 foreach ( $template_dom->getElementsByTagName( 'div' ) as $div ) {
310 if ( \stripos( $div->getAttribute( 'class' ), 'embed-privacy-container' ) !== false ) {
311 $overlay = $div;
312 break;
313 }
314 }
315
316 // store the elements to replace (see regressive loop down below)
317 if ( $overlay instanceof DOMNode || $overlay instanceof DOMElement ) {
318 $replacements[] = [
319 'element' => $element,
320 'replace' => $dom->importNode( $overlay, true ),
321 ];
322 }
323
324 // reset embed provider name
325 if ( $this->provider->is_unknown() ) {
326 $this->provider->set_name( '' );
327 $this->provider->set_title( '' );
328 }
329 }
330
331 if ( ! empty( $replacements ) ) {
332 $this->replacements = \array_merge( $this->replacements, $replacements );
333 Embed_Privacy::get_instance()->has_embed = true;
334 $elements = $dom->getElementsByTagName( $tag );
335 $i = $elements->length - 1;
336
337 // use regressive loop for replaceChild()
338 // see: https://www.php.net/manual/en/domnode.replacechild.php#50500
339 while ( $i > -1 ) {
340 $element = $elements->item( $i );
341
342 foreach ( $replacements as $replacement ) {
343 if ( $replacement['element'] === $element ) {
344 $element->parentNode->replaceChild( $replacement['replace'], $replacement['element'] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
345 }
346 }
347
348 --$i;
349 }
350
351 $content = $dom->saveHTML( $dom->documentElement ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
352 }
353 }
354
355 \libxml_use_internal_errors( false );
356
357 $i = -1;
358
359 // embeds for other elements need to be handled manually
360 if (
361 empty( $this->replacements )
362 && ! empty( $attributes['regex'] )
363 && ! $this->provider->is_unknown()
364 && ! $this->provider->is_disabled()
365 && \preg_match_all( $attributes['regex'], $content, $matches ) >= 1
366 ) {
367 foreach ( $matches[0] as $matched_content ) {
368 ++$i;
369
370 if ( \str_contains( $matched_content, 'embed-privacy-' ) ) {
371 continue;
372 }
373
374 if ( empty( $matches['original_pattern'][ $i ] ) ) {
375 continue;
376 }
377
378 // the original pattern must not be inside a href attribute
379 if ( \str_contains( $matched_content, 'href="' . $matches['original_pattern'][ $i ] ) ) {
380 continue;
381 }
382
383 // if the content contains an embed wrapper class, that means that the
384 // embed is broken
385 if (
386 $this->provider->is_system()
387 && \str_contains( $matched_content, 'class="wp-block-embed__wrapper' )
388 ) {
389 return $content;
390 }
391
392 $content = \str_replace(
393 $matched_content,
394 Template::get(
395 $this->provider,
396 $matched_content,
397 $attributes
398 ),
399 $content
400 );
401 }
402 }
403
404 // decode to make sure there is nothing left encoded if replacements have been made
405 // otherwise, content is untouched by DOMDocument, and we don't need a decoding
406 // only required for WPBakery Page Builder
407 if ( ! empty( $this->replacements ) && \str_contains( 'vc_row', $content ) ) {
408 $content = \rawurldecode( $content );
409 }
410
411 return \str_replace(
412 \array_merge(
413 [
414 '<html><meta charset="utf-8">',
415 '</html>',
416 '%20data-epi-spacing%20',
417 '%_epi_20data-epi-spacing%_epi_20', // % has been replaced with %_epi_ after replacing spaces
418 ],
419 \array_values( $character_replacements )
420 ),
421 \array_merge(
422 [
423 '',
424 '',
425 ' ',
426 ' ',
427 ],
428 \array_keys( $character_replacements )
429 ),
430 $content
431 );
432 }
433
434 /**
435 * Set the provider for this overlay.
436 *
437 * @param string $content Content to get the provider from
438 * @param string $url URL to the embedded content
439 */
440 private function set_provider( $content, $url = '' ) {
441 $current_provider = null;
442 $providers = Providers::get_instance()->get_list();
443
444 foreach ( $providers as $provider ) {
445 if (
446 ! $provider->is_matching(
447 $content,
448 Replacer::extend_pattern( $provider->get_pattern(), $provider )
449 )
450 && ( empty( $url ) || ! $provider->is_matching( $url ) )
451 ) {
452 continue;
453 }
454
455 $current_provider = $provider;
456
457 /**
458 * Filter the overlay provider.
459 *
460 * @since 1.10.0
461 *
462 * @param \epiphyt\Embed_Privacy\embed\Provider $provider Current provider
463 * @param string $content Content to get the provider from
464 * @param string $url URL to the embedded content
465 */
466 $this->providers[] = \apply_filters( 'embed_privacy_overlay_provider', $current_provider, $content, $url );
467 }
468
469 // support unknown oEmbed provider
470 // see https://github.com/epiphyt/embed-privacy/issues/89
471 if ( $current_provider === null && ! empty( $url ) ) {
472 $parsed_url = \wp_parse_url( $url );
473 $provider = isset( $parsed_url['host'] ) ? $parsed_url['host'] : '';
474 $current_provider = new Provider();
475 $current_provider->set_name( $provider );
476 $current_provider->set_title( $provider );
477 }
478
479 // unknown embeds
480 if ( $current_provider === null ) {
481 $current_provider = new Provider();
482 }
483
484 /**
485 * This filter is documented in inc/embed/class-replacement.php.
486 */
487 $this->providers[] = \apply_filters( 'embed_privacy_overlay_provider', $current_provider, $content, $url );
488 }
489 }
490