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 +99 -22 5.5.25.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
@@ -120,9 +127,9 @@
120 127
121 128 // Gallery
122 129 register_rest_route( $this->namespace, '/images/', array(
123 130 'methods' => 'POST',
124 - 'permission_callback' => '__return_true',
131 + 'permission_callback' => array( $this, 'can_load_images' ),
125 132 'callback' => array( $this, 'rest_images' )
126 133 ) );
127 134
128 135 register_rest_route( $this->namespace, '/fetch_posts', array(
@@ -136,8 +143,18 @@
136 143 )
137 144 ) );
138 145 }
139 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 +
140 157 function preview( WP_REST_Request $request ) {
141 158 $params = $request->get_body( );
142 159 $params = json_decode( $params );
143 160 $params->ids = implode( ',', $params->ids );
@@ -142,25 +159,40 @@
142 159 $params = json_decode( $params );
143 160 $params->ids = implode( ',', $params->ids );
144 161 $atts = ( array ) $params;
145 162
163 + $full = !empty( $atts['full'] );
164 + unset( $atts['full'] );
165 +
146 166 $is_collection = isset( $atts['collection'] ) && !empty( $atts['collection'] );
147 167 if ( $is_collection ) {
148 - $html = do_shortcode( '[meow-collection id="' . $atts['collection'] . '"]' );
168 + $html = $this->core->render_collection( $atts['collection'] );
169 + $counts = [ 'total' => 0, 'shown' => 0 ];
149 170 } else {
171 + $this->core->last_preview_counts = [ 'total' => 0, 'shown' => 0 ];
172 + if ( $full ) {
173 + $this->core->preview_cutoff = PHP_INT_MAX;
174 + }
150 175 $html = $this->core->gallery( $atts, [ 'isPreview' => true ] );
176 + $counts = $this->core->last_preview_counts;
151 177 }
152 178
153 179
154 - 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 );
155 186 }
156 187
157 188 function rest_load_gallery_collection( $request ) {
158 189 try {
159 190 $params = $request->get_json_params( );
160 - $gallery_id = $params['id'];
161 - $search_slug = $params['search_slug'];
162 - $gallery_atts = $params['gallery_atts'];
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();
163 195
164 196 $key = [
165 197 'gallery_id' => 'id',
166 198 'wplr_collection_id' => 'wplr-collection',
@@ -166,8 +198,26 @@
166 198 'wplr_collection_id' => 'wplr-collection',
167 199 'rml' => 'rml',
168 200 ];
169 201
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 +
170 220 $shortcode_atts = array();
171 221 $shortcode_atts[ $key[$search_slug] ] = $gallery_id;
172 222 $shortcode_atts = [...$shortcode_atts, ...$gallery_atts];
173 223
@@ -201,9 +251,9 @@
201 251 global $wpdb;
202 252 $params = $request->get_json_params( );
203 253
204 254 $id = $params['id'];
205 - $medias = $params['medias'];
255 + $medias = Meow_MGL_Core::normalize_medias( $params['medias'] ?? null );
206 256 $name = $params['name'];
207 257 $layout = $params['layout'];
208 258 $description = $params['description'];
209 259 $posts = $params['posts'];
@@ -219,9 +269,9 @@
219 269 if ( !$name ) {
220 270 throw new Exception( __( 'Please enter a name for your shortcode.', MGL_DOMAIN ));
221 271 }
222 272
223 - if ( !$is_post_mode && ( !$medias || !count( $medias['thumbnail_ids'] )) ) {
273 + if ( !$is_post_mode && empty( $medias['thumbnail_ids'] ) ) {
224 274 throw new Exception( __( 'Please select at least one image.', MGL_DOMAIN ));
225 275 }
226 276
227 277 if ( $is_post_mode && $dynamic_source === 'posts' && ( !$posts && !$latest_posts )) {
@@ -403,9 +453,9 @@
403 453 $galleries[$gallery['id']] = [
404 454 'name' => $gallery['name'],
405 455 'description' => $gallery['description'],
406 456 'layout' => $gallery['layout'],
407 - 'medias' => unserialize( $gallery['medias'] ),
457 + 'medias' => Meow_MGL_Core::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
408 458 'is_post_mode' => ( bool )$gallery['is_post_mode'],
409 459 'hero' => ( bool )$gallery['is_hero_mode'],
410 460 'posts' => $gallery['posts'] ? unserialize( $gallery['posts'] ) : null,
411 461 'latest_posts' => $gallery['latest_posts'],
@@ -515,24 +565,21 @@
515 565 "AND p.ID NOT IN ( " . implode( ', ', array_fill( 0, count( $except ), '%s' )) . " )", $except
516 566 ) : '';
517 567 $join_clause = '';
518 568 if ( $unusedImages ) {
519 - // Retrieve the serialized option from the database
520 - $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();
521 573
522 - // Deserialize the option to get the array
523 - $shortcodes_array = maybe_unserialize( $meow_gallery_shortcodes );
524 -
525 - // Extract all thumbnail IDs from the array
526 574 $used_thumbnail_ids = [];
527 - foreach ( $shortcodes_array as $shortcode ) {
528 - if ( isset( $shortcode['medias']['thumbnail_ids'] ) && is_array( $shortcode['medias']['thumbnail_ids'] ) ) {
529 - $used_thumbnail_ids = array_merge( $used_thumbnail_ids, $shortcode['medias']['thumbnail_ids'] );
530 - }
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'] );
531 578 }
532 579
533 580 // Make sure the IDs are integers
534 - $used_thumbnail_ids = array_map( 'intval', $used_thumbnail_ids );
581 + $used_thumbnail_ids = array_unique( array_map( 'intval', $used_thumbnail_ids ) );
535 582
536 583 // Include the NOT IN clause to exclude used thumbnail IDs
537 584 if ( !empty( $used_thumbnail_ids ) ) {
538 585 $placeholders = implode( ',', array_fill( 0, count( $used_thumbnail_ids ), '%d' ) );
@@ -615,8 +662,33 @@
615 662 'data' => $this->core->get_gallery_images( $image_ids, $atts, $layout, $size )
616 663 ], 200 );
617 664 }
618 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 +
619 691 function rest_fetch_posts( $request ) {
620 692 try {
621 693 $params = $request->get_json_params();
622 694 $search = isset($params['search']) ? $params['search'] : '';
@@ -631,8 +703,13 @@
631 703 $searchPlaceholder,
632 704 $searchPlaceholder
633 705 ) : '';
634 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 +
635 712 $posts = $wpdb->get_results(
636 713 $wpdb->prepare(
637 714 "SELECT p.ID, p.post_title, p.post_date, p.post_status, u.display_name as author
638 715 FROM $wpdb->posts p
@@ -637,9 +714,9 @@
637 714 "SELECT p.ID, p.post_title, p.post_date, p.post_status, u.display_name as author
638 715 FROM $wpdb->posts p
639 716 LEFT JOIN $wpdb->users u ON p.post_author = u.ID
640 717 WHERE p.post_type = 'post'
641 - AND p.post_status IN ('publish', 'draft', 'private')
718 + $where_status_clause
642 719 $where_search_clause
643 720 ORDER BY p.post_date DESC
644 721 LIMIT %d, %d",
645 722 $offset,
@@ -651,9 +728,9 @@
651 728 $posts_count = (int)$wpdb->get_var(
652 729 "SELECT COUNT(*)
653 730 FROM $wpdb->posts p
654 731 WHERE p.post_type = 'post'
655 - AND p.post_status IN ('publish', 'draft', 'private')
732 + $where_status_clause
656 733 $where_search_clause"
657 734 );
658 735
659 736 $data = array_map(function($post) {