PluginProbe
Media Cloud Sync / 1.4.1
Media Cloud Sync v1.4.1
1.4.1 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 All 35 releases
← All changes | includes/config/utils.php +137 -25 1.4.01.4.1 View file →
@@ -121,9 +121,9 @@
121 121 * @since 1.0.0
122 122 * @return array|boolean|string|integer|float|double
123 123 */
124 124 public static function get_user_meta($post_id, $key, $default = false, $meta_name = false, $expire = false){
125 - $data = Cache::get_object_cache( $key, $post_id, $meta_name, $expire, true );
125 + $data = Cache::get_object_cache( $key, $post_id, $meta_name, $expire, 'user' );
126 126 return $data === false ? $default : $data;
127 127 }
128 128
129 129 /**
@@ -131,9 +131,9 @@
131 131 * @since 1.0.0
132 132 * @return boolean
133 133 */
134 134 public static function update_user_meta($post_id, $key, $options, $meta_name = false, $expire = false){
135 - return Cache::set_object_cache( $key, $options, $post_id, $meta_name, $expire, true );
135 + return Cache::set_object_cache( $key, $options, $post_id, $meta_name, $expire, 'user' );
136 136 }
137 137
138 138 /**
139 139 * Function To delete Plugin Specific Wordpress user meta
@@ -140,13 +140,47 @@
140 140 * @since 1.0.0
141 141 * @return boolean
142 142 */
143 143 public static function delete_user_meta($post_id, $key, $meta_name = false){
144 - return Cache::delete_object_cache( $key, $post_id, $meta_name, true );
144 + return Cache::delete_object_cache( $key, $post_id, $meta_name, 'user' );
145 145 }
146 146
147 + /**
148 + * Function To get Plugin Specific meta via a caller-supplied storage backend
149 + * — for a meta table that isn't 'posts'/'users' and doesn't follow WP's
150 + * standard get_metadata() column conventions (e.g. BuddyBoss's groupmeta,
151 + * which uses its own get/update/delete functions internally).
152 + * @param array $backend ['get'=>callable, 'update'=>callable, 'delete'=>callable, 'prefix'=>string]
153 + * Each callable is shaped like get_post_meta($id,$key,true)/
154 + * update_post_meta($id,$key,$value)/delete_post_meta($id,$key).
155 + * @since 1.4.0.3
156 + * @return array|boolean|string|integer|float|double
157 + */
158 + public static function get_custom_meta($post_id, $key, $default = false, $meta_name = false, $expire = false, $backend = []){
159 + $data = Cache::get_object_cache( $key, $post_id, $meta_name, $expire, $backend );
160 + return $data === false ? $default : $data;
161 + }
147 162
148 163 /**
164 + * Function To update Plugin Specific meta via a caller-supplied storage backend. See get_custom_meta().
165 + * @since 1.4.0.3
166 + * @return boolean
167 + */
168 + public static function update_custom_meta($post_id, $key, $options, $meta_name = false, $expire = false, $backend = []){
169 + return Cache::set_object_cache( $key, $options, $post_id, $meta_name, $expire, $backend );
170 + }
171 +
172 + /**
173 + * Function To delete Plugin Specific meta via a caller-supplied storage backend. See get_custom_meta().
174 + * @since 1.4.0.3
175 + * @return boolean
176 + */
177 + public static function delete_custom_meta($post_id, $key, $meta_name = false, $backend = []){
178 + return Cache::delete_object_cache( $key, $post_id, $meta_name, $backend );
179 + }
180 +
181 +
182 + /**
149 183 * Clear meta from database
150 184 *
151 185 * @param string|false $meta_name
152 186 * @param string $meta_table
@@ -171,9 +205,9 @@
171 205 if( in_array('usermeta', $meta_tables) ) {
172 206 // Clear user meta
173 207 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->usermeta WHERE meta_key = %s", $meta_name ) );
174 208 }
175 -
209 +
176 210 if( in_array('options', $meta_tables) ) {
177 211 // Clear options
178 212 $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name = %s", $meta_name ) );
179 213 }
@@ -438,13 +472,27 @@
438 472 public static function is_ok_to_serve($attachment_id = false, $check_id = true){
439 473 return (
440 474 self::is_service_enabled() &&
441 475 self::get_settings('rewrite_url') &&
442 - ( $check_id ? isset($attachment_id) && !empty($attachment_id) : true )
476 + ( $check_id ? isset($attachment_id) && !empty($attachment_id) : true )
443 477 );
444 478 }
445 479
446 480 /**
481 + * Whether a specific attachment's URL should resolve to the cloud copy — same as
482 + * is_ok_to_serve() plus a per-item override point (e.g. Pro's "Use Server URL").
483 + * Only for genuine URL-building call sites; is_ok_to_serve() is also reused elsewhere
484 + * as a plain "is this item managed" check and must keep its original meaning.
485 + * @since 1.4.1
486 + */
487 + public static function should_serve_from_cloud($attachment_id, $source_type = 'media_library') {
488 + if (!self::is_ok_to_serve($attachment_id)) {
489 + return false;
490 + }
491 + return (bool) apply_filters('wpmcs_should_serve_from_cloud', true, $attachment_id, $source_type);
492 + }
493 +
494 + /**
447 495 * Function to check uploading media environment is ok
448 496 * @since 1.0.0
449 497 * @return boolean
450 498 */
@@ -575,8 +623,14 @@
575 623
576 624 // Normalize slashes early
577 625 $file = str_replace( '\\', '/', $file );
578 626
627 + // filter_var(..., FILTER_VALIDATE_URL) requires a scheme, but callers like
628 + // FilterContent::get_item_sources_from_urls() intentionally pass scheme-relative
629 + // URLs (Utils::remove_scheme()/reduce_url() strip it) — wp_parse_url() handles
630 + // "//host/path" correctly, so treat that as URL-like too.
631 + $is_url = filter_var( $file, FILTER_VALIDATE_URL ) || 0 === strpos( $file, '//' );
632 +
579 633 /**
580 634 * -------------------------------------------------
581 635 * TYPE: SOURCE (WordPress local paths / URLs)
582 636 * -------------------------------------------------
@@ -590,19 +644,26 @@
590 644
591 645 $basedir = str_replace( '\\', '/', $uploads['basedir'] );
592 646 $baseurl = str_replace( '\\', '/', $uploads['baseurl'] );
593 647
594 - // If URL → extract path
595 - if ( filter_var( $file, FILTER_VALIDATE_URL ) ) {
648 + // If URL → extract path, then strip using baseurl's own path component —
649 + // once scheme+host are gone, comparing against the full $baseurl string
650 + // (which still has them) never matches.
651 + if ( $is_url ) {
596 652 $parsed = wp_parse_url( $file );
597 653 $file = $parsed['path'] ?? '';
598 - }
599 654
600 - // Strip WordPress upload root
601 - if ( 0 === strpos( $file, $basedir ) ) {
602 - $file = substr( $file, strlen( $basedir ) );
603 - } elseif ( 0 === strpos( $file, $baseurl ) ) {
604 - $file = substr( $file, strlen( $baseurl ) );
655 + $baseurl_path = (string) wp_parse_url( $baseurl, PHP_URL_PATH );
656 + if ( $baseurl_path !== '' && 0 === strpos( $file, $baseurl_path ) ) {
657 + $file = substr( $file, strlen( $baseurl_path ) );
658 + }
659 + } else {
660 + // Strip WordPress upload root
661 + if ( 0 === strpos( $file, $basedir ) ) {
662 + $file = substr( $file, strlen( $basedir ) );
663 + } elseif ( 0 === strpos( $file, $baseurl ) ) {
664 + $file = substr( $file, strlen( $baseurl ) );
665 + }
605 666 }
606 667 }
607 668
608 669 /**
@@ -612,9 +673,9 @@
612 673 */
613 674 elseif ( $type === 'key' ) {
614 675
615 676 // URL → extract path only
616 - if ( filter_var( $file, FILTER_VALIDATE_URL ) ) {
677 + if ( $is_url ) {
617 678 $parsed = wp_parse_url( $file );
618 679 $file = $parsed['path'] ?? '';
619 680 }
620 681
@@ -645,8 +706,15 @@
645 706 if ( $file === '' || substr( $file, -1 ) === '/' ) {
646 707 return false;
647 708 }
648 709
710 + // Reject a literal ".." path segment — callers resolve this against the uploads
711 + // basedir and pass it straight to file_exists()/upload, so an untrimmed "../../wp-config.php"
712 + // would otherwise let a crafted source URL read/upload a file outside the uploads directory.
713 + if ( in_array( '..', explode( '/', $file ), true ) ) {
714 + return false;
715 + }
716 +
649 717 return apply_filters(
650 718 'wpmcs_get_relative_file_path_from_upload_directory',
651 719 $file,
652 720 $type
@@ -652,9 +720,35 @@
652 720 $type
653 721 );
654 722 }
655 723
724 + /**
725 + * Resolve a relative path (from get_attachment_source_path()) to an absolute path,
726 + * only if it genuinely stays within the uploads basedir — a defense-in-depth check
727 + * for callers about to file_exists()/read the result, alongside get_attachment_source_path()'s
728 + * own "..".
729 + * @since 1.4.1
730 + * @return string|false
731 + */
732 + public static function resolve_within_uploads( $relative_path ) {
733 + if ( empty( $relative_path ) || ! is_string( $relative_path ) ) {
734 + return false;
735 + }
656 736
737 + $basedir = trailingslashit( wp_get_upload_dir()['basedir'] );
738 + $absolute_path = $basedir . ltrim( $relative_path, '/' );
739 +
740 + $real_basedir = realpath( $basedir );
741 + $real_path = realpath( $absolute_path );
742 +
743 + if ( $real_basedir === false || $real_path === false || 0 !== strpos( $real_path, $real_basedir ) ) {
744 + return false;
745 + }
746 +
747 + return $absolute_path;
748 + }
749 +
750 +
657 751 /**
658 752 * Whether the file may be synced based on plugin extension settings only.
659 753 *
660 754 * Uses `extensions_exclude` to block listed extensions and optional `extensions_include` as an allow-list.
@@ -738,9 +832,9 @@
738 832 /**
739 833 * Generate Key for Objects
740 834 * @since 1.0.0
741 835 */
742 - public static function generate_object_key($relative_source_path, $prefix) {
836 + public static function generate_object_key($relative_source_path, $prefix, $is_private = false) {
743 837 $upload_path = '';
744 838 $enable_base_path = self::get_settings('enable_base_path', true);
745 839 $base_path = self::get_settings('base_path', 'wp-content/uploads');
746 840 $year_month = self::get_settings('year_month', true);
@@ -746,21 +840,39 @@
746 840 $year_month = self::get_settings('year_month', true);
747 841 $relative_source_path = ltrim( $relative_source_path, '/' );
748 842 $file_name = wp_basename( $relative_source_path );
749 843
750 - if(!$enable_base_path) { // If base path is not enabled
751 - $base_path = '';
844 + if($is_private) {
845 + // Private media is a Pro feature — Pro hooks this filter to supply the
846 + // actual base_path+private_path root (see ProItem/ProPrivateMedia). An
847 + // item can carry is_private=1 from when Pro *was* active and later have
848 + // this filter go unanswered — Pro deactivated/uninstalled, or its license
849 + // simply lapsing (ProPrivateMedia::register_hooks() itself requires an
850 + // active license) — so this is a real, reachable state, not a hypothetical.
851 + // Falling back to an empty root would silently place the file outside
852 + // whatever path the bucket policy actually carves out — publicly
853 + // readable, while is_private stays 1 and Item::get_url() keeps serving it
854 + // as if it were still protected. Refuse instead: no key at all is safer
855 + // than a wrong one for a file that's supposed to stay private.
856 + if ( ! has_filter( 'wpmcs_private_object_key_root' ) ) {
857 + return false;
858 + }
859 + $upload_path = apply_filters( 'wpmcs_private_object_key_root', '', $relative_source_path, $prefix );
860 + } else {
861 + if(!$enable_base_path) { // If base path is not enabled
862 + $base_path = '';
863 + }
864 +
865 + if(isset($base_path) && !empty($base_path)) {
866 + $upload_path.= preg_replace('~/+~', '/',
867 + str_replace('\\', '/',
868 + trim($base_path," \n\r\t\v\x00\/ ")
869 + )
870 + );
871 + }
752 872 }
753 873
754 874 $keep_original_folder_structure = apply_filters( 'wpmcs_keep_original_folder_structure', false );
755 -
756 - if(isset($base_path) && !empty($base_path)) {
757 - $upload_path.= preg_replace('~/+~', '/',
758 - str_replace('\\', '/',
759 - trim($base_path," \n\r\t\v\x00\/ ")
760 - )
761 - );
762 - }
763 875
764 876 if($keep_original_folder_structure) {
765 877 $object_key = ltrim($upload_path . '/' . dirname( $relative_source_path ) . '/' . $prefix . $file_name, '/');
766 878 } else {