PluginProbe
Embed Privacy / 1.10.10
Embed Privacy v1.10.10
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.10, at inc/embed/class-replacement.php

526 lines 15.7 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_all( $attributes['regex'], $element->getAttribute( $attributes['element_attribute'] ), $matches )
267 ) {
268 continue;
269 }
270
271 foreach ( $matches[0] as $matched_content ) {
272 /**
273 * Filter whether the replacement should take place for given matches.
274 *
275 * @since 1.10.9
276 *
277 * @param bool $should_replace Whether the replacement should take place
278 * @param string $matched_content Actual matched content
279 * @param \epiphyt\Embed_privacy\embed\Provider $provider Provider object
280 * @param string $content Current content
281 * @param mixed[] $attributes Current attributes
282 */
283 $should_replace = \apply_filters( 'embed_privacy_should_replace_match', true, $matched_content, $this->provider, $content, $attributes );
284
285 if ( ! $should_replace ) {
286 continue 2;
287 }
288 }
289
290 if ( $this->provider->is_unknown() ) {
291 $embedded_host = \wp_parse_url( $element->getAttribute( $attributes['element_attribute'] ), \PHP_URL_HOST );
292
293 // embeds with relative paths have no host
294 // and they are local by definition, so do nothing
295 // see https://github.com/epiphyt/embed-privacy/issues/27
296 if ( empty( $embedded_host ) ) {
297 return $content;
298 }
299
300 $this->provider->set_title( $embedded_host );
301 $this->provider->set_name( \sanitize_title( $embedded_host ) );
302
303 // check URL for available provider
304 foreach ( Providers::get_instance()->get_list() as $provider ) {
305 if (
306 $provider->is_matching( $element->getAttribute( $attributes['element_attribute'] ) )
307 && empty( $replacements )
308 ) {
309 continue 2;
310 }
311 }
312 }
313
314 /* translators: embed title */
315 $attributes['embed_title'] = $element->hasAttribute( 'title' ) ? $element->getAttribute( 'title' ) : '';
316 $attributes['embed_url'] = $element->getAttribute( $attributes['element_attribute'] );
317 $attributes['height'] = $element->hasAttribute( 'height' ) ? $element->getAttribute( 'height' ) : 0;
318 $attributes['width'] = $element->hasAttribute( 'width' ) ? $element->getAttribute( 'width' ) : 0;
319
320 // get overlay template as DOM element
321 $template_dom->loadHTML(
322 '<html><meta charset="utf-8">' . \str_replace( '%', '%_epi_', Template::get( $this->provider, $dom->saveHTML( $element ), $attributes ) ) . '</html>',
323 \LIBXML_HTML_NOIMPLIED | \LIBXML_HTML_NODEFDTD
324 );
325 $overlay = null;
326
327 /** @var \DOMElement $div */
328 foreach ( $template_dom->getElementsByTagName( 'div' ) as $div ) {
329 if ( \stripos( $div->getAttribute( 'class' ), 'embed-privacy-container' ) !== false ) {
330 $overlay = $div;
331 break;
332 }
333 }
334
335 // store the elements to replace (see regressive loop down below)
336 if ( $overlay instanceof DOMNode || $overlay instanceof DOMElement ) {
337 $replacements[] = [
338 'element' => $element,
339 'replace' => $dom->importNode( $overlay, true ),
340 ];
341 }
342
343 // reset embed provider name
344 if ( $this->provider->is_unknown() ) {
345 $this->provider->set_name( '' );
346 $this->provider->set_title( '' );
347 }
348 }
349
350 if ( ! empty( $replacements ) ) {
351 $this->replacements = \array_merge( $this->replacements, $replacements );
352 Embed_Privacy::get_instance()->has_embed = true;
353 $elements = $dom->getElementsByTagName( $tag );
354 $i = $elements->length - 1;
355
356 // use regressive loop for replaceChild()
357 // see: https://www.php.net/manual/en/domnode.replacechild.php#50500
358 while ( $i > -1 ) {
359 $element = $elements->item( $i );
360
361 foreach ( $replacements as $replacement ) {
362 if ( $replacement['element'] === $element ) {
363 $element->parentNode->replaceChild( $replacement['replace'], $replacement['element'] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
364 }
365 }
366
367 --$i;
368 }
369
370 $content = $dom->saveHTML( $dom->documentElement ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
371 }
372 }
373
374 \libxml_use_internal_errors( false );
375
376 $i = -1;
377
378 // embeds for other elements need to be handled manually
379 if (
380 empty( $this->replacements )
381 && ! empty( $attributes['regex'] )
382 && ! $this->provider->is_unknown()
383 && ! $this->provider->is_disabled()
384 && \preg_match_all( $attributes['regex'], $content, $matches ) >= 1
385 ) {
386 foreach ( $matches[0] as $matched_content ) {
387 ++$i;
388
389 if ( \str_contains( $matched_content, 'embed-privacy-' ) ) {
390 continue;
391 }
392
393 if ( isset( $matches['original_pattern'] ) ) {
394 if ( empty( $matches['original_pattern'][ $i ] ) ) {
395 continue;
396 }
397
398 // the original pattern must not be inside a href attribute
399 if ( \str_contains( $matched_content, 'href="' . $matches['original_pattern'][ $i ] ) ) {
400 continue;
401 }
402 }
403
404 // if the content contains an embed wrapper class, that means that the
405 // embed is broken
406 if (
407 $this->provider->is_system()
408 && \str_contains( $matched_content, 'class="wp-block-embed__wrapper' )
409 ) {
410 return $content;
411 }
412
413 /**
414 * Filter whether the replacement should take place for given matches.
415 *
416 * @since 1.10.9
417 *
418 * @param bool $should_replace Whether the replacement should take place
419 * @param string $matched_content Actual matched content
420 * @param \epiphyt\Embed_privacy\embed\Provider $provider Provider object
421 * @param string $content Current content
422 * @param mixed[] $attributes Current attributes
423 */
424 $should_replace = \apply_filters( 'embed_privacy_should_replace_match', true, $matched_content, $this->provider, $content, $attributes );
425
426 if ( $should_replace ) {
427 $content = \str_replace(
428 $matched_content,
429 Template::get(
430 $this->provider,
431 $matched_content,
432 $attributes
433 ),
434 $content
435 );
436 }
437 }
438 }
439
440 // decode to make sure there is nothing left encoded if replacements have been made
441 // otherwise, content is untouched by DOMDocument, and we don't need a decoding
442 // only required for WPBakery Page Builder
443 if ( ! empty( $this->replacements ) && \str_contains( 'vc_row', $content ) ) {
444 $content = \rawurldecode( $content );
445 }
446
447 return \str_replace(
448 \array_merge(
449 [
450 '<html><meta charset="utf-8">',
451 '</html>',
452 '%20data-epi-spacing%20',
453 '%_epi_20data-epi-spacing%_epi_20', // % has been replaced with %_epi_ after replacing spaces
454 ],
455 \array_values( $character_replacements )
456 ),
457 \array_merge(
458 [
459 '',
460 '',
461 ' ',
462 ' ',
463 ],
464 \array_keys( $character_replacements )
465 ),
466 $content
467 );
468 }
469
470 /**
471 * Set the provider for this overlay.
472 *
473 * @param string $content Content to get the provider from
474 * @param string $url URL to the embedded content
475 */
476 private function set_provider( $content, $url = '' ) {
477 $current_provider = null;
478 $providers = Providers::get_instance()->get_list();
479
480 foreach ( $providers as $provider ) {
481 if (
482 ! $provider->is_matching(
483 $content,
484 Replacer::extend_pattern( $provider->get_pattern(), $provider )
485 )
486 && ( empty( $url ) || ! $provider->is_matching( $url ) )
487 ) {
488 continue;
489 }
490
491 $current_provider = $provider;
492
493 /**
494 * Filter the overlay provider.
495 *
496 * @since 1.10.0
497 *
498 * @param \epiphyt\Embed_Privacy\embed\Provider $provider Current provider
499 * @param string $content Content to get the provider from
500 * @param string $url URL to the embedded content
501 */
502 $this->providers[] = \apply_filters( 'embed_privacy_overlay_provider', $current_provider, $content, $url );
503 }
504
505 // support unknown oEmbed provider
506 // see https://github.com/epiphyt/embed-privacy/issues/89
507 if ( $current_provider === null && ! empty( $url ) ) {
508 $parsed_url = \wp_parse_url( $url );
509 $provider = isset( $parsed_url['host'] ) ? $parsed_url['host'] : '';
510 $current_provider = new Provider();
511 $current_provider->set_name( $provider );
512 $current_provider->set_title( $provider );
513 }
514
515 // unknown embeds
516 if ( $current_provider === null ) {
517 $current_provider = new Provider();
518 }
519
520 /**
521 * This filter is documented in inc/embed/class-replacement.php.
522 */
523 $this->providers[] = \apply_filters( 'embed_privacy_overlay_provider', $current_provider, $content, $url );
524 }
525 }
526