PluginProbe
Embed Privacy / trunk
Embed Privacy vtrunk
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 / thumbnail / class-thumbnail.php

class-thumbnail.php in Embed Privacy trunk, at inc/thumbnail/class-thumbnail.php

560 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\thumbnail;
3
4 use epiphyt\Embed_Privacy\thumbnail\provider\SlideShare;
5 use epiphyt\Embed_Privacy\thumbnail\provider\Thumbnail_Provider;
6 use epiphyt\Embed_Privacy\thumbnail\provider\Vimeo;
7 use epiphyt\Embed_Privacy\thumbnail\provider\WordPress_TV;
8 use epiphyt\Embed_Privacy\thumbnail\provider\YouTube;
9 use WP_Post;
10
11 /**
12 * Thumbnail related functionality.
13 *
14 * @author Epiphyt
15 * @license GPL2
16 * @package epiphyt\Embed_Privacy
17 */
18 final class Thumbnail {
19 const METADATA_PREFIX = 'embed_privacy_thumbnail';
20
21 /**
22 * @var \epiphyt\Embed_Privacy\thumbnail\provider\Thumbnail_Provider[] List of thumbnail provider classes
23 */
24 public $providers = [
25 SlideShare::class,
26 Vimeo::class,
27 WordPress_TV::class,
28 YouTube::class,
29 ];
30
31 /**
32 * Initialize functionality.
33 */
34 public function init() {
35 if ( ! \get_option( 'embed_privacy_download_thumbnails' ) ) {
36 return;
37 }
38
39 \add_action( 'before_delete_post', [ self::class, 'delete_thumbnails' ] );
40 \add_action( 'init', [ $this, 'register_providers' ] );
41 \add_action( 'post_updated', [ $this, 'delete_orphaned' ], 10, 2 );
42 \add_filter( 'oembed_dataparse', [ $this, 'get_from_provider' ], 10, 3 );
43 }
44
45 /**
46 * Check and delete orphaned thumbnails.
47 *
48 * @param int $post_id The post ID
49 * @param \WP_Post $post The post object
50 */
51 public function delete_orphaned( $post_id, $post ) {
52 // don't check for orphaned if it's not a proper post update
53 // e.g. via REST API, where not all fields are updated
54 if ( empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
55 return;
56 }
57
58 $global_metadata = self::get_metadata();
59 $metadata = \get_post_meta( $post_id );
60 $supported_providers = \array_map(
61 static function( $provider ) {
62 $provider_obj = new $provider(); // phpcs:ignore NeutronStandard.Functions.VariableFunctions.VariableFunction
63
64 return $provider_obj::$name;
65 },
66 $this->providers
67 );
68
69 /**
70 * Filter the supported provider names.
71 *
72 * @since 1.7.0
73 *
74 * @param \epiphyt\Embed_Privacy\thumbnail\provider\Thumbnail_Provider[] $supported_providers Current supported provider names
75 */
76 $supported_providers = (array) \apply_filters( 'embed_privacy_thumbnail_supported_provider_names', $supported_providers );
77
78 foreach ( $metadata as $meta_key => $meta_value ) {
79 if ( ! \str_contains( $meta_key, self::METADATA_PREFIX . '_' ) ) {
80 continue;
81 }
82
83 if ( \is_array( $meta_value ) ) {
84 $meta_value = \reset( $meta_value );
85 }
86
87 foreach ( $supported_providers as $provider ) {
88 if ( \str_contains( $meta_key, '_' . $provider . '_' ) && ! \str_contains( $meta_key, '_url' ) ) {
89 $id = \str_replace( self::METADATA_PREFIX . '_' . $provider . '_', '', $meta_key );
90 $missing_id = ! \str_contains( $post->post_content, $id );
91 $missing_url = true;
92 $url = '';
93
94 if ( $missing_id && self::is_in_acf_fields( $post_id, $id ) ) {
95 $missing_id = false;
96 }
97
98 if ( $missing_id && isset( $metadata[ $meta_key . '_url' ] ) ) {
99 $url = $metadata[ $meta_key . '_url' ];
100
101 if ( \is_array( $url ) ) {
102 $url = \reset( $url );
103 }
104
105 $missing_url = ! \str_contains( $post->post_content, $url );
106
107 if ( $missing_url && self::is_in_acf_fields( $post_id, $url ) ) {
108 $missing_url = false;
109 }
110 }
111
112 $should_delete = $missing_id && $missing_url && ! self::is_in_use( $meta_value, $post_id, $global_metadata );
113
114 /**
115 * Filters whether an thumbnail marked as orphaned should be deleted.
116 *
117 * @since 1.10.0
118 *
119 * @param bool $should_delete Whether the thumbnail should be deleted
120 * @param string $id The thumbnail ID
121 * @param string $url The thumbnail URL
122 * @param int $post_id The post ID
123 * @param string $provider The provider
124 */
125 $should_delete = \apply_filters( 'embed_privacy_thumbnail_delete_orphaned', $should_delete, $id, $url, $post_id, $provider );
126
127 if ( $should_delete ) {
128 /**
129 * Fires before orphaned data are deleted.
130 *
131 * @deprecated 1.10.0 Use filter embed_privacy_thumbnail_delete_orphaned instead
132 * @since 1.8.0
133 *
134 * @param string $id The thumbnail ID
135 * @param string $url The thumbnail URL
136 * @param int $post_id The post ID
137 * @param string $provider The provider name
138 */
139 \do_action_deprecated(
140 'embed_privacy_pre_thumbnail_delete_orphaned_delete',
141 [
142 $id,
143 $url,
144 $post_id,
145 $provider,
146 ],
147 '1.10.0',
148 'embed_privacy_thumbnail_delete_orphaned'
149 );
150
151 $should_delete = ! \has_action( 'embed_privacy_pre_thumbnail_delete_orphaned_delete' );
152
153 if ( $should_delete ) {
154 self::delete( $meta_value );
155 \delete_post_meta( $post_id, $meta_key );
156 \delete_post_meta( $post_id, $meta_key . '_url' );
157 }
158 }
159
160 /**
161 * Fires after orphaned data have been checked.
162 *
163 * @since 1.7.0
164 *
165 * @param string $provider Provider name
166 * @param string $id The ID of the embedded content
167 * @param string $url The embed URL
168 * @param bool $missing_id Whether the ID is missing
169 * @param bool $missing_url Whether the URL is missing
170 * @param string $meta_value The thumbnail filename
171 * @param string $meta_key The thumbnail meta key
172 * @param \WP_Post $post The post object
173 * @param int $post_id The post ID
174 */
175 \do_action( 'embed_privacy_thumbnail_checked_orphaned', $provider, $id, $url, $missing_id, $missing_url, $meta_value, $meta_key, $post, $post_id );
176 }
177 }
178 }
179 }
180
181 /**
182 * Delete a thumbnail.
183 *
184 * @param string $filename The thumbnail filename
185 */
186 private static function delete( $filename ) {
187 $directory = self::get_directory();
188 // make sure to remove all sub-paths from the filename
189 $filename = \basename( $filename );
190
191 if ( ! \file_exists( $directory['base_dir'] . '/' . $filename ) ) {
192 return;
193 }
194
195 \wp_delete_file( $directory['base_dir'] . '/' . $filename );
196 }
197
198 /**
199 * Delete thumbnails for a given post ID.
200 *
201 * @param int $post_id Post ID
202 */
203 public static function delete_thumbnails( $post_id ) {
204 $global_metadata = self::get_metadata();
205 $metadata = \get_post_meta( $post_id );
206
207 foreach ( $metadata as $meta_key => $meta_value ) {
208 if ( ! \str_contains( $meta_key, self::METADATA_PREFIX . '_' ) ) {
209 continue;
210 }
211
212 if ( \is_array( $meta_value ) ) {
213 $meta_value = \reset( $meta_value );
214 }
215
216 if ( ! self::is_in_use( $meta_value, $post_id, $global_metadata ) ) {
217 self::delete( $meta_value );
218 }
219 }
220 }
221
222 /**
223 * Get path and URL to an embed thumbnail.
224 *
225 * @param \WP_Post $post Post object
226 * @param string $url Embedded URL
227 * @return array Thumbnail path and URL
228 */
229 public function get_data( $post, $url ) {
230 $empty = [
231 'thumbnail_path' => '',
232 'thumbnail_url' => '',
233 ];
234
235 if ( ! $post instanceof WP_Post ) {
236 return $empty;
237 }
238
239 $id = '';
240 $provider = $this->get_provider_by_url( $url );
241 $thumbnail = '';
242 $thumbnail_path = '';
243 $thumbnail_url = '';
244
245 if ( ! $provider instanceof Thumbnail_Provider ) {
246 return $empty;
247 }
248
249 $id = $provider->get_id( $url );
250 $thumbnail = \get_post_meta( $post->ID, self::METADATA_PREFIX . '_' . $provider::$name . '_' . $id, true );
251
252 /**
253 * Filter the thumbnail ID.
254 *
255 * @since 1.7.0
256 *
257 * @param string $id The thumbnail ID
258 * @param \WP_Post $post The post object
259 * @param string $url The embed URL
260 */
261 $id = \apply_filters( 'embed_privacy_thumbnail_data_id', $id, $post, $url );
262
263 /**
264 * Filter the thumbnail filename.
265 *
266 * @since 1.7.0
267 *
268 * @param string $thumbnail The thumbnail filename
269 * @param \WP_Post $post The post object
270 * @param string $url The embed URL
271 */
272 $thumbnail = \apply_filters( 'embed_privacy_thumbnail_data_filename', $thumbnail, $post, $url );
273
274 if ( $thumbnail ) {
275 $directory = self::get_directory();
276 $thumbnail_path = $directory['base_dir'] . '/' . $thumbnail;
277
278 if ( \file_exists( $thumbnail_path ) ) {
279 $thumbnail_url = $directory['base_url'] . '/' . $thumbnail;
280 }
281 }
282
283 /**
284 * Filter the thumbnail path.
285 *
286 * @since 1.7.0
287 *
288 * @param string $thumbnail The thumbnail path
289 * @param \WP_Post $post The post object
290 * @param string $url The embed URL
291 */
292 $thumbnail_path = \apply_filters( 'embed_privacy_thumbnail_data_path', $thumbnail_path, $post, $url );
293
294 /**
295 * Filter the thumbnail URL.
296 *
297 * @since 1.7.0
298 *
299 * @param string $thumbnail The thumbnail URL
300 * @param \WP_Post $post The post object
301 * @param string $url The embed URL
302 */
303 $thumbnail_url = \apply_filters( 'embed_privacy_thumbnail_data_url', $thumbnail_url, $post, $url );
304
305 return [
306 'thumbnail_path' => $thumbnail_path,
307 'thumbnail_url' => $thumbnail_url,
308 ];
309 }
310
311 /**
312 * Get the thumbnail directory and URL.
313 * Since we don't want to have a directory per site in a network, we need to
314 * get rid of the site ID in the path.
315 *
316 * @return string[] Thumbnail directory and URL
317 */
318 public static function get_directory() {
319 $upload_dir = \wp_get_upload_dir();
320
321 if ( ! $upload_dir || $upload_dir['error'] !== false ) {
322 return [
323 'base_dir' => '',
324 'base_url' => '',
325 ];
326 }
327
328 if ( ! \file_exists( $upload_dir['basedir'] . '/embed-privacy/thumbnails' ) ) {
329 \wp_mkdir_p( $upload_dir['basedir'] . '/embed-privacy/thumbnails' );
330 }
331
332 return [
333 'base_dir' => $upload_dir['basedir'] . '/embed-privacy/thumbnails',
334 'base_url' => $upload_dir['baseurl'] . '/embed-privacy/thumbnails',
335 ];
336 }
337
338 /**
339 * Get embed thumbnails from the embed provider.
340 *
341 * @param string $output The returned oEmbed HTML
342 * @param object $data A data object result from an oEmbed provider
343 * @param string $url The URL of the content to be embedded
344 * @return string The returned oEmbed HTML
345 */
346 public function get_from_provider( $output, $data, $url ) {
347 foreach ( $this->providers as $provider ) {
348 $provider_obj = new $provider(); // phpcs:ignore NeutronStandard.Functions.VariableFunctions.VariableFunction
349 $provider_obj->get( $data, $url );
350 }
351
352 /**
353 * Fires after getting data from provider.
354 *
355 * @since 1.7.0
356 *
357 * @param string $output The returned oEmbed HTML
358 * @param object $data A data object result from an oEmbed provider
359 * @param string $url The URL of the content to be embedded
360 */
361 \do_action( 'embed_privacy_thumbnail_get_from_provider', $output, $data, $url );
362
363 return $output;
364 }
365
366 /**
367 * Get all thumbnail metadata of all posts.
368 *
369 * @param string $provider Optional provider name to limit metadata
370 * @return array All thumbnail metadata
371 */
372 public static function get_metadata( $provider = '' ) {
373 global $wpdb;
374
375 if ( ! empty( $provider ) ) {
376 $key = self::METADATA_PREFIX . '_' . $provider . '_%';
377 }
378 else {
379 $key = self::METADATA_PREFIX . '_%';
380 }
381
382 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
383 return $wpdb->get_results(
384 $wpdb->prepare(
385 "SELECT DISTINCT post_id,
386 meta_key,
387 meta_value
388 FROM $wpdb->postmeta
389 WHERE meta_key LIKE %s",
390 $key
391 ),
392 \ARRAY_A
393 );
394 // phpcs:enable
395 }
396
397 /**
398 * Get a thumbnail provider by an URL.
399 *
400 * @param string $url Embed URL
401 * @return \epiphyt\Embed_Privacy\thumbnail\provider\Thumbnail_Provider|null Thumbnail provider object or null
402 */
403 public function get_provider_by_url( $url ) {
404 foreach ( $this->providers as $provider ) {
405 $provider_obj = new $provider(); // phpcs:ignore NeutronStandard.Functions.VariableFunctions.VariableFunction
406
407 foreach ( $provider_obj::$domains as $domain ) {
408 if ( \str_contains( $url, $domain ) ) {
409 return $provider_obj;
410 }
411 }
412 }
413
414 return null;
415 }
416
417 /**
418 * Get the provider titles
419 *
420 * @return string[] List of provider titles
421 */
422 public function get_provider_titles() {
423 $titles = \array_map(
424 static function( $provider ) {
425 $provider_obj = new $provider(); // phpcs:ignore NeutronStandard.Functions.VariableFunctions.VariableFunction
426
427 return $provider_obj->get_title();
428 },
429 $this->providers
430 );
431
432 \asort( $titles, \SORT_NATURAL );
433
434 return $titles;
435 }
436
437 /**
438 * Check whether a thumbnail is in use in ACF fields.
439 *
440 * @param int $post_id Current post ID
441 * @param string $content Content to search for
442 * @param array $fields List of ACF fields
443 * @return bool Whether thumbnail is in use in ACF fields
444 */
445 private function is_in_acf_fields( $post_id, $content, array $fields = [] ) {
446 // phpcs:disable WordPress.Security.NonceVerification.Missing
447 if ( ! \function_exists( 'get_fields' ) ) {
448 return false;
449 }
450
451 if ( empty( $fields ) ) {
452 if ( empty( $_POST['acf'] ) ) {
453 return false;
454 }
455
456 // we need to use the post fields since get_fields() doesn't contain
457 // the actual value since it will be stored after the post is saved
458 $fields = \wp_unslash( $_POST['acf'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
459 }
460 // phpcs:enable
461
462 $is_in_fields = false;
463
464 foreach ( $fields as $field ) {
465 if ( \is_array( $field ) || \is_object( $field ) ) {
466 if ( self::is_in_acf_fields( $post_id, $content, (array) $field ) ) {
467 $is_in_fields = true;
468 break;
469 }
470 }
471 else if ( \str_contains( (string) $field, $content ) ) {
472 $is_in_fields = true;
473 break;
474 }
475 }
476
477 return $is_in_fields;
478 }
479
480 /**
481 * Check whether a thumbnail is in use in another post.
482 *
483 * @param string $meta_value The thumbnail filename
484 * @param int $post_id The post ID of the current post
485 * @param array $global_metadata Global metadata to check in
486 * @return bool Whether a thumbnail is in use in another post
487 */
488 private static function is_in_use( $meta_value, $post_id, $global_metadata ) {
489 $is_in_use = false;
490
491 foreach ( $global_metadata as $global_meta_value ) {
492 if ( (int) $global_meta_value['post_id'] === $post_id ) {
493 continue;
494 }
495
496 if ( $global_meta_value['meta_value'] === $meta_value ) {
497 $is_in_use = true;
498 break;
499 }
500 }
501
502 return $is_in_use;
503 }
504
505 /**
506 * Register thumbnail providers.
507 */
508 public function register_providers() {
509 /**
510 * Filter the supported providers.
511 *
512 * @since 1.9.0
513 *
514 * @param \epiphyt\Embed_Privacy\thumbnail\provider\Thumbnail_Provider[] $supported_providers Current supported providers
515 */
516 $this->providers = (array) \apply_filters( 'embed_privacy_thumbnail_providers', $this->providers );
517
518 $providers = [
519 \_x( 'SlideShare', 'embed provider', 'embed-privacy' ),
520 \_x( 'Vimeo', 'embed provider', 'embed-privacy' ),
521 \_x( 'YouTube', 'embed provider', 'embed-privacy' ),
522 ];
523
524 /**
525 * Filter the supported providers.
526 *
527 * @deprecated 1.9.0
528 * @since 1.7.0
529 *
530 * @param array $supported_providers Current supported providers
531 */
532 $providers = \apply_filters_deprecated( 'embed_privacy_thumbnail_supported_providers', $providers, '1.9.0', 'embed_privacy_thumbnail_providers' );
533
534 foreach ( $this->providers as $provider ) {
535 try {
536 $provider_obj = new $provider(); // phpcs:ignore NeutronStandard.Functions.VariableFunctions.VariableFunction
537
538 if ( ! $provider_obj instanceof Thumbnail_Provider ) {
539 \wp_die(
540 \sprintf(
541 /* translators: PHP class */
542 \esc_html__( 'Thumbnail provider is not an instance of %s', 'embed-privacy' ),
543 'epiphyt\Embed_Privacy\thumbnail\provider\Thumbnail_Provider'
544 )
545 );
546 }
547 }
548 catch ( \Exception $e ) {
549 \wp_die(
550 \sprintf(
551 /* translators: PHP class */
552 \esc_html__( 'Thumbnail provider is not an instance of %s', 'embed-privacy' ),
553 'epiphyt\Embed_Privacy\thumbnail\provider\Thumbnail_Provider'
554 )
555 );
556 }
557 }
558 }
559 }
560