PluginProbe
Embed Privacy / 1.10.4
Embed Privacy v1.10.4
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.4, at inc/embed/class-replacement.php

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