PluginProbe
Embed Privacy / 1.10.9
Embed Privacy v1.10.9
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.9, at inc/embed/class-replacement.php

524 lines 15.6 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 ( empty( $matches['original_pattern'][ $i ] ) ) {
394 continue;
395 }
396
397 // the original pattern must not be inside a href attribute
398 if ( \str_contains( $matched_content, 'href="' . $matches['original_pattern'][ $i ] ) ) {
399 continue;
400 }
401
402 // if the content contains an embed wrapper class, that means that the
403 // embed is broken
404 if (
405 $this->provider->is_system()
406 && \str_contains( $matched_content, 'class="wp-block-embed__wrapper' )
407 ) {
408 return $content;
409 }
410
411 /**
412 * Filter whether the replacement should take place for given matches.
413 *
414 * @since 1.10.9
415 *
416 * @param bool $should_replace Whether the replacement should take place
417 * @param string $matched_content Actual matched content
418 * @param \epiphyt\Embed_privacy\embed\Provider $provider Provider object
419 * @param string $content Current content
420 * @param mixed[] $attributes Current attributes
421 */
422 $should_replace = \apply_filters( 'embed_privacy_should_replace_match', true, $matched_content, $this->provider, $content, $attributes );
423
424 if ( $should_replace ) {
425 $content = \str_replace(
426 $matched_content,
427 Template::get(
428 $this->provider,
429 $matched_content,
430 $attributes
431 ),
432 $content
433 );
434 }
435 }
436 }
437
438 // decode to make sure there is nothing left encoded if replacements have been made
439 // otherwise, content is untouched by DOMDocument, and we don't need a decoding
440 // only required for WPBakery Page Builder
441 if ( ! empty( $this->replacements ) && \str_contains( 'vc_row', $content ) ) {
442 $content = \rawurldecode( $content );
443 }
444
445 return \str_replace(
446 \array_merge(
447 [
448 '<html><meta charset="utf-8">',
449 '</html>',
450 '%20data-epi-spacing%20',
451 '%_epi_20data-epi-spacing%_epi_20', // % has been replaced with %_epi_ after replacing spaces
452 ],
453 \array_values( $character_replacements )
454 ),
455 \array_merge(
456 [
457 '',
458 '',
459 ' ',
460 ' ',
461 ],
462 \array_keys( $character_replacements )
463 ),
464 $content
465 );
466 }
467
468 /**
469 * Set the provider for this overlay.
470 *
471 * @param string $content Content to get the provider from
472 * @param string $url URL to the embedded content
473 */
474 private function set_provider( $content, $url = '' ) {
475 $current_provider = null;
476 $providers = Providers::get_instance()->get_list();
477
478 foreach ( $providers as $provider ) {
479 if (
480 ! $provider->is_matching(
481 $content,
482 Replacer::extend_pattern( $provider->get_pattern(), $provider )
483 )
484 && ( empty( $url ) || ! $provider->is_matching( $url ) )
485 ) {
486 continue;
487 }
488
489 $current_provider = $provider;
490
491 /**
492 * Filter the overlay provider.
493 *
494 * @since 1.10.0
495 *
496 * @param \epiphyt\Embed_Privacy\embed\Provider $provider Current provider
497 * @param string $content Content to get the provider from
498 * @param string $url URL to the embedded content
499 */
500 $this->providers[] = \apply_filters( 'embed_privacy_overlay_provider', $current_provider, $content, $url );
501 }
502
503 // support unknown oEmbed provider
504 // see https://github.com/epiphyt/embed-privacy/issues/89
505 if ( $current_provider === null && ! empty( $url ) ) {
506 $parsed_url = \wp_parse_url( $url );
507 $provider = isset( $parsed_url['host'] ) ? $parsed_url['host'] : '';
508 $current_provider = new Provider();
509 $current_provider->set_name( $provider );
510 $current_provider->set_title( $provider );
511 }
512
513 // unknown embeds
514 if ( $current_provider === null ) {
515 $current_provider = new Provider();
516 }
517
518 /**
519 * This filter is documented in inc/embed/class-replacement.php.
520 */
521 $this->providers[] = \apply_filters( 'embed_privacy_overlay_provider', $current_provider, $content, $url );
522 }
523 }
524