PluginProbe
Meow Gallery / 5.5.5
Meow Gallery v5.5.5
5.5.5 5.5.4 5.5.3 5.5.2 5.5.1 5.5.0 5.4.9 5.4.8 5.4.7 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 All 157 releases
← All changes | classes/rest.php +124 -23 5.4.85.5.5 View file →
@@ -4,8 +4,15 @@
4 4 {
5 5 private $core;
6 6 private $namespace = 'meow-gallery/v1';
7 7
8 + // Gallery attributes that decide *which* media a gallery shows. They must never be taken from
9 + // an untrusted request: see rest_load_gallery_collection().
10 + private static $source_atts = [
11 + 'collection', 'id', 'ids', 'include', 'tags', 'posts', 'latest_posts', 'attachments',
12 + 'rml', 'wplr-collection', 'meow',
13 + ];
14 +
8 15 public function __construct( $core ) {
9 16 $this->core = $core;
10 17
11 18 // FOR DEBUG
@@ -66,8 +73,13 @@
66 73 'methods' => 'POST',
67 74 'permission_callback' => array( $this->core, 'can_access_settings' ),
68 75 'callback' => array( $this, 'rest_update_gallery_rank' ),
69 76 ) );
77 + register_rest_route( $this->namespace, '/rml_folders', array(
78 + 'methods' => 'GET',
79 + 'permission_callback' => array( $this->core, 'can_access_settings' ),
80 + 'callback' => array( $this, 'rest_rml_folders' ),
81 + ) );
70 82
71 83
72 84 register_rest_route( $this->namespace, '/fetch_shortcodes', array(
73 85 'methods' => 'POST',
@@ -115,9 +127,9 @@
115 127
116 128 // Gallery
117 129 register_rest_route( $this->namespace, '/images/', array(
118 130 'methods' => 'POST',
119 - 'permission_callback' => '__return_true',
131 + 'permission_callback' => array( $this, 'can_load_images' ),
120 132 'callback' => array( $this, 'rest_images' )
121 133 ) );
122 134
123 135 register_rest_route( $this->namespace, '/fetch_posts', array(
@@ -131,8 +143,18 @@
131 143 )
132 144 ) );
133 145 }
134 146
147 + // The /images/ route feeds the infinite scroll and nothing else: when it is off (the default,
148 + // and always in the free version) the gallery is rendered whole and the front-end never calls
149 + // this. It has to stay open to visitors when infinite scroll IS on, but leaving it open
150 + // everywhere exposed the title, caption and URL of any attachment ID, including attachments of
151 + // posts that are not published.
152 + public function can_load_images() {
153 + $infinite = class_exists( 'MeowPro_MGL_Core' ) && Meow_MGL_Core::get_plugin_option( 'infinite', false );
154 + return apply_filters( 'mgl_allow_load_images', (bool) $infinite );
155 + }
156 +
135 157 function preview( WP_REST_Request $request ) {
136 158 $params = $request->get_body( );
137 159 $params = json_decode( $params );
138 160 $params->ids = implode( ',', $params->ids );
@@ -137,31 +159,70 @@
137 159 $params = json_decode( $params );
138 160 $params->ids = implode( ',', $params->ids );
139 161 $atts = ( array ) $params;
140 162
163 + $full = !empty( $atts['full'] );
164 + unset( $atts['full'] );
165 +
141 166 $is_collection = isset( $atts['collection'] ) && !empty( $atts['collection'] );
142 167 if ( $is_collection ) {
143 - $html = do_shortcode( '[meow-collection id="' . $atts['collection'] . '"]' );
168 + $html = $this->core->render_collection( $atts['collection'] );
169 + $counts = [ 'total' => 0, 'shown' => 0 ];
144 170 } else {
171 + $this->core->last_preview_counts = [ 'total' => 0, 'shown' => 0 ];
172 + if ( $full ) {
173 + $this->core->preview_cutoff = PHP_INT_MAX;
174 + }
145 175 $html = $this->core->gallery( $atts, [ 'isPreview' => true ] );
176 + $counts = $this->core->last_preview_counts;
146 177 }
147 178
148 179
149 - return new WP_REST_Response( [ 'success' => true, 'data' => $html ], 200 );
180 + return new WP_REST_Response( [
181 + 'success' => true,
182 + 'data' => $html,
183 + 'total' => intval( $counts['total'] ),
184 + 'shown' => intval( $counts['shown'] ),
185 + ], 200 );
150 186 }
151 187
152 188 function rest_load_gallery_collection( $request ) {
153 189 try {
154 190 $params = $request->get_json_params( );
155 - $gallery_id = $params['id'];
156 - $search_slug = $params['search_slug'];
191 + $gallery_id = $params['id'] ?? '';
192 + $search_slug = $params['search_slug'] ?? '';
193 + $gallery_atts = $params['gallery_atts'] ?? array();
194 + $gallery_atts = is_array( $gallery_atts ) ? $gallery_atts : array();
157 195
158 196 $key = [
159 197 'gallery_id' => 'id',
160 198 'wplr_collection_id' => 'wplr-collection',
199 + 'rml' => 'rml',
161 200 ];
162 201
163 - $html = $this->core->gallery( [ $key[$search_slug] => $gallery_id ], [ 'isPreview' => false, 'isRest' => true ] );
202 + // This route is public (visitors open galleries from a collection), so everything it
203 + // receives is untrusted. The gallery to render is decided by 'search_slug' + 'id'
204 + // only: the caller-supplied attributes are stripped of anything that could point the
205 + // gallery at other content. Without this, 'collection' could be used to inject
206 + // arbitrary shortcodes (reported by JunHee CHO, 2026-09).
207 + if ( !isset( $key[ $search_slug ] ) ) {
208 + return new WP_REST_Response( [ 'success' => false, 'message' => __( 'Unknown gallery source.', MGL_DOMAIN ) ], 400 );
209 + }
210 + $gallery_atts = array_diff_key( $gallery_atts, array_flip( self::$source_atts ) );
211 +
212 + // The RML source is a folder path, the others are identifiers.
213 + if ( $search_slug !== 'rml' ) {
214 + $gallery_id = Meow_MGL_Core::sanitize_id( $gallery_id );
215 + if ( $gallery_id === '' ) {
216 + return new WP_REST_Response( [ 'success' => false, 'message' => __( 'Invalid gallery ID.', MGL_DOMAIN ) ], 400 );
217 + }
218 + }
219 +
220 + $shortcode_atts = array();
221 + $shortcode_atts[ $key[$search_slug] ] = $gallery_id;
222 + $shortcode_atts = [...$shortcode_atts, ...$gallery_atts];
223 +
224 + $html = $this->core->gallery( $shortcode_atts, [ 'isPreview' => false, 'isRest' => true ] );
164 225 $mwlData = json_encode( $this->core->get_rewritten_mwl_data( ) );
165 226 return new WP_REST_Response( [ 'success' => true, 'data' => $html, 'mwl_data' => $mwlData ], 200 );
166 227 }
167 228 catch ( Exception $e ) {
@@ -172,8 +233,15 @@
172 233 function rest_all_settings( ) {
173 234 return new WP_REST_Response( [ 'success' => true, 'data' => $this->core->get_all_options( ) ], 200 );
174 235 }
175 236
237 + function rest_rml_folders( ) {
238 + if ( ! Meow_MGL_RML::is_available() ) {
239 + return new WP_REST_Response( [ 'success' => true, 'available' => false, 'data' => [] ], 200 );
240 + }
241 + return new WP_REST_Response( [ 'success' => true, 'available' => true, 'data' => Meow_MGL_RML::get_all_folders() ], 200 );
242 + }
243 +
176 244 function rest_reset_options( ) {
177 245 $this->core->reset_options( );
178 246 return new WP_REST_Response( [ 'success' => true, 'options' => $this->core->get_all_options( ) ], 200 );
179 247 }
@@ -183,9 +251,9 @@
183 251 global $wpdb;
184 252 $params = $request->get_json_params( );
185 253
186 254 $id = $params['id'];
187 - $medias = $params['medias'];
255 + $medias = Meow_MGL_Core::normalize_medias( $params['medias'] ?? null );
188 256 $name = $params['name'];
189 257 $layout = $params['layout'];
190 258 $description = $params['description'];
191 259 $posts = $params['posts'];
@@ -195,14 +263,15 @@
195 263 $lead_image_id = $params['lead_image_id'];
196 264 $order_by = $params['order_by'];
197 265 $is_post_mode = $params['is_post_mode'];
198 266 $is_hero_mode = $params['is_hero_mode'];
267 + $rml = $params['rml'] ?? null;
199 268
200 269 if ( !$name ) {
201 270 throw new Exception( __( 'Please enter a name for your shortcode.', MGL_DOMAIN ));
202 271 }
203 272
204 - if ( !$is_post_mode && ( !$medias || !count( $medias['thumbnail_ids'] )) ) {
273 + if ( !$is_post_mode && empty( $medias['thumbnail_ids'] ) ) {
205 274 throw new Exception( __( 'Please select at least one image.', MGL_DOMAIN ));
206 275 }
207 276
208 277 if ( $is_post_mode && $dynamic_source === 'posts' && ( !$posts && !$latest_posts )) {
@@ -212,8 +281,12 @@
212 281 if ( $is_post_mode && $dynamic_source === 'tags' && !$tags ) {
213 282 throw new Exception( __( 'Please enter at least one tag.', MGL_DOMAIN ));
214 283 }
215 284
285 + if ( $is_post_mode && $dynamic_source === 'rml' && empty( $rml ) ) {
286 + throw new Exception( __( 'Please select a Real Media Library folder.', MGL_DOMAIN ));
287 + }
288 +
216 289 if ( $is_hero_mode && !$is_post_mode ) {
217 290 throw new Exception( __( 'Hero mode is only available for post mode.', MGL_DOMAIN ));
218 291 }
219 292
@@ -238,9 +311,10 @@
238 311 'is_hero_mode' => $is_hero_mode ? 1 : 0,
239 312 'posts' => $posts ? serialize( $posts ) : null,
240 313 'latest_posts' => $latest_posts,
241 314 'tags' => serialize( $tags ),
242 - 'dynamic_source' => $dynamic_source
315 + 'dynamic_source' => $dynamic_source,
316 + 'rml' => $rml
243 317 ];
244 318
245 319 if ( $exists ) {
246 320 // Update existing record
@@ -379,9 +453,9 @@
379 453 $galleries[$gallery['id']] = [
380 454 'name' => $gallery['name'],
381 455 'description' => $gallery['description'],
382 456 'layout' => $gallery['layout'],
383 - 'medias' => unserialize( $gallery['medias'] ),
457 + 'medias' => Meow_MGL_Core::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
384 458 'is_post_mode' => ( bool )$gallery['is_post_mode'],
385 459 'hero' => ( bool )$gallery['is_hero_mode'],
386 460 'posts' => $gallery['posts'] ? unserialize( $gallery['posts'] ) : null,
387 461 'latest_posts' => $gallery['latest_posts'],
@@ -491,24 +565,21 @@
491 565 "AND p.ID NOT IN ( " . implode( ', ', array_fill( 0, count( $except ), '%s' )) . " )", $except
492 566 ) : '';
493 567 $join_clause = '';
494 568 if ( $unusedImages ) {
495 - // Retrieve the serialized option from the database
496 - $meow_gallery_shortcodes = get_option( 'mgl_shortcodes' );
569 + // Every image used by a gallery, read from the galleries table (this used to read the
570 + // old 'mgl_shortcodes' option, which isn't written anymore since the migration).
571 + $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
572 + Meow_MGL_Migrations::check_db();
497 573
498 - // Deserialize the option to get the array
499 - $shortcodes_array = maybe_unserialize( $meow_gallery_shortcodes );
500 -
501 - // Extract all thumbnail IDs from the array
502 574 $used_thumbnail_ids = [];
503 - foreach ( $shortcodes_array as $shortcode ) {
504 - if ( isset( $shortcode['medias']['thumbnail_ids'] ) && is_array( $shortcode['medias']['thumbnail_ids'] ) ) {
505 - $used_thumbnail_ids = array_merge( $used_thumbnail_ids, $shortcode['medias']['thumbnail_ids'] );
506 - }
575 + foreach ( $wpdb->get_col( "SELECT medias FROM $shortcodes_table" ) as $medias ) {
576 + $medias = Meow_MGL_Core::normalize_medias( maybe_unserialize( $medias ) );
577 + $used_thumbnail_ids = array_merge( $used_thumbnail_ids, $medias['thumbnail_ids'] );
507 578 }
508 579
509 580 // Make sure the IDs are integers
510 - $used_thumbnail_ids = array_map( 'intval', $used_thumbnail_ids );
581 + $used_thumbnail_ids = array_unique( array_map( 'intval', $used_thumbnail_ids ) );
511 582
512 583 // Include the NOT IN clause to exclude used thumbnail IDs
513 584 if ( !empty( $used_thumbnail_ids ) ) {
514 585 $placeholders = implode( ',', array_fill( 0, count( $used_thumbnail_ids ), '%d' ) );
@@ -591,8 +662,33 @@
591 662 'data' => $this->core->get_gallery_images( $image_ids, $atts, $layout, $size )
592 663 ], 200 );
593 664 }
594 665
666 + // Applies WordPress's own visibility rules to a raw posts query: published posts for everyone,
667 + // other people's drafts only with edit_others_posts, other people's private posts only with
668 + // read_private_posts, and your own in both cases. 'upload_files' (the capability gating this
669 + // REST controller) is held by Authors, who must not see the whole site's unpublished content.
670 + private function get_post_status_clause( $alias = 'p', $post_type = 'post' ) {
671 + global $wpdb;
672 +
673 + $post_type_object = get_post_type_object( $post_type );
674 + $read_private_cap = $post_type_object ? $post_type_object->cap->read_private_posts : 'read_private_posts';
675 + $edit_others_cap = $post_type_object ? $post_type_object->cap->edit_others_posts : 'edit_others_posts';
676 +
677 + $user_id = get_current_user_id();
678 + $clause = "AND ( $alias.post_status = 'publish'";
679 +
680 + $clause .= current_user_can( $read_private_cap )
681 + ? " OR $alias.post_status = 'private'"
682 + : $wpdb->prepare( " OR ( $alias.post_status = 'private' AND $alias.post_author = %d )", $user_id );
683 +
684 + $clause .= current_user_can( $edit_others_cap )
685 + ? " OR $alias.post_status = 'draft'"
686 + : $wpdb->prepare( " OR ( $alias.post_status = 'draft' AND $alias.post_author = %d )", $user_id );
687 +
688 + return $clause . " ) ";
689 + }
690 +
595 691 function rest_fetch_posts( $request ) {
596 692 try {
597 693 $params = $request->get_json_params();
598 694 $search = isset($params['search']) ? $params['search'] : '';
@@ -607,8 +703,13 @@
607 703 $searchPlaceholder,
608 704 $searchPlaceholder
609 705 ) : '';
610 706
707 + // The same clause is used by both queries on purpose: the search also matches
708 + // post_content, so a count taken over a wider set than the rows would let a user probe
709 + // the body of posts they cannot read (reported by Kaan Ă–zbek, 2026-09).
710 + $where_status_clause = $this->get_post_status_clause( 'p' );
711 +
611 712 $posts = $wpdb->get_results(
612 713 $wpdb->prepare(
613 714 "SELECT p.ID, p.post_title, p.post_date, p.post_status, u.display_name as author
614 715 FROM $wpdb->posts p
@@ -613,9 +714,9 @@
613 714 "SELECT p.ID, p.post_title, p.post_date, p.post_status, u.display_name as author
614 715 FROM $wpdb->posts p
615 716 LEFT JOIN $wpdb->users u ON p.post_author = u.ID
616 717 WHERE p.post_type = 'post'
617 - AND p.post_status IN ('publish', 'draft', 'private')
718 + $where_status_clause
618 719 $where_search_clause
619 720 ORDER BY p.post_date DESC
620 721 LIMIT %d, %d",
621 722 $offset,
@@ -627,9 +728,9 @@
627 728 $posts_count = (int)$wpdb->get_var(
628 729 "SELECT COUNT(*)
629 730 FROM $wpdb->posts p
630 731 WHERE p.post_type = 'post'
631 - AND p.post_status IN ('publish', 'draft', 'private')
732 + $where_status_clause
632 733 $where_search_clause"
633 734 );
634 735
635 736 $data = array_map(function($post) {