PluginProbe
Meow Gallery / 5.5.6
Meow Gallery v5.5.6
5.5.6 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 All 158 releases
← All changes | classes/core.php +276 -45 5.4.7 → 5.5.6 View file →
@@ -7,9 +7,12 @@
7 7 private $is_gallery_used = true; // TODO: Would be nice to detect if the gallery is actually used on the current page.
8 8 private $skeleton_handler;
9 9 private $pro_module = false;
10 10
11 + public $preview_cutoff = 12; // Limit the number of images to show in the preview (for performance reasons)
12 +
11 13 private static $plugin_option_name = 'mgl_options';
14 + private $pro;
12 15 private $option_name = 'mgl_options';
13 16 private $infinite_layouts = [
14 17 'tiles',
15 18 'masonry',
@@ -20,8 +23,11 @@
20 23 ];
21 24
22 25 private $rewrittenMwlData = [];
23 26
27 + // Holds the image counts of the last preview render (used by the block editor).
28 + public $last_preview_counts = [ 'total' => 0, 'shown' => 0 ];
29 +
24 30 public function __construct() {
25 31 load_plugin_textdomain( MGL_DOMAIN, false, MGL_PATH . '/languages' );
26 32
27 33 //TODO: Move Skeleton into PRO
@@ -32,20 +38,23 @@
32 38 // Initializes the classes needed
33 39 MeowKit_MGL_Helpers::is_rest() && new Meow_MGL_Rest( $this );
34 40
35 41 // The gallery build process should only be enabled if the request is non-asynchronous
42 + add_filter( 'wp_get_attachment_image_attributes', array( $this, 'wp_get_attachment_image_attributes' ), 25, 3 );
43 +
36 44 if ( !MeowKit_MGL_Helpers::is_asynchronous_request() ) {
37 - add_filter( 'wp_get_attachment_image_attributes', array( $this, 'wp_get_attachment_image_attributes' ), 25, 3 );
45 +
38 46 if ( is_admin() || $this->is_gallery_used ) {
39 47 new Meow_MGL_Run( $this );
40 48 }
41 49 }
42 50
43 - // Load the Pro version *after* loading the Run class due to the JS file was gatherd into one file.
51 + // Load the Pro version *after* the Run class: both share the same JS bundle, and Run is
52 + // the one registering it (the Pro class only localizes extra data on it).
44 53
45 54 $this->pro_module = class_exists( 'MeowPro_MGL_Core' );
46 55 if ( $this->pro_module ) {
47 - new MeowPro_MGL_Core( $this );
56 + $this->pro = new MeowPro_MGL_Core( $this );
48 57 } else {
49 58 add_shortcode( 'meow-collection', array( $this, 'collection' ) );
50 59 }
51 60
@@ -57,8 +66,13 @@
57 66 is_admin() && new Meow_MGL_Admin( $this );
58 67
59 68 global $wpmgl;
60 69 $wpmgl = $this;
70 +
71 + if ( $this->pro_module ) {
72 + global $wpmgl_pro;
73 + $wpmgl_pro = $this->pro;
74 + }
61 75 }
62 76
63 77 function collection() {
64 78 return "<b>Meow Collection</b>: This is only available in the Pro version. Please <a href='https://meowapps.com/products/meow-gallery-pro/'>upgrade to Meow Gallery Pro</a> to use this feature.";
@@ -63,8 +77,34 @@
63 77 function collection() {
64 78 return "<b>Meow Collection</b>: This is only available in the Pro version. Please <a href='https://meowapps.com/products/meow-gallery-pro/'>upgrade to Meow Gallery Pro</a> to use this feature.";
65 79 }
66 80
81 + // Gallery and collection IDs are plain identifiers (generate_uniqid(), stored as varchar).
82 + // Anything else is rejected, and rejected rather than stripped: stripping could turn a crafted
83 + // ID into a different existing one. Security: esc_attr() does NOT escape "]", so an ID coming
84 + // from a request and concatenated into a shortcode string could close the tag and run
85 + // arbitrary shortcodes. Keep IDs on this charset and never build a shortcode string from them.
86 + public static function sanitize_id( $id ) {
87 + if ( !is_scalar( $id ) ) {
88 + return '';
89 + }
90 + $id = (string) $id;
91 + return preg_match( '/^[A-Za-z0-9_-]+$/', $id ) ? $id : '';
92 + }
93 +
94 + // Renders a collection from its ID. Deliberately calls the handler directly instead of going
95 + // through do_shortcode(): there is no shortcode string to inject into that way.
96 + public function render_collection( $id, $is_preview = false ) {
97 + $id = self::sanitize_id( $id );
98 + if ( $id === '' ) {
99 + return "<p class='meow-error'><b>Meow Gallery:</b> This collection ID is not valid.</p>";
100 + }
101 + if ( $this->pro_module && $this->pro ) {
102 + return $this->pro->collection( array( 'id' => $id ), $is_preview );
103 + }
104 + return $this->collection();
105 + }
106 +
67 107 public function can_access_settings() {
68 108 return apply_filters( 'mgl_allow_setup', current_user_can( 'manage_options' ) );
69 109 }
70 110
@@ -89,11 +129,14 @@
89 129 else if ( $this->gallery_layout === 'cascade' )
90 130 $sizes = '80vw';
91 131 else if ( $this->gallery_layout === 'justified' )
92 132 $sizes = '(max-width: 800px) 80vw, 50vw';
133 +
93 134 $sizes = apply_filters( 'mgl_sizes', $sizes, $this->gallery_layout, $attachment, $attr );
135 +
94 136 if ( !empty( $sizes ) )
95 137 $attr['sizes'] = $sizes;
138 +
96 139 return $attr;
97 140 }
98 141
99 142 function get_rewritten_mwl_data() {
@@ -99,8 +142,14 @@
99 142 function get_rewritten_mwl_data() {
100 143 return $this->rewrittenMwlData;
101 144 }
102 145
146 + // Get the IDs of the image attachments attached to the current post.
147 + private function get_attached_image_ids() {
148 + $attachments = get_attached_media( 'image' );
149 + return array_map( function( $x ) { return $x->ID; }, $attachments );
150 + }
151 +
103 152 function gallery( $atts, $options = [] ) {
104 153 $atts = apply_filters( 'shortcode_atts_gallery', $atts, null, $atts, 'gallery' );
105 154
106 155 $isPreview = isset( $options['isPreview'] ) ? $options['isPreview'] : false;
@@ -120,29 +169,33 @@
120 169 if ( isset( $atts['meow'] ) && $atts['meow'] === 'false' ) {
121 170 return gallery_shortcode( $atts );
122 171 }
123 172
124 - // If the attributes contain "collection" then use the collection shortcode instead
173 + // If the attributes contain "collection" then render that collection instead
125 174 if ( isset( $atts['collection'] ) && !empty( $atts['collection'] ) ) {
126 - return do_shortcode( '[meow-collection id="' . $atts['collection'] . '"]' );
175 + return $this->render_collection( $atts['collection'] );
127 176 }
128 177
129 178 $image_ids = array();
130 179 $layout = '';
131 180
132 - if ( isset( $atts['id'] ) && isset( $atts['ids'] ) ) {
181 + // All potential attributes that can be used to get the images for the gallery
182 + $has_id = isset( $atts['id'] ) && !empty( $atts['id'] );
183 + $has_ids = isset( $atts['ids'] ) && !empty( $atts['ids'] );
184 + $has_include = isset( $atts['include'] ) && !empty( $atts['include'] );
185 + $has_tags = isset( $atts['tags'] ) && !empty( $atts['tags'] );
186 + $has_posts = isset( $atts['posts'] ) && !empty( $atts['posts'] );
187 + $has_latest_posts = isset( $atts['latest_posts'] ) && !empty( $atts['latest_posts'] );
188 + $has_attachments = isset( $atts['attachments'] ) && ( $atts['attachments'] === 'true' || $atts['attachments'] === true || $atts['attachments'] === 1 || $atts['attachments'] === '1' );
133 189
134 - // Check if the ids are empty, then we can use the id
135 - if ( empty( $atts['ids'] ) ) {
136 - unset( $atts['ids'] );
137 - } else {
138 - error_log( "⚠️ Meow Gallery: in gallery $atts[id] both 'id' and 'ids' attributes are used in the same shortcode. 'id' will be ignored." );
139 - }
190 + if ( $has_id && $has_ids ) {
191 + unset( $atts['ids'] );
192 + error_log( "⚠️ Meow Gallery: in gallery $atts[id] both 'id' and 'ids' attributes are used in the same shortcode. 'id' will be ignored." );
140 193 }
141 194
142 195 // Get the IDs
143 196 #region media_ids
144 - if ( (isset( $atts['id'] ) && !empty($atts['id']) ) && !isset( $atts['ids'] ) ) {
197 + if ( $has_id && !$has_ids ) {
145 198 $shortcode_id = $atts['id'];
146 199
147 200 try {
148 201 $shortcode = $this->get_gallery_by_id( $shortcode_id );
@@ -147,9 +200,10 @@
147 200 try {
148 201 $shortcode = $this->get_gallery_by_id( $shortcode_id );
149 202 }
150 203 catch ( Exception $e ) {
151 - return "<p class='meow-error'><b>Meow Gallery:</b> This ID wasn't found in the Gallery Manager. (ID: $shortcode_id). " . $e->getMessage() . "</p>";
204 + $safe_id = esc_html( is_scalar( $shortcode_id ) ? $shortcode_id : '' );
205 + return "<p class='meow-error'><b>Meow Gallery:</b> This ID wasn't found in the Gallery Manager. (ID: $safe_id). " . esc_html( $e->getMessage() ) . "</p>";
152 206 }
153 207
154 208 if ( !isset( $shortcode['medias'] ) || !isset( $shortcode['medias']['thumbnail_ids'])) {
155 209 return "<p class='meow-error'><b>Meow Gallery:</b> Thumbnail IDs not found.</p>";
@@ -167,18 +221,32 @@
167 221
168 222 $atts = array_merge( $shortcode, $atts );
169 223 }
170 224
171 - if ( isset( $atts['ids'] ) ) {
225 + // Real Media Library folder path (e.g. rml="2025/France/Tours").
226 + // Re-evaluated here (not only from the top) so it also works when the value
227 + // comes from a saved gallery loaded via [gallery id="..."]. The logic lives
228 + // in Meow_MGL_RML::get_image_ids() to keep this method clean.
229 + $has_rml = isset( $atts['rml'] ) && !empty( $atts['rml'] );
230 + if ( $has_rml ) {
231 + $rml_ids = Meow_MGL_RML::get_image_ids( $atts['rml'] );
232 + if ( is_wp_error( $rml_ids ) ) {
233 + return "<p class='meow-error'><b>Meow Gallery:</b> " . esc_html( $rml_ids->get_error_message() ) . "</p>";
234 + }
235 + $image_ids = implode( ',', $rml_ids );
236 + }
237 +
238 + if ( $has_ids ) {
172 239 $image_ids = $atts['ids'];
173 240 }
174 - if ( isset( $atts['include'] ) ) {
241 +
242 + if ( $has_include ) {
175 243 $image_ids = is_array( $atts['include'] ) ? implode( ',', $atts['include'] ) : $atts['include'];
176 244 $atts['include'] = $image_ids;
177 245 }
178 246
179 247 // Tags support
180 - if ( isset( $atts['tags'] ) && !empty( $atts['tags'] ) ) {
248 + if ( $has_tags ) {
181 249 $tags = is_array( $atts['tags'] ) ? $atts['tags'] : array_map( 'trim', explode( ',', $atts['tags'] ) );
182 250
183 251 // Try multiple common taxonomies used for media tagging
184 252 $taxonomies_to_try = [ 'post_tag', 'media_tag', 'attachment_tag', 'attachment_category' ];
@@ -223,9 +291,9 @@
223 291 }
224 292 }
225 293 }
226 294
227 - if ( isset( $atts['latest_posts'] ) ) {
295 + if ( $has_latest_posts ) {
228 296 $num_posts = intval( $atts['latest_posts'] );
229 297
230 298 if ( $num_posts > 0 ) {
231 299
@@ -231,9 +299,9 @@
231 299
232 300 $latest_posts = get_posts( [ 'numberposts' => $num_posts ] );
233 301 $latest_posts_ids = array_map( function( $x ) { return $x->ID; }, $latest_posts );
234 302
235 - if ( isset( $atts['posts'] ) ) {
303 + if ( $has_posts ) {
236 304 error_log( "⚠️ Meow Gallery: in gallery $atts[id] both 'latest_posts' and 'posts' attributes are used in the same shortcode. 'latest_posts' will be merged with 'posts'.");
237 305 $atts['posts'] = array_merge( $latest_posts_ids, explode(',', $atts['posts']) );
238 306 }
239 307 else {
@@ -242,10 +310,17 @@
242 310 }
243 311
244 312 }
245 313
314 + if( $has_attachments ) {
315 + $attachmentIds = $this->get_attached_image_ids();
316 + if ( !empty( $attachmentIds ) ) {
317 + $image_ids = implode( ',', $attachmentIds );
318 + }
319 + }
320 +
246 321 $posts_ids = [];
247 - if (isset($atts['posts'])) {
322 + if ( $has_posts ) {
248 323
249 324 $posts_ids = is_array( $atts['posts'] ) ? $atts['posts'] : explode( ',', $atts['posts'] );
250 325 $featured_images = [];
251 326
@@ -273,23 +348,32 @@
273 348 $image_ids = implode( ',', $ids );
274 349
275 350 #endregion
276 351
277 - // Use attached images if still empty
278 - if ( empty( $image_ids ) ) {
279 - $attachments = get_attached_media( 'image' );
280 - $attachmentIds = array_map( function($x) { return $x->ID; }, $attachments );
352 + // Fall back to the attachments of the current post if the gallery is empty and the option is enabled.
353 + if ( empty( $image_ids ) && !$has_attachments && $this->get_option( 'use_attachments_on_empty', false ) ) {
354 + $attachmentIds = $this->get_attached_image_ids();
281 355 if ( !empty( $attachmentIds ) ) {
282 356 $image_ids = implode( ',', $attachmentIds );
357 + $has_attachments = true;
283 358 }
284 - else {
285 - return "<p class='meow-error'><b>Meow Gallery:</b> The gallery is empty.</p>";
359 + }
360 +
361 + // Use attached images if still empty
362 + if ( empty( $image_ids ) ) {
363 +
364 + if( $has_attachments ) {
365 + return "<p class='meow-error'><b>Meow Gallery:</b> No attached medias were found in the current post.</p>";
286 366 }
367 +
368 + return "<p class='meow-error'><b>Meow Gallery:</b> The gallery is empty.</p>";
287 369 }
288 370
289 371 if ( $isPreview ) {
290 372 $check = explode( ',', $image_ids );
291 - $check = array_slice( $check, 0, 40 );
373 + $total = count( $check );
374 + $check = array_slice( $check, 0, $this->preview_cutoff );
375 + $this->last_preview_counts = [ 'total' => $total, 'shown' => count( $check ) ];
292 376 $image_ids = implode( ',', $check );
293 377 }
294 378
295 379 // Limit images on archive/listing pages (not viewing the full single post)
@@ -330,11 +414,14 @@
330 414 if ( isset( $atts['order_by'] ) ) {
331 415 $orderby = $atts['order_by'];
332 416 }
333 417
334 - if( strpos( $orderby, '-' ) === 0 ) {
335 - $orderby = explode( '-', $orderby )[0];
336 - $order = explode( '-', $orderby )[1];
418 + if( strpos( $orderby, '-' ) != false ) {
419 + $left = explode( '-', $orderby )[0];
420 + $right = explode( '-', $orderby )[1];
421 +
422 + $orderby = $left;
423 + $order = $right;
337 424 }
338 425
339 426
340 427 $image_ids = explode( ',', $image_ids );
@@ -428,14 +515,21 @@
428 515 $data_gallery_images,
429 516 $data_atts
430 517 );
431 518
432 - // Run at /wp-includes/formatting.php on line 3501
433 - $textarr = preg_split( '/(<.*>)/U', $html , -1, PREG_SPLIT_DELIM_CAPTURE);
434 - if ( $textarr === false ) {
435 - $error = preg_last_error();
436 - error_log( "[MEOW GALLERY] Regex: " . preg_last_error_msg() . " (Code $error)" );
437 - return "<p class='meow-error'><b>Meow Gallery:</b> There was an error while building the gallery. Check your PHP Logs.</p>";
519 + // Run at /wp-includes/formatting.php on line 6037
520 + // WordPress returns early if the text is simple ASCII without emoji-like content,
521 + // so we only need to check preg_split if WordPress would actually run it.
522 + $needs_emoji_check = str_contains( $html, '&#x' ) ||
523 + !( ( function_exists( 'mb_check_encoding' ) && mb_check_encoding( $html, 'ASCII' ) ) || ! preg_match( '/[^\x00-\x7F]/', $html ) );
524 +
525 + if ( $needs_emoji_check ) {
526 + $textarr = preg_split( '/(<.*>)/U', $html , -1, PREG_SPLIT_DELIM_CAPTURE);
527 + if ( $textarr === false ) {
528 + $error = preg_last_error();
529 + error_log( "[MEOW GALLERY] Regex: " . preg_last_error_msg() . " (Code $error)" );
530 + return "<p class='meow-error'><b>Meow Gallery:</b> The gallery is too large for your Wordpress pcre.backtrack_limit, increase it in your server settings, or reduce your gallery size.";
531 + }
438 532 }
439 533
440 534 //The Gallery Container is where the images in the right layout will be rendered.
441 535 $html .= '<div class="mgl-gallery-container"></div>';
@@ -455,9 +549,11 @@
455 549
456 550 foreach ( $gallery_images as $image ) {
457 551 if ( !empty( $image['link_href'] ) ) {
458 552 // If there is a link, we will get the alt from the image id so we have a proper aria-label
459 - $aria = get_post_meta( $image['id'], '_wp_attachment_image_alt', true );
553 + $aria_label = __('Open image', MGL_DOMAIN);
554 + $aria_value = esc_attr( get_post_meta( $image['id'], '_wp_attachment_image_alt', true ) );
555 + $aria = !empty( $aria_value ) ? $aria_label . ': ' . $aria_value : $aria_label;
460 556
461 557 $custom_link_classes = apply_filters( 'mgl_custom_link_classes', '', $image );
462 558 $html .= '<a class="' . $custom_link_classes . '" href="' . $image['link_href'] . '" target="' . $image['link_target'] . '" rel="' . $image['link_rel'] .
463 559 '" aria-label="' . $aria . '">';
@@ -615,8 +711,26 @@
615 711 static function get_plugin_option_name() {
616 712 return self::$plugin_option_name;
617 713 }
618 714
715 + // Tiles density, from the shortcode attributes when they set one, from the options otherwise.
716 + // Shared by the front-end settings (Meow_MGL_Run) and the tiles CSS (Meow_MGL_Builders_Tiles).
717 + static function get_tiles_density( $atts = [] ) {
718 + if ( isset( $atts['density'] ) ) {
719 + return array(
720 + 'desktop' => $atts['density'],
721 + 'tablet' => $atts['density'],
722 + 'mobile' => $atts['density'],
723 + );
724 + }
725 + $options = get_option( self::$plugin_option_name, [] );
726 + return array(
727 + 'desktop' => $options['tiles_density'] ?? 'high',
728 + 'tablet' => $options['tiles_density_tablet'] ?? 'medium',
729 + 'mobile' => $options['tiles_density_mobile'] ?? 'low',
730 + );
731 + }
732 +
619 733 static function get_plugin_option( $option_name, $default = null ) {
620 734 $options = get_option( self::$plugin_option_name, null );
621 735 if ( !empty( $options ) && array_key_exists( $option_name, $options ) ) {
622 736 return $options[$option_name];
@@ -648,8 +762,10 @@
648 762 'animation' => false,
649 763 'image_size' => 'srcset',
650 764 'truncate_on_listing' => true,
651 765 'truncate_count' => 4,
766 + 'use_attachments_on_empty' => false,
767 + 'debug_logs' => false,
652 768
653 769 'rendering_mode' => 'dom', // Can be 'dom' or 'js'
654 770 'tiles_gutter' => 10,
655 771 'tiles_gutter_tablet' => 10,
@@ -687,9 +803,17 @@
687 803 'mapbox_token' => '',
688 804 'mapbox_style' => '{"username":"", "style_id":""}',
689 805 'maptiler_token' => '',
690 806
691 -
807 + // Stylish effect options
808 + 'stylish_enabled' => false,
809 + 'stylish_border_radius' => 6,
810 + 'stylish_border_width' => 0,
811 + 'stylish_border_color' => '#ffffff',
812 + 'stylish_shadow_opacity' => 0.08,
813 + 'stylish_shadow_opacity_hover' => 0.12,
814 + 'stylish_hover_lift' => 2,
815 + 'stylish_transition_speed' => 250,
692 816
693 817 //PRO OPTIONS
694 818 'infinite' => false,
695 819 'infinite_buffer' => 0,
@@ -796,8 +920,16 @@
796 920
797 921 function get_gallery_images( array $image_ids, array $atts, string $layout, string $size, array $posts_ids = []) {
798 922 global $wpdb;
799 923
924 + // Enable the image-attribute rewriting (and the 'mgl_sizes' filter) for the duration of this method.
925 + // This matters when get_gallery_images() is called directly (e.g. infinite scroll via REST) without
926 + // going through gallery(), which is what normally sets these on the initial page load.
927 + $previous_gallery_process = $this->gallery_process;
928 + $previous_gallery_layout = $this->gallery_layout;
929 + $this->gallery_process = true;
930 + $this->gallery_layout = $layout;
931 +
800 932 // Escape the array of IDs for SQL
801 933 $ids = array_map( 'intval', $image_ids );
802 934 $ids_str = implode( ',', $ids );
803 935
@@ -835,9 +967,12 @@
835 967 }
836 968 $ids = apply_filters( 'mgl_sort', $cleanIds, $images, $layout, $atts );
837 969
838 970 if ($layout === 'map') {
839 - return $this->get_map_images( $ids, $images, $atts );
971 + $map_result = $this->get_map_images( $ids, $images, $atts );
972 + $this->gallery_process = $previous_gallery_process;
973 + $this->gallery_layout = $previous_gallery_layout;
974 + return $map_result;
840 975 }
841 976
842 977 $result = [];
843 978 foreach ($ids as $index => $id) {
@@ -887,11 +1022,21 @@
887 1022 $mergedArray['featured_post_url'] = get_permalink( $post_id );
888 1023
889 1024 }
890 1025
891 - $result[] = array_merge( $image, $mergedArray, $orientation );
1026 + // Everything above can read the full attachment metadata, but it ends up in the page as
1027 + // data-gallery-images, and the front-end only needs the dimensions. Every size and the EXIF
1028 + // were bloating the HTML of big galleries (wp.org forum, 2026-09). The map layout returns
1029 + // earlier with its own data.
1030 + $item = array_merge( $image, $mergedArray, $orientation );
1031 + $meta = is_array( $image['meta'] ) ? $image['meta'] : [];
1032 + $item['meta'] = [ 'width' => $meta['width'] ?? null, 'height' => $meta['height'] ?? null ];
1033 + $result[] = $item;
892 1034 }
893 1035
1036 + $this->gallery_process = $previous_gallery_process;
1037 + $this->gallery_layout = $previous_gallery_layout;
1038 +
894 1039 return $result;
895 1040 }
896 1041
897 1042 private function get_image_class( $id, $layout, $noLightbox ) {
@@ -1100,8 +1245,60 @@
1100 1245 }
1101 1246 }
1102 1247
1103 1248
1249 + /**
1250 + * Reads the "medias" of a gallery. Only the ordered attachment IDs are stored (older versions
1251 + * also stored their URLs and mime types, which are ignored: they were a copy of the Media
1252 + * Library that went stale). This is also the shape that gets written back, and all the
1253 + * front-end needs. The Admin uses hydrate_medias() to get the URLs to display.
1254 + */
1255 + public static function normalize_medias( $medias ) {
1256 + $ids = ( is_array( $medias ) && isset( $medias['thumbnail_ids'] ) && is_array( $medias['thumbnail_ids'] ) )
1257 + ? array_values( $medias['thumbnail_ids'] ) : [];
1258 +
1259 + return [ 'thumbnail_ids' => $ids ];
1260 + }
1261 +
1262 + /**
1263 + * Adds the 'thumbnails' the Admin displays: one entry per ID, with its URLs and mime type
1264 + * resolved from the Media Library. Never stale, and a deleted attachment simply gets empty
1265 + * URLs (the Admin then shows a placeholder). Not used on the front-end, which only needs
1266 + * the IDs.
1267 + */
1268 + public static function hydrate_medias( $medias ) {
1269 + $ids = self::normalize_medias( $medias )['thumbnail_ids'];
1270 +
1271 + // One query for all the attachments instead of one per thumbnail. _prime_post_caches()
1272 + // only caches the attachments which exist, so anything still absent from the cache
1273 + // afterwards is gone: it's skipped instead of being queried on every request.
1274 + if ( !empty( $ids ) ) {
1275 + _prime_post_caches( array_values( array_unique( array_map( 'intval', $ids ) ) ), false, true );
1276 + }
1277 +
1278 + $thumbnails = [];
1279 + foreach ( $ids as $id ) {
1280 + $thumbnail = [ 'id' => $id, 'url' => '', 'zoom_url' => '', 'mime' => '' ];
1281 +
1282 + if ( !empty( $id ) && wp_cache_get( (int)$id, 'posts' ) ) {
1283 + $mime = get_post_mime_type( $id ) ?: '';
1284 + // Same rule as the latest_photos endpoint: videos have no image sizes, so their
1285 + // own URL is used for both the thumbnail and the zoom.
1286 + $is_video = strpos( $mime, 'video' ) !== false;
1287 + $url = $is_video ? wp_get_attachment_url( $id ) : wp_get_attachment_image_url( $id, 'thumbnail' );
1288 + $zoom = $is_video ? $url : wp_get_attachment_image_url( $id, 'large' );
1289 +
1290 + $thumbnail['url'] = $url ?: '';
1291 + $thumbnail['zoom_url'] = $zoom ?: $thumbnail['url'];
1292 + $thumbnail['mime'] = $mime;
1293 + }
1294 +
1295 + $thumbnails[] = $thumbnail;
1296 + }
1297 +
1298 + return [ 'thumbnail_ids' => $ids, 'thumbnails' => $thumbnails ];
1299 + }
1300 +
1104 1301 public function get_gallery_by_id( $id ) {
1105 1302 global $wpdb;
1106 1303 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1107 1304 $gallery = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $shortcodes_table WHERE id = %s", $id ), ARRAY_A );
@@ -1108,9 +1305,9 @@
1108 1305
1109 1306 if ( !$gallery ) {
1110 1307 throw new Exception( __( 'Gallery not found.', MGL_DOMAIN ));
1111 1308 }
1112 - $gallery['medias'] = maybe_unserialize( $gallery['medias'] );
1309 + $gallery['medias'] = self::normalize_medias( maybe_unserialize( $gallery['medias'] ) );
1113 1310 $gallery['posts'] = $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null;
1114 1311 $gallery['tags'] = $gallery['tags'] ? unserialize( $gallery['tags'] ) : null;
1115 1312
1116 1313 return $gallery;
@@ -1127,9 +1324,9 @@
1127 1324 $galleries[$gallery['id']] = [
1128 1325 'name' => $gallery['name'],
1129 1326 'description' => $gallery['description'],
1130 1327 'layout' => $gallery['layout'],
1131 - 'medias' => maybe_unserialize( $gallery['medias'] ),
1328 + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ),
1132 1329 'lead_image_id' => $gallery['lead_image_id'],
1133 1330 'order_by' => $gallery['order_by'],
1134 1331 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1135 1332 'dynamic_source' => $gallery['dynamic_source'],
@@ -1136,8 +1333,10 @@
1136 1333 'hero' => ( bool )$gallery['is_hero_mode'],
1137 1334 'posts' => $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null,
1138 1335 'latest_posts' => $gallery['latest_posts'],
1139 1336 'tags' => $gallery['tags'] ? unserialize( $gallery['tags'] ) : null,
1337 + 'dynamic_source' => $gallery['dynamic_source'],
1338 + 'rml' => $gallery['rml'] ?? null,
1140 1339 'updated' => strtotime( $gallery['updated_at'] )
1141 1340 ];
1142 1341 }
1143 1342 return $galleries;
@@ -1185,9 +1384,9 @@
1185 1384 $shortcodes[$gallery['id']] = [
1186 1385 'name' => $gallery['name'],
1187 1386 'description' => $gallery['description'],
1188 1387 'layout' => $gallery['layout'],
1189 - 'medias' => maybe_unserialize( $gallery['medias'] ),
1388 + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
1190 1389 'lead_image_id' => $gallery['lead_image_id'],
1191 1390 'order_by' => $gallery['order_by'],
1192 1391 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1193 1392 'hero' => ( bool )$gallery['is_hero_mode'],
@@ -1194,8 +1393,9 @@
1194 1393 'posts' => $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null,
1195 1394 'latest_posts' => $gallery['latest_posts'],
1196 1395 'tags' => $gallery['tags'] ? unserialize( $gallery['tags'] ) : null,
1197 1396 'dynamic_source' => $gallery['dynamic_source'],
1397 + 'rml' => $gallery['rml'] ?? null,
1198 1398 'rank' => intval( $gallery['pref_rank'] ?? 0 ),
1199 1399 'updated' => strtotime( $gallery['updated_at'] )
1200 1400 ];
1201 1401 }
@@ -1205,8 +1405,39 @@
1205 1405 'galleries' => $shortcodes
1206 1406 ];
1207 1407 }
1208 1408
1409 + /**
1410 + * Just the id => name pairs, for the selectors (the Gutenberg block). They only need the
1411 + * names, so this avoids shipping every gallery's medias in the page and, unlike
1412 + * get_galleries(), it isn't paginated: the selectors used to be capped at 10 entries.
1413 + */
1414 + public function get_gallery_names() {
1415 + global $wpdb;
1416 + $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1417 + Meow_MGL_Migrations::check_db();
1418 +
1419 + $names = [];
1420 + $results = $wpdb->get_results( "SELECT id, name FROM $shortcodes_table ORDER BY name ASC", ARRAY_A );
1421 + foreach ( $results as $gallery ) {
1422 + $names[$gallery['id']] = [ 'name' => $gallery['name'] ];
1423 + }
1424 + return [ 'galleries' => $names ];
1425 + }
1426 +
1427 + public function get_collection_names() {
1428 + global $wpdb;
1429 + $collections_table = $wpdb->prefix . 'mgl_collections';
1430 + Meow_MGL_Migrations::check_db();
1431 +
1432 + $names = [];
1433 + $results = $wpdb->get_results( "SELECT id, name FROM $collections_table ORDER BY name ASC", ARRAY_A );
1434 + foreach ( $results as $collection ) {
1435 + $names[$collection['id']] = [ 'name' => $collection['name'] ];
1436 + }
1437 + return [ 'collections' => $names ];
1438 + }
1439 +
1209 1440 public function get_collection_by_id( $id ) {
1210 1441 global $wpdb;
1211 1442 $collections_table = $wpdb->prefix . 'mgl_collections';
1212 1443 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
@@ -1231,9 +1462,9 @@
1231 1462 'id' => $gallery['id'],
1232 1463 'name' => $gallery['name'],
1233 1464 'description' => $gallery['description'],
1234 1465 'layout' => $gallery['layout'],
1235 - 'medias' => unserialize( $gallery['medias'] ),
1466 + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ),
1236 1467 'lead_image_id' => $gallery['lead_image_id'],
1237 1468 'order_by' => $gallery['order_by'],
1238 1469 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1239 1470 'hero' => ( bool )$gallery['is_hero_mode'],
@@ -1293,9 +1524,9 @@
1293 1524 'id' => $gallery['id'],
1294 1525 'name' => $gallery['name'],
1295 1526 'description' => $gallery['description'],
1296 1527 'layout' => $gallery['layout'],
1297 - 'medias' => unserialize( $gallery['medias'] ),
1528 + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
1298 1529 'lead_image_id' => $gallery['lead_image_id'],
1299 1530 'order_by' => $gallery['order_by'],
1300 1531 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1301 1532 'hero' => ( bool )$gallery['is_hero_mode'],