PluginProbe
Embed Privacy / 1.10.2
Embed Privacy v1.10.2
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.2, at inc/embed/class-replacement.php

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