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 +147 -11 5.5.3 → 5.5.6 View file →
@@ -7,9 +7,9 @@
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 - private $preview_cutoff = 12; // Limit the number of images to show in the preview (for performance reasons)
11 + public $preview_cutoff = 12; // Limit the number of images to show in the preview (for performance reasons)
12 12
13 13 private static $plugin_option_name = 'mgl_options';
14 14 private $pro;
15 15 private $option_name = 'mgl_options';
@@ -47,9 +47,10 @@
47 47 new Meow_MGL_Run( $this );
48 48 }
49 49 }
50 50
51 - // 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).
52 53
53 54 $this->pro_module = class_exists( 'MeowPro_MGL_Core' );
54 55 if ( $this->pro_module ) {
55 56 $this->pro = new MeowPro_MGL_Core( $this );
@@ -76,8 +77,34 @@
76 77 function collection() {
77 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.";
78 79 }
79 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 +
80 107 public function can_access_settings() {
81 108 return apply_filters( 'mgl_allow_setup', current_user_can( 'manage_options' ) );
82 109 }
83 110
@@ -142,11 +169,11 @@
142 169 if ( isset( $atts['meow'] ) && $atts['meow'] === 'false' ) {
143 170 return gallery_shortcode( $atts );
144 171 }
145 172
146 - // If the attributes contain "collection" then use the collection shortcode instead
173 + // If the attributes contain "collection" then render that collection instead
147 174 if ( isset( $atts['collection'] ) && !empty( $atts['collection'] ) ) {
148 - return do_shortcode( '[meow-collection id="' . $atts['collection'] . '"]' );
175 + return $this->render_collection( $atts['collection'] );
149 176 }
150 177
151 178 $image_ids = array();
152 179 $layout = '';
@@ -173,9 +200,10 @@
173 200 try {
174 201 $shortcode = $this->get_gallery_by_id( $shortcode_id );
175 202 }
176 203 catch ( Exception $e ) {
177 - 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>";
178 206 }
179 207
180 208 if ( !isset( $shortcode['medias'] ) || !isset( $shortcode['medias']['thumbnail_ids'])) {
181 209 return "<p class='meow-error'><b>Meow Gallery:</b> Thumbnail IDs not found.</p>";
@@ -683,8 +711,26 @@
683 711 static function get_plugin_option_name() {
684 712 return self::$plugin_option_name;
685 713 }
686 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 +
687 733 static function get_plugin_option( $option_name, $default = null ) {
688 734 $options = get_option( self::$plugin_option_name, null );
689 735 if ( !empty( $options ) && array_key_exists( $option_name, $options ) ) {
690 736 return $options[$option_name];
@@ -976,9 +1022,16 @@
976 1022 $mergedArray['featured_post_url'] = get_permalink( $post_id );
977 1023
978 1024 }
979 1025
980 - $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;
981 1034 }
982 1035
983 1036 $this->gallery_process = $previous_gallery_process;
984 1037 $this->gallery_layout = $previous_gallery_layout;
@@ -1192,8 +1245,60 @@
1192 1245 }
1193 1246 }
1194 1247
1195 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 +
1196 1301 public function get_gallery_by_id( $id ) {
1197 1302 global $wpdb;
1198 1303 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1199 1304 $gallery = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $shortcodes_table WHERE id = %s", $id ), ARRAY_A );
@@ -1200,9 +1305,9 @@
1200 1305
1201 1306 if ( !$gallery ) {
1202 1307 throw new Exception( __( 'Gallery not found.', MGL_DOMAIN ));
1203 1308 }
1204 - $gallery['medias'] = maybe_unserialize( $gallery['medias'] );
1309 + $gallery['medias'] = self::normalize_medias( maybe_unserialize( $gallery['medias'] ) );
1205 1310 $gallery['posts'] = $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null;
1206 1311 $gallery['tags'] = $gallery['tags'] ? unserialize( $gallery['tags'] ) : null;
1207 1312
1208 1313 return $gallery;
@@ -1219,9 +1324,9 @@
1219 1324 $galleries[$gallery['id']] = [
1220 1325 'name' => $gallery['name'],
1221 1326 'description' => $gallery['description'],
1222 1327 'layout' => $gallery['layout'],
1223 - 'medias' => maybe_unserialize( $gallery['medias'] ),
1328 + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ),
1224 1329 'lead_image_id' => $gallery['lead_image_id'],
1225 1330 'order_by' => $gallery['order_by'],
1226 1331 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1227 1332 'dynamic_source' => $gallery['dynamic_source'],
@@ -1279,9 +1384,9 @@
1279 1384 $shortcodes[$gallery['id']] = [
1280 1385 'name' => $gallery['name'],
1281 1386 'description' => $gallery['description'],
1282 1387 'layout' => $gallery['layout'],
1283 - 'medias' => maybe_unserialize( $gallery['medias'] ),
1388 + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
1284 1389 'lead_image_id' => $gallery['lead_image_id'],
1285 1390 'order_by' => $gallery['order_by'],
1286 1391 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1287 1392 'hero' => ( bool )$gallery['is_hero_mode'],
@@ -1300,8 +1405,39 @@
1300 1405 'galleries' => $shortcodes
1301 1406 ];
1302 1407 }
1303 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 +
1304 1440 public function get_collection_by_id( $id ) {
1305 1441 global $wpdb;
1306 1442 $collections_table = $wpdb->prefix . 'mgl_collections';
1307 1443 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
@@ -1326,9 +1462,9 @@
1326 1462 'id' => $gallery['id'],
1327 1463 'name' => $gallery['name'],
1328 1464 'description' => $gallery['description'],
1329 1465 'layout' => $gallery['layout'],
1330 - 'medias' => unserialize( $gallery['medias'] ),
1466 + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ),
1331 1467 'lead_image_id' => $gallery['lead_image_id'],
1332 1468 'order_by' => $gallery['order_by'],
1333 1469 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1334 1470 'hero' => ( bool )$gallery['is_hero_mode'],
@@ -1388,9 +1524,9 @@
1388 1524 'id' => $gallery['id'],
1389 1525 'name' => $gallery['name'],
1390 1526 'description' => $gallery['description'],
1391 1527 'layout' => $gallery['layout'],
1392 - 'medias' => unserialize( $gallery['medias'] ),
1528 + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
1393 1529 'lead_image_id' => $gallery['lead_image_id'],
1394 1530 'order_by' => $gallery['order_by'],
1395 1531 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1396 1532 'hero' => ( bool )$gallery['is_hero_mode'],