PluginProbe
Embed Privacy / 1.10.0
Embed Privacy v1.10.0
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.0, at inc/embed/class-replacement.php

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