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/core.php +200 -12 5.5.25.5.5 View file →
@@ -7,8 +7,10 @@
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';
12 14 private $pro;
13 15 private $option_name = 'mgl_options';
14 16 private $infinite_layouts = [
@@ -21,8 +23,11 @@
21 23 ];
22 24
23 25 private $rewrittenMwlData = [];
24 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 +
25 30 public function __construct() {
26 31 load_plugin_textdomain( MGL_DOMAIN, false, MGL_PATH . '/languages' );
27 32
28 33 //TODO: Move Skeleton into PRO
@@ -33,16 +38,19 @@
33 38 // Initializes the classes needed
34 39 MeowKit_MGL_Helpers::is_rest() && new Meow_MGL_Rest( $this );
35 40
36 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 +
37 44 if ( !MeowKit_MGL_Helpers::is_asynchronous_request() ) {
38 - add_filter( 'wp_get_attachment_image_attributes', array( $this, 'wp_get_attachment_image_attributes' ), 25, 3 );
45 +
39 46 if ( is_admin() || $this->is_gallery_used ) {
40 47 new Meow_MGL_Run( $this );
41 48 }
42 49 }
43 50
44 - // 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).
45 53
46 54 $this->pro_module = class_exists( 'MeowPro_MGL_Core' );
47 55 if ( $this->pro_module ) {
48 56 $this->pro = new MeowPro_MGL_Core( $this );
@@ -69,8 +77,34 @@
69 77 function collection() {
70 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.";
71 79 }
72 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 +
73 107 public function can_access_settings() {
74 108 return apply_filters( 'mgl_allow_setup', current_user_can( 'manage_options' ) );
75 109 }
76 110
@@ -95,11 +129,14 @@
95 129 else if ( $this->gallery_layout === 'cascade' )
96 130 $sizes = '80vw';
97 131 else if ( $this->gallery_layout === 'justified' )
98 132 $sizes = '(max-width: 800px) 80vw, 50vw';
133 +
99 134 $sizes = apply_filters( 'mgl_sizes', $sizes, $this->gallery_layout, $attachment, $attr );
135 +
100 136 if ( !empty( $sizes ) )
101 137 $attr['sizes'] = $sizes;
138 +
102 139 return $attr;
103 140 }
104 141
105 142 function get_rewritten_mwl_data() {
@@ -105,8 +142,14 @@
105 142 function get_rewritten_mwl_data() {
106 143 return $this->rewrittenMwlData;
107 144 }
108 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 +
109 152 function gallery( $atts, $options = [] ) {
110 153 $atts = apply_filters( 'shortcode_atts_gallery', $atts, null, $atts, 'gallery' );
111 154
112 155 $isPreview = isset( $options['isPreview'] ) ? $options['isPreview'] : false;
@@ -126,11 +169,11 @@
126 169 if ( isset( $atts['meow'] ) && $atts['meow'] === 'false' ) {
127 170 return gallery_shortcode( $atts );
128 171 }
129 172
130 - // If the attributes contain "collection" then use the collection shortcode instead
173 + // If the attributes contain "collection" then render that collection instead
131 174 if ( isset( $atts['collection'] ) && !empty( $atts['collection'] ) ) {
132 - return do_shortcode( '[meow-collection id="' . $atts['collection'] . '"]' );
175 + return $this->render_collection( $atts['collection'] );
133 176 }
134 177
135 178 $image_ids = array();
136 179 $layout = '';
@@ -141,8 +184,9 @@
141 184 $has_include = isset( $atts['include'] ) && !empty( $atts['include'] );
142 185 $has_tags = isset( $atts['tags'] ) && !empty( $atts['tags'] );
143 186 $has_posts = isset( $atts['posts'] ) && !empty( $atts['posts'] );
144 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' );
145 189
146 190 if ( $has_id && $has_ids ) {
147 191 unset( $atts['ids'] );
148 192 error_log( "⚠️ Meow Gallery: in gallery $atts[id] both 'id' and 'ids' attributes are used in the same shortcode. 'id' will be ignored." );
@@ -156,9 +200,10 @@
156 200 try {
157 201 $shortcode = $this->get_gallery_by_id( $shortcode_id );
158 202 }
159 203 catch ( Exception $e ) {
160 - 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>";
161 206 }
162 207
163 208 if ( !isset( $shortcode['medias'] ) || !isset( $shortcode['medias']['thumbnail_ids'])) {
164 209 return "<p class='meow-error'><b>Meow Gallery:</b> Thumbnail IDs not found.</p>";
@@ -265,8 +310,15 @@
265 310 }
266 311
267 312 }
268 313
314 + if( $has_attachments ) {
315 + $attachmentIds = $this->get_attached_image_ids();
316 + if ( !empty( $attachmentIds ) ) {
317 + $image_ids = implode( ',', $attachmentIds );
318 + }
319 + }
320 +
269 321 $posts_ids = [];
270 322 if ( $has_posts ) {
271 323
272 324 $posts_ids = is_array( $atts['posts'] ) ? $atts['posts'] : explode( ',', $atts['posts'] );
@@ -296,16 +348,32 @@
296 348 $image_ids = implode( ',', $ids );
297 349
298 350 #endregion
299 351
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();
355 + if ( !empty( $attachmentIds ) ) {
356 + $image_ids = implode( ',', $attachmentIds );
357 + $has_attachments = true;
358 + }
359 + }
360 +
300 361 // Use attached images if still empty
301 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>";
366 + }
367 +
302 368 return "<p class='meow-error'><b>Meow Gallery:</b> The gallery is empty.</p>";
303 369 }
304 370
305 371 if ( $isPreview ) {
306 372 $check = explode( ',', $image_ids );
307 - $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 ) ];
308 376 $image_ids = implode( ',', $check );
309 377 }
310 378
311 379 // Limit images on archive/listing pages (not viewing the full single post)
@@ -643,8 +711,26 @@
643 711 static function get_plugin_option_name() {
644 712 return self::$plugin_option_name;
645 713 }
646 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 +
647 733 static function get_plugin_option( $option_name, $default = null ) {
648 734 $options = get_option( self::$plugin_option_name, null );
649 735 if ( !empty( $options ) && array_key_exists( $option_name, $options ) ) {
650 736 return $options[$option_name];
@@ -676,8 +762,10 @@
676 762 'animation' => false,
677 763 'image_size' => 'srcset',
678 764 'truncate_on_listing' => true,
679 765 'truncate_count' => 4,
766 + 'use_attachments_on_empty' => false,
767 + 'debug_logs' => false,
680 768
681 769 'rendering_mode' => 'dom', // Can be 'dom' or 'js'
682 770 'tiles_gutter' => 10,
683 771 'tiles_gutter_tablet' => 10,
@@ -718,10 +806,13 @@
718 806
719 807 // Stylish effect options
720 808 'stylish_enabled' => false,
721 809 'stylish_border_radius' => 6,
810 + 'stylish_border_width' => 0,
811 + 'stylish_border_color' => '#ffffff',
722 812 'stylish_shadow_opacity' => 0.08,
723 813 'stylish_shadow_opacity_hover' => 0.12,
814 + 'stylish_hover_lift' => 2,
724 815 'stylish_transition_speed' => 250,
725 816
726 817 //PRO OPTIONS
727 818 'infinite' => false,
@@ -829,8 +920,16 @@
829 920
830 921 function get_gallery_images( array $image_ids, array $atts, string $layout, string $size, array $posts_ids = []) {
831 922 global $wpdb;
832 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 +
833 932 // Escape the array of IDs for SQL
834 933 $ids = array_map( 'intval', $image_ids );
835 934 $ids_str = implode( ',', $ids );
836 935
@@ -868,9 +967,12 @@
868 967 }
869 968 $ids = apply_filters( 'mgl_sort', $cleanIds, $images, $layout, $atts );
870 969
871 970 if ($layout === 'map') {
872 - 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;
873 975 }
874 976
875 977 $result = [];
876 978 foreach ($ids as $index => $id) {
@@ -923,8 +1025,11 @@
923 1025
924 1026 $result[] = array_merge( $image, $mergedArray, $orientation );
925 1027 }
926 1028
1029 + $this->gallery_process = $previous_gallery_process;
1030 + $this->gallery_layout = $previous_gallery_layout;
1031 +
927 1032 return $result;
928 1033 }
929 1034
930 1035 private function get_image_class( $id, $layout, $noLightbox ) {
@@ -1133,8 +1238,60 @@
1133 1238 }
1134 1239 }
1135 1240
1136 1241
1242 + /**
1243 + * Reads the "medias" of a gallery. Only the ordered attachment IDs are stored (older versions
1244 + * also stored their URLs and mime types, which are ignored: they were a copy of the Media
1245 + * Library that went stale). This is also the shape that gets written back, and all the
1246 + * front-end needs. The Admin uses hydrate_medias() to get the URLs to display.
1247 + */
1248 + public static function normalize_medias( $medias ) {
1249 + $ids = ( is_array( $medias ) && isset( $medias['thumbnail_ids'] ) && is_array( $medias['thumbnail_ids'] ) )
1250 + ? array_values( $medias['thumbnail_ids'] ) : [];
1251 +
1252 + return [ 'thumbnail_ids' => $ids ];
1253 + }
1254 +
1255 + /**
1256 + * Adds the 'thumbnails' the Admin displays: one entry per ID, with its URLs and mime type
1257 + * resolved from the Media Library. Never stale, and a deleted attachment simply gets empty
1258 + * URLs (the Admin then shows a placeholder). Not used on the front-end, which only needs
1259 + * the IDs.
1260 + */
1261 + public static function hydrate_medias( $medias ) {
1262 + $ids = self::normalize_medias( $medias )['thumbnail_ids'];
1263 +
1264 + // One query for all the attachments instead of one per thumbnail. _prime_post_caches()
1265 + // only caches the attachments which exist, so anything still absent from the cache
1266 + // afterwards is gone: it's skipped instead of being queried on every request.
1267 + if ( !empty( $ids ) ) {
1268 + _prime_post_caches( array_values( array_unique( array_map( 'intval', $ids ) ) ), false, true );
1269 + }
1270 +
1271 + $thumbnails = [];
1272 + foreach ( $ids as $id ) {
1273 + $thumbnail = [ 'id' => $id, 'url' => '', 'zoom_url' => '', 'mime' => '' ];
1274 +
1275 + if ( !empty( $id ) && wp_cache_get( (int)$id, 'posts' ) ) {
1276 + $mime = get_post_mime_type( $id ) ?: '';
1277 + // Same rule as the latest_photos endpoint: videos have no image sizes, so their
1278 + // own URL is used for both the thumbnail and the zoom.
1279 + $is_video = strpos( $mime, 'video' ) !== false;
1280 + $url = $is_video ? wp_get_attachment_url( $id ) : wp_get_attachment_image_url( $id, 'thumbnail' );
1281 + $zoom = $is_video ? $url : wp_get_attachment_image_url( $id, 'large' );
1282 +
1283 + $thumbnail['url'] = $url ?: '';
1284 + $thumbnail['zoom_url'] = $zoom ?: $thumbnail['url'];
1285 + $thumbnail['mime'] = $mime;
1286 + }
1287 +
1288 + $thumbnails[] = $thumbnail;
1289 + }
1290 +
1291 + return [ 'thumbnail_ids' => $ids, 'thumbnails' => $thumbnails ];
1292 + }
1293 +
1137 1294 public function get_gallery_by_id( $id ) {
1138 1295 global $wpdb;
1139 1296 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1140 1297 $gallery = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $shortcodes_table WHERE id = %s", $id ), ARRAY_A );
@@ -1141,9 +1298,9 @@
1141 1298
1142 1299 if ( !$gallery ) {
1143 1300 throw new Exception( __( 'Gallery not found.', MGL_DOMAIN ));
1144 1301 }
1145 - $gallery['medias'] = maybe_unserialize( $gallery['medias'] );
1302 + $gallery['medias'] = self::normalize_medias( maybe_unserialize( $gallery['medias'] ) );
1146 1303 $gallery['posts'] = $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null;
1147 1304 $gallery['tags'] = $gallery['tags'] ? unserialize( $gallery['tags'] ) : null;
1148 1305
1149 1306 return $gallery;
@@ -1160,9 +1317,9 @@
1160 1317 $galleries[$gallery['id']] = [
1161 1318 'name' => $gallery['name'],
1162 1319 'description' => $gallery['description'],
1163 1320 'layout' => $gallery['layout'],
1164 - 'medias' => maybe_unserialize( $gallery['medias'] ),
1321 + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ),
1165 1322 'lead_image_id' => $gallery['lead_image_id'],
1166 1323 'order_by' => $gallery['order_by'],
1167 1324 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1168 1325 'dynamic_source' => $gallery['dynamic_source'],
@@ -1220,9 +1377,9 @@
1220 1377 $shortcodes[$gallery['id']] = [
1221 1378 'name' => $gallery['name'],
1222 1379 'description' => $gallery['description'],
1223 1380 'layout' => $gallery['layout'],
1224 - 'medias' => maybe_unserialize( $gallery['medias'] ),
1381 + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
1225 1382 'lead_image_id' => $gallery['lead_image_id'],
1226 1383 'order_by' => $gallery['order_by'],
1227 1384 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1228 1385 'hero' => ( bool )$gallery['is_hero_mode'],
@@ -1241,8 +1398,39 @@
1241 1398 'galleries' => $shortcodes
1242 1399 ];
1243 1400 }
1244 1401
1402 + /**
1403 + * Just the id => name pairs, for the selectors (the Gutenberg block). They only need the
1404 + * names, so this avoids shipping every gallery's medias in the page and, unlike
1405 + * get_galleries(), it isn't paginated: the selectors used to be capped at 10 entries.
1406 + */
1407 + public function get_gallery_names() {
1408 + global $wpdb;
1409 + $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1410 + Meow_MGL_Migrations::check_db();
1411 +
1412 + $names = [];
1413 + $results = $wpdb->get_results( "SELECT id, name FROM $shortcodes_table ORDER BY name ASC", ARRAY_A );
1414 + foreach ( $results as $gallery ) {
1415 + $names[$gallery['id']] = [ 'name' => $gallery['name'] ];
1416 + }
1417 + return [ 'galleries' => $names ];
1418 + }
1419 +
1420 + public function get_collection_names() {
1421 + global $wpdb;
1422 + $collections_table = $wpdb->prefix . 'mgl_collections';
1423 + Meow_MGL_Migrations::check_db();
1424 +
1425 + $names = [];
1426 + $results = $wpdb->get_results( "SELECT id, name FROM $collections_table ORDER BY name ASC", ARRAY_A );
1427 + foreach ( $results as $collection ) {
1428 + $names[$collection['id']] = [ 'name' => $collection['name'] ];
1429 + }
1430 + return [ 'collections' => $names ];
1431 + }
1432 +
1245 1433 public function get_collection_by_id( $id ) {
1246 1434 global $wpdb;
1247 1435 $collections_table = $wpdb->prefix . 'mgl_collections';
1248 1436 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
@@ -1267,9 +1455,9 @@
1267 1455 'id' => $gallery['id'],
1268 1456 'name' => $gallery['name'],
1269 1457 'description' => $gallery['description'],
1270 1458 'layout' => $gallery['layout'],
1271 - 'medias' => unserialize( $gallery['medias'] ),
1459 + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ),
1272 1460 'lead_image_id' => $gallery['lead_image_id'],
1273 1461 'order_by' => $gallery['order_by'],
1274 1462 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1275 1463 'hero' => ( bool )$gallery['is_hero_mode'],
@@ -1329,9 +1517,9 @@
1329 1517 'id' => $gallery['id'],
1330 1518 'name' => $gallery['name'],
1331 1519 'description' => $gallery['description'],
1332 1520 'layout' => $gallery['layout'],
1333 - 'medias' => unserialize( $gallery['medias'] ),
1521 + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
1334 1522 'lead_image_id' => $gallery['lead_image_id'],
1335 1523 'order_by' => $gallery['order_by'],
1336 1524 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1337 1525 'hero' => ( bool )$gallery['is_hero_mode'],