PluginProbe
Media Cloud Sync / 1.3.0
Media Cloud Sync v1.3.0
1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 All 34 releases
media-cloud-sync / includes / filters / content.php

content.php in Media Cloud Sync 1.3.0, at includes/filters/content.php

701 lines 18.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Dudlewebs\WPMCS;
3
4 defined('ABSPATH') || exit;
5
6 class FilterContent {
7 private static $instance = null;
8 private $assets_url;
9 private $version;
10 private $token;
11
12
13 protected $post_meta_key;
14 protected $query_cache = [];
15
16 /**
17 * Admin constructor.
18 * @since 1.0.0
19 */
20 public function __construct() {
21 $this->assets_url = WPMCS_ASSETS_URL;
22 $this->version = WPMCS_VERSION;
23 $this->token = WPMCS_TOKEN;
24
25 $this->post_meta_key = Schema::getConstant('CONTENT_META_KEY');
26
27 // Initialize setup
28 $this->registerActions();
29 }
30
31 /**
32 * Register integration access
33 */
34 public function registerActions() {
35 // Posts
36 add_action( 'the_post',[ $this, 'filter_post_data' ] );
37 add_filter( 'content_pagination',[ $this, 'filter_content_pagination' ] );
38 add_filter( 'the_content',[ $this, 'filter_post' ], 100 );
39 add_filter( 'the_excerpt',[ $this, 'filter_post' ], 100 );
40 add_filter( 'rss_enclosure',[ $this, 'filter_post' ], 100 );
41 add_filter( 'content_edit_pre',[ $this, 'filter_post' ] );
42 add_filter( 'excerpt_edit_pre',[ $this, 'filter_post' ] );
43 add_filter( 'wpmcs_filter_post',[ $this, 'filter_post' ] );
44
45 // Post edit screen use Backward compatibility
46 add_filter( 'content_save_pre', [ $this, 'filter_post_backward' ] );
47 add_filter( 'excerpt_save_pre', [ $this, 'filter_post_backward' ] );
48 }
49
50
51 /**
52 * Filter post data.
53 *
54 * @param WP_Post $post
55 */
56 public function filter_post_data( $post ) {
57 global $pages;
58
59 $cache = Utils::get_meta($post->ID, 'items', [], $this->post_meta_key);
60
61 $to_cache = [];
62
63 if ( is_array( $pages ) && 1 === count( $pages ) && ! empty( $pages[0] ) ) {
64 // Post already filtered and available on global $page array, continue
65 $post->post_content = $pages[0];
66 } else {
67 $post->post_content = $this->process_content( $post->post_content, $cache, $to_cache );
68 }
69
70 $post->post_excerpt = $this->process_content( $post->post_excerpt, $cache, $to_cache );
71
72 $this->maybe_update_post_cache( $to_cache );
73 }
74
75 /**
76 * filter post for backward compatibility
77 */
78 public function filter_post_backward( $content ) {
79
80 // Enable backword compatibility
81 add_filter( 'wpmcs_enable_backward_url_replacement', [ $this, 'enable_backward_url_check' ] );
82
83 $content = $this->filter_post( $content );
84
85 // remove backword compatibility after process
86 remove_filter( 'wpmcs_enable_backward_url_replacement', [ $this, 'enable_backward_url_check' ] );
87
88 return $content;
89 }
90
91
92 /**
93 * Filter post.
94 *
95 * @param string $content
96 *
97 * @return string
98 */
99 public function filter_post( $content ) {
100 if ( empty( $content ) ) {
101 // Nothing to filter, continue
102 return $content;
103 }
104
105 $cache = $this->get_post_cache();
106 $to_cache = array();
107
108 $content = $this->process_content( $content, $cache, $to_cache );
109
110 $this->maybe_update_post_cache( $to_cache );
111
112 return $content;
113 }
114
115 /**
116 * Filter content pagination.
117 *
118 * @param array $pages
119 *
120 * @return array
121 */
122 public function filter_content_pagination( $pages ) {
123 $cache = $this->get_post_cache();
124
125 $to_cache = array();
126
127 foreach ( $pages as $key => $page ) {
128 $pages[ $key ] = $this->process_content( $page, $cache, $to_cache );
129 }
130
131 $this->maybe_update_post_cache( $to_cache );
132
133 return $pages;
134 }
135
136
137 /**
138 * may be update the post cache
139 */
140 public function maybe_update_post_cache( $to_cache ) {
141 $post_id = (int) get_post_field( 'ID', null );
142
143 if ( ! $post_id ) {
144 return array();
145 }
146
147 $cache = $this->get_post_cache();
148
149 // Merge the arrays and keep unique key-value pairs
150 $to_cache = $to_cache + $cache;
151
152 if($to_cache !== $cache) {
153 Utils::update_meta($post_id, 'items', $to_cache, $this->post_meta_key);
154 }
155 }
156
157
158 /**
159 * Get post cache
160 *
161 * @param null|int|WP_Post $post Optional. Post ID or post object. Defaults to current post.
162 * @return array|int
163 */
164 public function get_post_cache( $post = null, $transform_ints = true ) {
165 $post_id = (int) get_post_field( 'ID', $post );
166
167 if ( ! $post_id ) {
168 return array();
169 }
170
171 $cache = Utils::get_meta($post_id, 'items', [], $this->post_meta_key);
172
173 if ( Utils::is_empty( $cache ) ) {
174 $cache = array();
175 }
176
177 return $cache;
178 }
179
180
181 /**
182 * Process content.
183 *
184 * @param string $content
185 * @param array $cache
186 * @param array $to_cache
187 *
188 * @return mixed
189 */
190 protected function process_content( $content, $cache, &$to_cache ) {
191 if ( empty( $content ) || !Utils::is_ok_to_serve(false, false) ) {
192 return $content;
193 }
194
195 $content = $this->pre_filter_content( $content );
196
197 // Find URLs from img src
198 $url_pairs = $this->get_urls_from_img_src( $content, $to_cache );
199 $content = $this->replace_urls( $content, $url_pairs );
200
201 // Find leftover URLs
202 $content = $this->find_urls_and_replace( $content, $cache, $to_cache );
203
204 // Perform post processing if required
205 $content = apply_filters('wpmcs_post_process_content', $content);
206
207 return $content;
208 }
209
210 /**
211 * Pre replace content.
212 * @since 1.0.0
213 * @param string $content
214 * @return string
215 */
216 protected function pre_filter_content( $content ) {
217 $uploads = wp_upload_dir();
218 $base_url = Utils::remove_scheme( $uploads['baseurl'] );
219
220 return Utils::remove_query_strings( $content, $base_url );
221 }
222
223 /**
224 * Get URLs from img src.
225 *
226 * @param string $content
227 * @param array $to_cache
228 *
229 * @return array
230 */
231 protected function get_urls_from_img_src( $content, &$to_cache ) {
232 $url_pairs = array();
233
234 if ( ! is_string( $content ) ) {
235 return $url_pairs;
236 }
237
238 if ( ! preg_match_all( '/<img [^>]+>/', $content, $matches ) || ! isset( $matches[0] ) ) { // No img tags found
239 return $url_pairs;
240 }
241
242 $matches = array_unique( $matches[0] );
243 $item_sources = array();
244
245 foreach ( $matches as $image ) {
246 if ( ! preg_match( '/wp-image-([0-9]+)/i', $image, $class_id ) || ! isset( $class_id[1] ) ) {
247 // Can't determine ID from class, skip
248 continue;
249 }
250
251 if ( ! preg_match( '/src=\\\?["\']+([^"\'\\\]+)/', $image, $src ) || ! isset( $src[1] ) ) {
252 // Can't determine URL, skip
253 continue;
254 }
255
256 $url = $src[1];
257
258 if ( ! Utils::is_file_url( $url ) ) {
259 continue;
260 }
261
262 if ( ! self::url_needs_replacing( $url ) ) {
263 // URL already correct, skip
264 continue;
265 }
266
267 $bare_url = Utils::reduce_url( $url );
268
269 $item_sources[ $bare_url ] = [
270 'id' => absint( $class_id[1] ),
271 'source_type' => 'media_library'
272 ];
273 }
274
275 if ( count( $item_sources ) > 1 ) {
276 /*
277 * Warm object cache for use with 'get_post_meta()'.
278 *
279 * To avoid making a database call for each image, a single query
280 * warms the object cache with the meta information for all images.
281 */
282 update_meta_cache( 'post', array_unique(array_column($item_sources, 'id')));
283 }
284
285 foreach ( $item_sources as $url => $item_source ) {
286 if ( ! $this->item_matches_src( $item_source, $url ) ) {
287 // Path doesn't match attachment, skip
288 continue;
289 }
290
291 $this->push_to_url_pairs( $url_pairs, $item_source, $url, $to_cache );
292 }
293
294 return $url_pairs;
295 }
296
297 /**
298 * Does URL need replacing?
299 *
300 * @param string $url
301 *
302 * @return bool
303 */
304 public static function url_needs_replacing( $url ) {
305 $reverse_compatibility = apply_filters('wpmcs_enable_backward_url_replacement', false);
306
307 if( $reverse_compatibility ) {
308 if ( str_replace( Filter::get_bare_upload_base_urls(), '', $url ) !== $url ) {
309 return false;
310 }
311
312 if ( str_replace( Filter::get_remote_domains(), '', $url ) === $url ) {
313 return false;
314 }
315 } else {
316 if ( str_replace( Filter::get_bare_upload_base_urls(), '', $url ) === $url ) {
317 // Remote URL, no replacement needed
318 if ( str_replace( Filter::get_bare_upload_base_urls(), '', $url ) === $url ) {
319 // Remote URL, no replacement needed
320 return false;
321 }
322 }
323 }
324
325 // Server URL, perform replacement
326 return true;
327 }
328
329 /**
330 * Does attachment ID match src?
331 *
332 * @param array $item_source
333 * @param string $url
334 *
335 * @return bool
336 */
337 public function item_matches_src( $item_source, $url ): bool {
338 $upload_dir = wp_get_upload_dir();
339 $wpmcsItem = Item::instance();
340
341 $backward_replacement = apply_filters('wpmcs_enable_backward_url_replacement', false);
342
343 if (
344 $this->is_empty_item_source($item_source) ||
345 'media_library' !== $item_source['source_type'] ||
346 get_post_type( $item_source['id'] ) !== 'attachment'
347 ) {
348 return false;
349 }
350
351 $item = $wpmcsItem->get( $item_source['id'], $item_source['source_type'] );
352
353 if(!$item) return false;
354
355 $bare_url = [];
356
357 $bare_url[] = $backward_replacement
358 ? Utils::reduce_url($wpmcsItem->get_url( $item_source['id'], 'full', $item_source['source_type'] ))
359 : Utils::reduce_url(trailingslashit($upload_dir['baseurl']).$item['source_path']);
360
361
362 $extras = $wpmcsItem->get_extras( $item_source['id'], false, $item_source['source_type'] );
363
364 if(isset($extras['original']) && !Utils::is_empty($extras['original'])) {
365 $bare_url[] = $backward_replacement
366 ? Utils::reduce_url($wpmcsItem->get_url( $item_source['id'], 'original', $item_source['source_type'] ))
367 : Utils::reduce_url(trailingslashit($upload_dir['baseurl']).$extras['original']['source_path']);
368 }
369
370 if(isset($extras['sizes']) && !Utils::is_empty($extras['sizes'])) {
371 foreach($extras['sizes'] as $size => $sub_image) {
372 if(!Utils::is_empty($sub_image)){
373 $bare_url[] = $backward_replacement
374 ? Utils::reduce_url($wpmcsItem->get_url( $item_source['id'], $size, $item_source['source_type'] ))
375 : Utils::reduce_url(trailingslashit($upload_dir['baseurl']).$sub_image['source_path']);
376 }
377 }
378 }
379
380 if ( in_array( $url, $bare_url ) ) {
381 // Match found, return true
382 return true;
383 }
384
385 return false;
386 }
387
388
389 /**
390 * Push to URL pairs.
391 *
392 * @param array $url_pairs
393 * @param array $item_source
394 * @param string $find
395 * @param array $to_cache
396 */
397 protected function push_to_url_pairs( &$url_pairs, $item_source, $find, &$to_cache ) {
398 $upload_dir = wp_get_upload_dir();
399 $wpmcsItem = Item::instance();
400 $item = $wpmcsItem->get( $item_source['id'], $item_source['source_type'] );
401
402 if (
403 Utils::is_empty($item) ||
404 get_post_type( $item_source['id'] ) !== 'attachment'
405 ) {
406 return false;
407 }
408
409 $base = trailingslashit($upload_dir['baseurl']);
410 $backward_replacement = apply_filters('wpmcs_enable_backward_url_replacement', false);
411
412 $source_server_file = Utils::remove_scheme($base.$item['source_path']);
413 $source_cloud_file = Utils::remove_scheme($wpmcsItem->get_url($item_source['id'], 'full', $item_source['source_type']));
414
415 $url_pairs[$source_server_file] = $source_cloud_file;
416 if($backward_replacement) {
417 $url_pairs[$source_cloud_file] = $source_server_file; // Backward compatibility
418 }
419
420 $extras = $wpmcsItem->get_extras($item_source['id'], false, $item_source['source_type']);
421
422 if(isset($extras['original']) && !Utils::is_empty($extras['original'])) {
423 $original_server_file = Utils::remove_scheme($base.$extras['original']['source_path']);
424 $original_cloud_file = Utils::remove_scheme($wpmcsItem->get_url($item_source['id'], 'original', $item_source['source_type']));
425 $url_pairs[$original_server_file] = $original_cloud_file;
426 if($backward_replacement) {
427 $url_pairs[$original_cloud_file] = $original_server_file; // Backward compatibility
428 }
429 }
430 if(isset($extras['sizes'])){
431 foreach ($extras['sizes'] as $size => $sub_image) {
432 $sub_server_file = Utils::remove_scheme($base.$sub_image['source_path']);
433 $sub_cloud_file = Utils::remove_scheme($wpmcsItem->get_url( $item_source['id'], $size, $item_source['source_type'] ));
434 $url_pairs[$sub_server_file] = $sub_cloud_file;
435 if($backward_replacement) {
436 $url_pairs[$sub_cloud_file ] = $sub_server_file; // Backward compatibility
437 }
438 }
439 }
440
441 $to_cache[ $find ] = $item_source;
442 }
443
444
445 /**
446 * Replace URLs.
447 *
448 * @param string $content
449 * @param array $url_pairs
450 *
451 * @return string
452 */
453 protected function replace_urls( $content, $url_pairs ) {
454 if ( empty( $url_pairs ) ) {
455 // No URLs to replace return
456 return $content;
457 }
458
459 foreach ( $url_pairs as $find => $replace ) {
460 $content = apply_filters('wpmcs_before_url_replace', $content, $find, $replace );
461 $content = str_replace( $find, $replace, $content );
462 $content = apply_filters('wpmcs_after_url_replace', $content, $find, $replace );
463 }
464
465 return $content;
466 }
467
468 /**
469 * Find URLs and replace.
470 *
471 * @param string $value
472 * @param array $cache
473 * @param array $to_cache
474 *
475 * @return string
476 */
477 protected function find_urls_and_replace( $value, $cache, &$to_cache ) {
478 if ( !Utils::is_ok_to_serve(false, false) ) {
479 return $value;
480 }
481
482 $url_pairs = $this->get_urls_from_content( $value, $cache, $to_cache );
483 $value = $this->replace_urls( $value, $url_pairs );
484
485 return $value;
486 }
487
488 /**
489 * Get URLs from content.
490 *
491 * @param string $content
492 * @param array $cache
493 * @param array $to_cache
494 *
495 * @return array
496 */
497 protected function get_urls_from_content( $content, $cache, &$to_cache ) {
498 $url_pairs = array();
499
500 if ( ! is_string( $content ) ) {
501 return $url_pairs;
502 }
503
504 if ( ! preg_match_all( '/(http|https)?:?\/\/[^"\'\s<>()\\\]*/', $content, $matches ) || ! isset( $matches[0] ) ) {
505 // No URLs found, return
506 return $url_pairs;
507 }
508
509 $matches = array_unique( $matches[0] );
510 $urls = array();
511
512 foreach ( $matches as $url ) {
513 $url = preg_replace( '/[^a-zA-Z0-9]$/', '', $url );
514
515 if ( ! Utils::is_file_url( $url ) ) {
516 continue;
517 }
518
519 if ( ! self::url_needs_replacing( $url ) ) {
520 // URL already correct, skip
521 continue;
522 }
523
524 $item_source = null;
525 $bare_url = Utils::reduce_url( $url );
526
527 // If attachment ID recently or previously cached, skip full search.
528 if ( isset( $to_cache[ $bare_url ] ) ) {
529 $item_source = $to_cache[ $bare_url ];
530 } elseif ( isset( $cache[ $bare_url ] ) && is_array($cache[ $bare_url ]) ) {
531 $item_source = $cache[ $bare_url ];
532 }
533
534 if ( is_null( $item_source ) ) {
535 // Attachment ID not cached, need to search by URL.
536 $urls[] = $bare_url;
537 } else {
538 $this->push_to_url_pairs( $url_pairs, $item_source, $bare_url, $to_cache );
539 }
540 }
541
542 if ( ! empty( $urls ) ) {
543 $item_sources = $this->get_item_sources_from_urls( $urls );
544
545 foreach ( $item_sources as $url => $item_source ) {
546 if ( ! $item_source ) {
547 continue;
548 }
549
550 $this->push_to_url_pairs( $url_pairs, $item_source, $url, $to_cache );
551 }
552 }
553
554 return $url_pairs;
555 }
556
557 /**
558 * Get item source descriptors from URLs.
559 *
560 * @param array $urls
561 *
562 * @return array url => item source descriptor array (or false)
563 */
564 protected function get_item_sources_from_urls( $urls ) {
565 $results = array();
566
567 if ( empty( $urls ) ) {
568 return $results;
569 }
570
571 if ( ! is_array( $urls ) ) {
572 $urls = array( $urls );
573 }
574
575 $wpmcsItem = Item::instance();
576
577 $query_set = array();
578 $paths = array();
579 $full_urls = array();
580
581 // Quickly parse given URLs to add versions without size as we should lookup with size info first as that could be the "full" size.
582 foreach ( $urls as $url ) {
583 $query_set[] = $url;
584 $size_removed = Utils::remove_size_from_filename( $url );
585
586 if ( $url !== $size_removed ) {
587 $query_set[] = $size_removed;
588 }
589 }
590
591 foreach ( $query_set as $url ) {
592 // Path to search for in query set should be based on bare URL.
593 $bare_url = Utils::remove_scheme( $url );
594 // There can be multiple URLs in the query set that belong to the same full URL for the Media Library item.
595 $full_url = Utils::remove_size_from_filename( $bare_url );
596
597 if ( isset( $this->query_cache[ $full_url ] ) ) {
598 // ID already cached, use it.
599 $results[ $url ] = $this->query_cache[ $full_url ];
600
601 continue;
602 }
603
604
605 $path = Utils::get_attachment_source_path( $full_url );
606
607 $paths[ $path ] = $full_url;
608 $full_urls[ $full_url ][] = $url;
609 }
610
611 if ( ! empty( $paths ) ) {
612 $wpmcsItems = $wpmcsItem->get_items_by_source_paths( array_keys( $paths ) );
613
614 if ( ! empty( $wpmcsItems ) ) {
615 foreach ( $wpmcsItems as $item ) {
616 // Each returned item may have matched on either the source_path or original_source_path.
617 // Because the base image file name of a thumbnail might match the primary rather scaled or rotated full image
618 // it's possible that both source paths are used by separate URLs.
619 $source_id = (int)$item['source_id'];
620 $source_type = $item['source_type'];
621 $source_paths = [ $item['source_path'] ];
622 $original = $wpmcsItem->get_extras($source_id, 'original', $source_type);
623 if(isset($original['source_path'])) {
624 $source_paths[] = $original['source_path'];
625 }
626
627 foreach ( $source_paths as $source_path ) {
628 if ( ! empty( $paths[ $source_path ] ) ) {
629 $matched_full_url = $paths[ $source_path ];
630
631 if ( ! empty( $full_urls[ $matched_full_url ] ) ) {
632 $item_source = [
633 'id' => $source_id,
634 'source_type' => $source_type
635 ];
636
637 $this->query_cache[ $matched_full_url ] = $item_source;
638
639 foreach ( $full_urls[ $matched_full_url ] as $url ) {
640 $results[ $url ] = $item_source;
641 }
642 unset( $full_urls[ $matched_full_url ] );
643 }
644 }
645 }
646 }
647 }
648
649 // No more item IDs found, set remaining results as false.
650 if ( count( $query_set ) !== count( $results ) ) {
651 foreach ( $full_urls as $full_url => $schema_urls ) {
652 foreach ( $schema_urls as $url ) {
653 if ( ! array_key_exists( $url, $results ) ) {
654 $this->query_cache[ $full_url ] = false;
655 $results[ $url ] = false;
656 }
657 }
658 }
659 }
660 }
661
662 return $results;
663 }
664
665 /**
666 * Is the supplied item_source considered to be empty?
667 *
668 * @param array $item_source
669 *
670 * @return bool
671 */
672 public function is_empty_item_source( $item_source ) {
673 if (
674 empty( $item_source['source_type'] ) ||
675 ! isset( $item_source['id'] ) ||
676 ! is_numeric( $item_source['id'] ) ||
677 $item_source['id'] < 0
678 ) {
679 return true;
680 }
681
682 return false;
683 }
684
685
686
687 /**
688 * Define the method for the backword compatibility filter
689 * */
690 public function enable_backward_url_check( $enable ) {
691 return true;
692 }
693
694
695 public static function instance(){
696 if (is_null(self::$instance)) {
697 self::$instance = new self();
698 }
699 return self::$instance;
700 }
701 }