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 +1461 -42 4.2.45.5.5 View file →
@@ -2,27 +2,109 @@
2 2
3 3 class Meow_MGL_Core {
4 4
5 5 private $gallery_process = false;
6 + private $gallery_layout = 'tiles';
6 7 private $is_gallery_used = true; // TODO: Would be nice to detect if the gallery is actually used on the current page.
8 + private $skeleton_handler;
9 + private $pro_module = false;
7 10
11 + public $preview_cutoff = 12; // Limit the number of images to show in the preview (for performance reasons)
12 +
13 + private static $plugin_option_name = 'mgl_options';
14 + private $pro;
15 + private $option_name = 'mgl_options';
16 + private $infinite_layouts = [
17 + 'tiles',
18 + 'masonry',
19 + 'justified',
20 + 'square',
21 + 'cascade',
22 + // 'carousel', Added dynamically if the option is enabled
23 + ];
24 +
25 + private $rewrittenMwlData = [];
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 +
8 30 public function __construct() {
9 31 load_plugin_textdomain( MGL_DOMAIN, false, MGL_PATH . '/languages' );
10 32
33 + //TODO: Move Skeleton into PRO
34 + // Initialize skeleton handler
35 + require_once( MGL_PATH . '/classes/skeleton.php' );
36 + $this->skeleton_handler = new Meow_MGL_Skeleton();
37 +
11 38 // Initializes the classes needed
12 - MeowCommon_Helpers::is_rest() && new Meow_MGL_Rest( $this );
13 - is_admin() && new Meow_MGL_Admin();
14 - class_exists( 'MeowPro_MGL_Core' ) && new MeowPro_MGL_Core();
39 + MeowKit_MGL_Helpers::is_rest() && new Meow_MGL_Rest( $this );
15 40
16 41 // The gallery build process should only be enabled if the request is non-asynchronous
17 - if ( !MeowCommon_Helpers::is_asynchronous_request() ) {
18 - add_filter( 'wp_get_attachment_image_attributes', array( $this, 'wp_get_attachment_image_attributes' ), 25, 3 );
42 + add_filter( 'wp_get_attachment_image_attributes', array( $this, 'wp_get_attachment_image_attributes' ), 25, 3 );
43 +
44 + if ( !MeowKit_MGL_Helpers::is_asynchronous_request() ) {
45 +
19 46 if ( is_admin() || $this->is_gallery_used ) {
20 47 new Meow_MGL_Run( $this );
21 48 }
22 49 }
50 +
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).
53 +
54 + $this->pro_module = class_exists( 'MeowPro_MGL_Core' );
55 + if ( $this->pro_module ) {
56 + $this->pro = new MeowPro_MGL_Core( $this );
57 + } else {
58 + add_shortcode( 'meow-collection', array( $this, 'collection' ) );
59 + }
60 +
61 + // Initialize the Admin if needed
62 + add_action( 'init', array( $this, 'init' ) );
23 63 }
24 64
65 + function init() {
66 + is_admin() && new Meow_MGL_Admin( $this );
67 +
68 + global $wpmgl;
69 + $wpmgl = $this;
70 +
71 + if ( $this->pro_module ) {
72 + global $wpmgl_pro;
73 + $wpmgl_pro = $this->pro;
74 + }
75 + }
76 +
77 + function collection() {
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.";
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 +
25 107 public function can_access_settings() {
26 108 return apply_filters( 'mgl_allow_setup', current_user_can( 'manage_options' ) );
27 109 }
28 110
@@ -35,8 +117,9 @@
35 117 // Rewrite the sizes attributes of the src-set for each image
36 118 function wp_get_attachment_image_attributes( $attr, $attachment, $size ) {
37 119 if (!$this->gallery_process)
38 120 return $attr;
121 +
39 122 $sizes = null;
40 123 if ( $this->gallery_layout === 'tiles' )
41 124 $sizes = '50vw';
42 125 else if ( $this->gallery_layout === 'masonry' )
@@ -46,65 +129,331 @@
46 129 else if ( $this->gallery_layout === 'cascade' )
47 130 $sizes = '80vw';
48 131 else if ( $this->gallery_layout === 'justified' )
49 132 $sizes = '(max-width: 800px) 80vw, 50vw';
133 +
50 134 $sizes = apply_filters( 'mgl_sizes', $sizes, $this->gallery_layout, $attachment, $attr );
135 +
51 136 if ( !empty( $sizes ) )
52 137 $attr['sizes'] = $sizes;
138 +
53 139 return $attr;
54 140 }
55 141
56 - function gallery( $atts, $isPreview = false ) {
57 - $atts = apply_filters( 'shortcode_atts_gallery', $atts, null, $atts );
142 + function get_rewritten_mwl_data() {
143 + return $this->rewrittenMwlData;
144 + }
58 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 +
152 + function gallery( $atts, $options = [] ) {
153 + $atts = apply_filters( 'shortcode_atts_gallery', $atts, null, $atts, 'gallery' );
154 +
155 + $isPreview = isset( $options['isPreview'] ) ? $options['isPreview'] : false;
156 + $isRest = isset( $options['isRest'] ) ? $options['isRest'] : false;
157 +
158 + // Sanitize the atts to avoid XSS
159 + $atts = array_map( function( $x ) {
160 + if ( is_array( $x ) ) {
161 + // In case it contains an array, we need to sanitize each element, and avoid a string conversion issue
162 + return array_map( function( $y ) { return is_null( $y ) ? $y : esc_attr( $y ); }, $x );
163 + } else {
164 + // We don't sanitize null value, as it would convert it to a empty string
165 + return is_null( $x ) ? $x : esc_attr( $x );
166 + }
167 + }, $atts );
168 +
169 + if ( isset( $atts['meow'] ) && $atts['meow'] === 'false' ) {
170 + return gallery_shortcode( $atts );
171 + }
172 +
173 + // If the attributes contain "collection" then render that collection instead
174 + if ( isset( $atts['collection'] ) && !empty( $atts['collection'] ) ) {
175 + return $this->render_collection( $atts['collection'] );
176 + }
177 +
178 + $image_ids = array();
179 + $layout = '';
180 +
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' );
189 +
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." );
193 + }
194 +
59 195 // Get the IDs
60 - $images = array();
61 - if ( isset( $atts['ids'] ) )
62 - $images = $atts['ids'];
63 - if ( isset( $atts['include'] ) ) {
64 - $images = is_array( $atts['include'] ) ? implode( ',', $atts['include'] ) : $atts['include'];
65 - $atts['include'] = $images;
196 + #region media_ids
197 + if ( $has_id && !$has_ids ) {
198 + $shortcode_id = $atts['id'];
199 +
200 + try {
201 + $shortcode = $this->get_gallery_by_id( $shortcode_id );
202 + }
203 + catch ( Exception $e ) {
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>";
206 + }
207 +
208 + if ( !isset( $shortcode['medias'] ) || !isset( $shortcode['medias']['thumbnail_ids'])) {
209 + return "<p class='meow-error'><b>Meow Gallery:</b> Thumbnail IDs not found.</p>";
210 + }
211 +
212 + $image_ids = $shortcode['medias']['thumbnail_ids'];
213 + unset( $shortcode['medias'] );
214 +
215 + //* We merge $atts into $shortcode ( not $shortcode into $atts ) so we can override the gallery settings even when using the ID.
216 + // Override specifics: "default" layout from atts should be the layout from shortcode
217 + if( array_key_exists('layout', $atts) && $atts['layout'] == 'default' )
218 + {
219 + $atts['layout'] = $shortcode['layout'];
220 + }
221 +
222 + $atts = array_merge( $shortcode, $atts );
66 223 }
67 - if ( empty( $images ) ) {
68 - $attachments = get_attached_media( 'image' );
69 - $attachmentIds = array_map( function($x) { return $x->ID; }, $attachments );
70 - if ( !empty( $attachmentIds ) )
71 - $images = implode( ',', $attachmentIds );
72 - else
73 - return "<p class='meow-error'><b>Meow Gallery:</b> The gallery is empty.</p>";
224 +
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 );
74 236 }
75 237
238 + if ( $has_ids ) {
239 + $image_ids = $atts['ids'];
240 + }
241 +
242 + if ( $has_include ) {
243 + $image_ids = is_array( $atts['include'] ) ? implode( ',', $atts['include'] ) : $atts['include'];
244 + $atts['include'] = $image_ids;
245 + }
246 +
247 + // Tags support
248 + if ( $has_tags ) {
249 + $tags = is_array( $atts['tags'] ) ? $atts['tags'] : array_map( 'trim', explode( ',', $atts['tags'] ) );
250 +
251 + // Try multiple common taxonomies used for media tagging
252 + $taxonomies_to_try = [ 'post_tag', 'media_tag', 'attachment_tag', 'attachment_category' ];
253 + $taxonomies_to_try = apply_filters( 'mgl_tags_taxonomies', $taxonomies_to_try );
254 +
255 + $tagged_media_ids = [];
256 +
257 + foreach ( $taxonomies_to_try as $taxonomy ) {
258 + if ( !taxonomy_exists( $taxonomy ) ) {
259 + continue;
260 + }
261 +
262 + $args = [
263 + 'post_type' => 'attachment',
264 + 'post_status' => 'inherit',
265 + 'posts_per_page' => -1,
266 + 'fields' => 'ids',
267 + 'tax_query' => [
268 + [
269 + 'taxonomy' => $taxonomy,
270 + 'field' => 'slug',
271 + 'terms' => $tags,
272 + ]
273 + ]
274 + ];
275 +
276 + $query = new WP_Query( $args );
277 + if ( !empty( $query->posts ) ) {
278 + $tagged_media_ids = array_merge( $tagged_media_ids, $query->posts );
279 + }
280 + }
281 +
282 + $tagged_media_ids = array_unique( $tagged_media_ids );
283 +
284 + if ( !empty( $tagged_media_ids ) ) {
285 + if ( !empty( $image_ids ) ) {
286 + // Merge with existing IDs
287 + $existing_ids = is_array( $image_ids ) ? $image_ids : explode( ',', $image_ids );
288 + $image_ids = implode( ',', array_unique( array_merge( $existing_ids, $tagged_media_ids ) ) );
289 + } else {
290 + $image_ids = implode( ',', $tagged_media_ids );
291 + }
292 + }
293 + }
294 +
295 + if ( $has_latest_posts ) {
296 + $num_posts = intval( $atts['latest_posts'] );
297 +
298 + if ( $num_posts > 0 ) {
299 +
300 + $latest_posts = get_posts( [ 'numberposts' => $num_posts ] );
301 + $latest_posts_ids = array_map( function( $x ) { return $x->ID; }, $latest_posts );
302 +
303 + if ( $has_posts ) {
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'.");
305 + $atts['posts'] = array_merge( $latest_posts_ids, explode(',', $atts['posts']) );
306 + }
307 + else {
308 + $atts['posts'] = implode( ',', $latest_posts_ids );
309 + }
310 + }
311 +
312 + }
313 +
314 + if( $has_attachments ) {
315 + $attachmentIds = $this->get_attached_image_ids();
316 + if ( !empty( $attachmentIds ) ) {
317 + $image_ids = implode( ',', $attachmentIds );
318 + }
319 + }
320 +
321 + $posts_ids = [];
322 + if ( $has_posts ) {
323 +
324 + $posts_ids = is_array( $atts['posts'] ) ? $atts['posts'] : explode( ',', $atts['posts'] );
325 + $featured_images = [];
326 +
327 + foreach ($posts_ids as $key => $post_id) {
328 + $image_id = get_post_thumbnail_id($post_id);
329 + if ($image_id === false || $image_id == 0) {
330 + unset($posts_ids[$key]);
331 + } else {
332 + $featured_images[] = $image_id;
333 + }
334 + }
335 +
336 + if ( count( $posts_ids ) !== count( $featured_images ) ) {
337 + return "<p class='meow-error'><b>Meow Gallery:</b> The number of featured images and posts id should be the same.</p>";
338 + }
339 +
340 + $image_ids = implode(',', $featured_images);
341 + $posts_ids = array_values($posts_ids);
342 + }
343 +
344 +
345 + // Filter the IDs
346 + $ids = is_array( $image_ids ) ? $image_ids : explode( ',', $image_ids );
347 + $ids = apply_filters( 'mgl_ids', $ids, $atts );
348 + $image_ids = implode( ',', $ids );
349 +
350 + #endregion
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 +
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>";
366 + }
367 +
368 + return "<p class='meow-error'><b>Meow Gallery:</b> The gallery is empty.</p>";
369 + }
370 +
76 371 if ( $isPreview ) {
77 - $check = explode( ',', $images );
78 - $check = array_slice( $check, 0, 40 );
79 - $images = implode( ',', $check );
372 + $check = explode( ',', $image_ids );
373 + $total = count( $check );
374 + $check = array_slice( $check, 0, $this->preview_cutoff );
375 + $this->last_preview_counts = [ 'total' => $total, 'shown' => count( $check ) ];
376 + $image_ids = implode( ',', $check );
80 377 }
81 378
379 + // Limit images on archive/listing pages (not viewing the full single post)
380 + $should_truncate = $this->get_option( 'truncate_on_listing', true );
381 + $is_archive_context = !is_singular() && !is_admin() && !$isPreview && !$isRest && $should_truncate;
382 + $is_archive_context = apply_filters( 'mgl_is_archive_context', $is_archive_context, $atts );
383 + if ( $is_archive_context ) {
384 + $archive_limit = intval( $this->get_option( 'truncate_count', 4 ) );
385 +
386 + if( $archive_limit === 0 ) {
387 + return ""; // If the user does not want the gallery to be shown at all on listing pages
388 + }
389 +
390 + if ( $archive_limit > 0 ) {
391 + $check = explode( ',', $image_ids );
392 + if ( count( $check ) > $archive_limit ) {
393 + $check = array_slice( $check, 0, $archive_limit );
394 + $image_ids = implode( ',', $check );
395 + $atts['is_truncated'] = true; // Flag to potentially show "view more" indicator
396 + }
397 + }
398 + }
399 +
82 400 // Ordering
83 - if ( isset( $atts['orderby'] ) ) {
84 - $images = explode( ',', $images );
85 - $images = Meow_MGL_OrderBy::run( $images, $atts['orderby'], isset( $atts['order'] ) ? $atts['order'] : 'asc' );
86 - $images = implode( ',', $images );
401 + if ( isset( $atts['orderby'] ) || isset( $atts['order_by'] ) ) {
402 +
403 + $orderby = '';
404 + $order = 'asc';
405 +
406 + if ( isset( $atts['order'] ) ) {
407 + $order = $atts['order'];
408 + }
409 +
410 + if ( isset( $atts['orderby'] ) ) {
411 + $orderby = $atts['orderby'];
412 + }
413 +
414 + if ( isset( $atts['order_by'] ) ) {
415 + $orderby = $atts['order_by'];
416 + }
417 +
418 + if( strpos( $orderby, '-' ) != false ) {
419 + $left = explode( '-', $orderby )[0];
420 + $right = explode( '-', $orderby )[1];
421 +
422 + $orderby = $left;
423 + $order = $right;
424 + }
425 +
426 +
427 + $image_ids = explode( ',', $image_ids );
428 + $image_ids = Meow_MGL_OrderBy::run( $image_ids, $orderby, $order );
429 + $image_ids = implode( ',', $image_ids );
87 430 }
88 -
431 +
89 432 // Layout
90 - $layout = 'none';
91 - if ( isset( $atts['layout'] ) && $atts['layout'] != 'default' )
433 + if ( isset( $atts['layout'] ) && $atts['layout'] != 'default' ) {
92 434 $layout = $atts['layout'];
93 - else if ( isset( $atts['mgl-layout'] ) && $atts['mgl-layout'] != 'default' )
435 + }
436 + else if ( isset( $atts['mgl-layout'] ) && $atts['mgl-layout'] != 'default' ) {
94 437 $layout = $atts['mgl-layout'];
95 - else
96 - $layout = get_option( 'mgl_layout', 'tiles' );
438 + $atts['layout'] = $layout;
439 + } else {
440 + $layout = $this->get_option( 'layout', 'tiles' );
441 + $atts['layout'] = $layout;
442 + }
97 443
98 - // Check the settings
99 - if ( $layout === 'none' )
100 - return gallery_shortcode( $atts );
444 +
445 + if ( $layout === 'none' || $layout === '' ) {
446 + $layout = $this->get_option( 'layout', 'tiles' );
447 + }
448 +
449 +
101 450 $layoutClass = 'Meow_MGL_Builders_' . ucfirst( $layout );
102 451 if ( !class_exists( $layoutClass ) ) {
103 452 error_log( "Meow Gallery: Class $layoutClass does not exist." );
104 453 return "<p class='meow-error'><b>Meow Gallery:</b> The layout $layout is not available in this version.</p>";
105 454 }
106 -
455 +
107 456 // Captions
108 457 if ( isset( $atts['captions'] ) && ( $atts['captions'] === false || $atts['captions'] === 'false' ) ) {
109 458 // This is to avoid issues linked to the old block editor for the Meow Gallery
110 459 $atts['captions'] = 'none';
@@ -109,8 +458,13 @@
109 458 // This is to avoid issues linked to the old block editor for the Meow Gallery
110 459 $atts['captions'] = 'none';
111 460 }
112 461
462 + // apply filter 'mgl_sort_ahead' to sort the images before anything else
463 + if ( !empty( $image_ids ) ){
464 + $image_ids = implode( ',', apply_filters( 'mgl_sort_ahead', explode( ',', $image_ids ), $layout, $atts ) );
465 + }
466 +
113 467 //DEBUG: Display $atts
114 468 //error_log( print_r( $atts, 1 ) );
115 469
116 470 // Start the process of building the gallery
@@ -119,18 +473,1083 @@
119 473
120 474 // This should be probably removed.
121 475 // wp_enqueue_style( 'mgl-css' );
122 476
123 - $infinite = get_option( 'mgl_infinite', false ) && class_exists( 'MeowPro_MGL_Core' );
124 - $gen = new $layoutClass( $atts, !$isPreview && $infinite, $isPreview );
125 - $result = $gen->build( $images );
126 - $this->gallery_process = false;
477 + $infinite = $this->get_option( 'infinite', false ) && class_exists( 'MeowPro_MGL_Core' );
478 +
479 + // $gen = new $layoutClass( $atts, !$isPreview && $infinite, $isPreview );
480 + // $result = $gen->build( $image_ids );
481 +
127 482 do_action( 'mgl_' . $layout . '_gallery_created', $layout );
128 483 //$result = apply_filters( 'post_gallery', $result, $atts, null );
484 +
485 + $this->rewrittenMwlData = apply_filters('mgl_force_rewrite_mwl_data', explode( ',', $image_ids ) );
486 + do_action( 'mgl_gallery_created', $atts, explode( ',', $image_ids ), $layout );
129 487
130 - do_action( 'mgl_gallery_created' );
488 + $gallery_options = $this->get_gallery_options( $image_ids, $atts, $infinite, $isPreview, $layout );
131 489
490 + // If infinite scroll option was enabled, get the images up to 12 at first.
491 + $loading_image_ids = explode(',', $image_ids);
492 + $loading_image_ids = apply_filters( 'mgl_sort', $loading_image_ids, [], $layout, $atts );
493 +
494 + // Only add the carousel to the infinite layouts if the option is enabled
495 + if( $infinite && $this->get_option( 'carousel_infinite', false ) ) {
496 + $this->infinite_layouts[] = 'carousel';
497 + }
498 +
499 + if (!$isPreview && $infinite && in_array( $layout, $this->infinite_layouts ) ) {
500 + $loading_image_ids = array_slice( $loading_image_ids, 0, 12 );
501 + }
502 +
503 + $gallery_images = $this->get_gallery_images( $loading_image_ids, $atts, $layout, $gallery_options['size'], $posts_ids );
504 +
505 + // Get the class and data attributes
506 + $class = $this->get_mgl_root_class( $atts );
507 + $data_atts = $this->get_data_as_json( $atts );
508 + $data_gallery_options = $this->get_data_as_json( $gallery_options );
509 + $data_gallery_images = $this->get_data_as_json( $gallery_images );
510 +
511 + $html = sprintf(
512 + '<div class="%s" data-gallery-options="%s" data-gallery-images="%s" data-atts="%s">',
513 + esc_attr( $class ),
514 + $data_gallery_options,
515 + $data_gallery_images,
516 + $data_atts
517 + );
518 +
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 + }
532 + }
533 +
534 + //The Gallery Container is where the images in the right layout will be rendered.
535 + $html .= '<div class="mgl-gallery-container"></div>';
536 +
537 + // Add skeleton loading placeholder to prevent layout shift
538 + $skeleton_loading = $this->get_option( 'skeleton_loading', true );
539 + if ( $skeleton_loading ) {
540 + $image_count = count( $gallery_images );
541 + $html .= $this->skeleton_handler->get_skeleton_html( $layout, $gallery_options, $image_count );
542 + }
543 +
544 + // Use the DOM to generate the images (so that lightboxes can hook into them, and for better SEO)
545 + // If there are no images, the JS will look for the img_html and build the gallery from there.
546 + // TODO: We should check why it's not working with the carousel (for map, it's normal).
547 + if ( $layout !== 'map' && $layout !== 'carousel' && $this->get_option( 'rendering_mode', 'dom' ) === 'dom' ) {
548 + $html .= '<div class="mgl-gallery-images">';
549 +
550 + foreach ( $gallery_images as $image ) {
551 + if ( !empty( $image['link_href'] ) ) {
552 + // If there is a link, we will get the alt from the image id so we have a proper aria-label
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;
556 +
557 + $custom_link_classes = apply_filters( 'mgl_custom_link_classes', '', $image );
558 + $html .= '<a class="' . $custom_link_classes . '" href="' . $image['link_href'] . '" target="' . $image['link_target'] . '" rel="' . $image['link_rel'] .
559 + '" aria-label="' . $aria . '">';
560 + $html .= $image['img_html'];
561 + $html .= '</a>';
562 + }
563 + else {
564 + $html .= $image['img_html'];
565 + }
566 + }
567 + $html .= '</div>';
568 + }
569 +
570 + $html .= '</div>';
571 +
572 + $this->gallery_process = false;
573 +
574 + return $html;
575 + }
576 +
577 + public function get_gallery_options(string $image_ids, array $atts, bool $infinite, bool $is_preview, string $layout) {
578 + $image_ids = explode(',', $image_ids);
579 + $wp_upload_dir = wp_upload_dir();
580 + $options = $this->get_all_options();
581 + $id = uniqid();
582 + $size = isset( $atts['size'] ) ? $atts['size'] : 'large';
583 + $size = apply_filters( 'mgl_media_size', $size );
584 + $custom_class = isset( $atts['custom-class'] ) ? $atts['custom-class'] : null;
585 + $link = isset( $atts['link'] ) ? $atts['link'] : ( $options['link'] ?? null );
586 + $updir = trailingslashit( $wp_upload_dir['baseurl'] );
587 + $captions = isset( $atts['captions'] ) ? $atts['captions'] : ( $options['captions'] ?? 'none' );
588 + $animation = null;
589 + if ( isset( $atts['animation'] ) && $atts['animation'] != 'default' ) {
590 + $animation = $atts['animation'];
591 + } else {
592 + $animation = $options['animation'] ?? null;
593 + }
594 + $class_id = 'mgl-gallery-' . $id;
595 + $layouts = [];
596 +
597 + // Justified
598 + $justified_row_height = $options['justified_row_height'];
599 + $justified_gutter = $options['justified_gutter'];
600 + if ( $layout === 'justified' ) {
601 + $justified_row_height = $atts['row-height'] ?? $options['justified_row_height'];
602 + $justified_gutter = $atts['gutter'] ?? $options['justified_gutter'];
603 + }
604 + // Masonry
605 + $masonry_gutter = $options['masonry_gutter'];
606 + $masonry_columns = $options['masonry_columns'];
607 + if ( $layout === 'masonry' ) {
608 + $masonry_gutter = $atts['gutter'] ?? $options['masonry_gutter'];
609 + $masonry_columns = $atts['columns'] ?? $options['masonry_columns'];
610 + }
611 + // Square
612 + $square_gutter = $options['square_gutter'];
613 + $square_columns = $options['square_columns'];
614 + if ( $layout === 'square' ) {
615 + $square_gutter = $atts['gutter'] ?? $options['square_gutter'];
616 + $square_columns = $atts['columns'] ?? $options['square_columns'];
617 + }
618 + // Cascade
619 + $cascade_gutter = $options['cascade_gutter'];
620 + if ( $layout === 'cascade' ) {
621 + $layouts = [ 'o', 'i', 'ii' ];
622 + $cascade_gutter = $atts['gutter'] ?? $options['cascade_gutter'];
623 + }
624 + // Tiles
625 + $tiles_gutter = $options['tiles_gutter'];
626 + $tiles_gutter_tablet = $options['tiles_gutter_tablet'];
627 + $tiles_gutter_mobile = $options['tiles_gutter_mobile'];
628 + $tiles_density = $options['tiles_density'];
629 + $tiles_density_tablet = $options['tiles_density_tablet'];
630 + $tiles_density_mobile = $options['tiles_density_mobile'];
631 + if ( $layout === 'tiles' ) {
632 + $tiles_gutter = $atts['gutter'] ?? $options['tiles_gutter'];
633 + $tiles_gutter_tablet = $atts['gutter'] ?? $options['tiles_gutter_tablet'];
634 + $tiles_gutter_mobile = $atts['gutter'] ?? $options['tiles_gutter_mobile'];
635 + $tiles_density = $atts['density'] ?? $options['tiles_density'];
636 + $tiles_density_tablet = $atts['density'] ?? $options['tiles_density_tablet'];
637 + $tiles_density_mobile = $atts['density'] ?? $options['tiles_density_mobile'];
638 + }
639 + // Horizontal
640 + $horizontal_gutter = $options['horizontal_gutter'];
641 + $horizontal_image_height = $options['horizontal_image_height'];
642 + $horizontal_hide_scrollbar = $options['horizontal_hide_scrollbar'];
643 + if ( $layout === 'horizontal' ) {
644 + $horizontal_gutter = $atts['gutter'] ?? $options['horizontal_gutter'];
645 + $horizontal_image_height = $atts['image_height'] ?? $options['horizontal_image_height'];
646 + $horizontal_hide_scrollbar = $atts['hide_scrollbar'] ?? $options['horizontal_hide_scrollbar'];
647 + }
648 + // Carousel
649 + $carousel_gutter = $options['carousel_gutter'];
650 + $carousel_arrow_nav_enabled = $options['carousel_arrow_nav_enabled'];
651 + $carousel_dot_nav_enabled = $options['carousel_dot_nav_enabled'];
652 + $carousel_image_height = $options['carousel_image_height'];
653 + $carousel_keep_aspect_ratio = $options['carousel_aspect_ratio'] ?? false;
654 + if ( $layout === 'carousel' ) {
655 + $carousel_gutter = $atts['gutter'] ?? $options['carousel_gutter'];
656 + $carousel_arrow_nav_enabled = $atts['arrow_nav_enabled'] ?? $options['carousel_arrow_nav_enabled'];
657 + $carousel_dot_nav_enabled = $atts['dot_nav_enabled'] ?? $options['carousel_dot_nav_enabled'];
658 + $carousel_image_height = $atts['image_height'] ?? $options['carousel_image_height'];
659 + $carousel_keep_aspect_ratio = array_key_exists( 'keep-aspect-ratio', $atts ) ? $atts['keep-aspect-ratio'] : 1;
660 + }
661 + // Map
662 + $map_gutter = $options['map_gutter'];
663 + $map_height = $options['map_height'];
664 + if ( $layout === 'map' ) {
665 + $map_gutter = $atts['gutter'] ?? $options['map_gutter'];
666 + $map_height = $atts['map_height'] ?? $options['map_height'];
667 + }
668 +
669 + return compact(
670 + 'image_ids',
671 + 'id',
672 + 'size',
673 + 'infinite',
674 + 'custom_class',
675 + 'link',
676 + 'is_preview',
677 + 'updir',
678 + 'captions',
679 + 'animation',
680 + 'layout',
681 + 'justified_row_height',
682 + 'justified_gutter',
683 + 'masonry_gutter',
684 + 'masonry_columns',
685 + 'square_gutter',
686 + 'square_columns',
687 + 'cascade_gutter',
688 + 'class_id',
689 + 'layouts',
690 + 'tiles_gutter',
691 + 'tiles_gutter_tablet',
692 + 'tiles_gutter_mobile',
693 + 'tiles_density',
694 + 'tiles_density_tablet',
695 + 'tiles_density_mobile',
696 + 'horizontal_gutter',
697 + 'horizontal_image_height',
698 + 'horizontal_hide_scrollbar',
699 + 'carousel_gutter',
700 + 'carousel_arrow_nav_enabled',
701 + 'carousel_dot_nav_enabled',
702 + 'carousel_image_height',
703 + 'carousel_keep_aspect_ratio',
704 + 'map_gutter',
705 + 'map_height',
706 + );
707 + }
708 +
709 + // #region Options
710 +
711 + static function get_plugin_option_name() {
712 + return self::$plugin_option_name;
713 + }
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 +
733 + static function get_plugin_option( $option_name, $default = null ) {
734 + $options = get_option( self::$plugin_option_name, null );
735 + if ( !empty( $options ) && array_key_exists( $option_name, $options ) ) {
736 + return $options[$option_name];
737 + }
738 + return $default;
739 + }
740 +
741 + function get_option( $option_name, $default = null ) {
742 + $options = $this->get_all_options();
743 + if ( array_key_exists( $option_name, $options ) ) {
744 + return $options[$option_name];
745 + }
746 + return $default;
747 + }
748 +
749 + function reset_options() {
750 + delete_option( 'mgl_db_version');
751 + delete_option( $this->option_name );
752 + }
753 +
754 + function list_options() {
755 + return array(
756 + 'layout' => 'tiles',
757 + 'ç' => 'none',
758 + 'link' => null,
759 + 'caption_source' => 'caption',
760 + 'captions_alignment' => 'center',
761 + 'captions_background' => 'fade-black',
762 + 'animation' => false,
763 + 'image_size' => 'srcset',
764 + 'truncate_on_listing' => true,
765 + 'truncate_count' => 4,
766 + 'use_attachments_on_empty' => false,
767 + 'debug_logs' => false,
768 +
769 + 'rendering_mode' => 'dom', // Can be 'dom' or 'js'
770 + 'tiles_gutter' => 10,
771 + 'tiles_gutter_tablet' => 10,
772 + 'tiles_gutter_mobile' => 10,
773 + 'tiles_density' => 'high',
774 + 'tiles_density_tablet' => 'medium',
775 + 'tiles_density_mobile' => 'low',
776 +
777 + 'justified_density' => 'low',
778 + 'justified_density_tablet' => 'medium',
779 + 'justified_density_mobile' => 'low',
780 +
781 + 'masonry_gutter' => 5,
782 + 'masonry_columns' => 3,
783 + 'masonry_left_to_right' => false,
784 + 'justified_gutter' => 5,
785 + 'justified_row_height' => 200,
786 + 'square_gutter' => 5,
787 + 'square_columns' => 5,
788 + 'cascade_gutter' => 10,
789 + 'horizontal_gutter' => 10,
790 + 'horizontal_image_height' => 500,
791 + 'horizontal_hide_scrollbar' => false,
792 + 'carousel_gutter' => 5,
793 + 'carousel_image_height' => 500,
794 + 'carousel_arrow_nav_enabled' => true,
795 + 'carousel_dot_nav_enabled' => true,
796 + 'carousel_infinite' => false,
797 + 'map_engine' => 'googlemaps',
798 + 'map_height' => 500,
799 + 'map_zoom' => 10,
800 + 'map_gutter' => 10,
801 + 'googlemaps_token' => '',
802 + 'googlemaps_style' => '[]',
803 + 'mapbox_token' => '',
804 + 'mapbox_style' => '{"username":"", "style_id":""}',
805 + 'maptiler_token' => '',
806 +
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,
816 +
817 + //PRO OPTIONS
818 + 'infinite' => false,
819 + 'infinite_buffer' => 0,
820 + 'right_click' => false,
821 + 'gallery_shortcode_override_disabled' => false,
822 + 'skeleton_loading' => false,
823 + );
824 + }
825 +
826 + function list_pro_options() {
827 + return array(
828 + 'infinite' => false,
829 + 'infinite_buffer' => 0,
830 + 'right_click' => false,
831 + 'gallery_shortcode_override_disabled' => false,
832 + 'skeleton_loading' => false,
833 + );
834 + }
835 +
836 + function get_all_options() {
837 + $options = get_option( $this->option_name, null );
838 + $options = $this->check_options( $options );
839 + return $options;
840 + }
841 +
842 + // Upgrade from the old way of storing options to the new way.
843 +
844 + function check_options( $options = [] ) {
845 + $plugin_options = $this->list_options();
846 + $pro_options = $this->list_pro_options();
847 +
848 + $options = empty( $options ) ? [] : $options;
849 + $hasChanges = false;
850 +
851 + foreach ( $plugin_options as $option => $default ) {
852 + // The option already exists
853 + if ( isset( $options[$option] ) ) {
854 + continue;
855 + }
856 + // The option does not exist, so we need to add it.
857 + // Let's use the old value if any, or the default value.
858 + $options[$option] = get_option( 'mgl_' . $option, $default );
859 + delete_option( 'mgl_' . $option );
860 + $hasChanges = true;
861 + }
862 +
863 + if( !$this->pro_module ) {
864 + foreach ( $pro_options as $pro_option => $default ) {
865 + if ( $options[$pro_option] !== $default ) {
866 + $options[$pro_option] = $default;
867 + $hasChanges = true;
868 + }
869 + }
870 + }
871 +
872 + if ( $hasChanges ) {
873 + update_option( $this->option_name , $options );
874 + }
875 + return $options;
876 + }
877 +
878 + function update_options( $options ) {
879 + if ( !update_option( $this->option_name, $options, false ) ) {
880 + return false;
881 + }
882 + $options = $this->sanitize_options();
883 + return $options;
884 + }
885 +
886 +
887 + // Validate and keep the options clean and logical.
888 + function sanitize_options() {
889 + $options = $this->get_all_options();
890 + // something to do
891 + return $options;
892 + }
893 +
894 + # endregion
895 +
896 + function get_caption_from_source( $image ) {
897 + $caption_source = $this->get_option( 'caption_source', 'caption' );
898 + $caption = '';
899 +
900 + switch ( $caption_source ) {
901 + case 'title':
902 + $caption = $image->title;
903 + break;
904 + case 'caption':
905 + $caption = $image->caption;
906 + break;
907 + case 'description':
908 + $caption = $image->description;
909 + break;
910 + case 'alt':
911 + $caption = $image->alt;
912 + break;
913 + default:
914 + $caption = $image->caption;
915 + break;
916 + }
917 +
918 + return $caption;
919 + }
920 +
921 + function get_gallery_images( array $image_ids, array $atts, string $layout, string $size, array $posts_ids = []) {
922 + global $wpdb;
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 +
932 + // Escape the array of IDs for SQL
933 + $ids = array_map( 'intval', $image_ids );
934 + $ids_str = implode( ',', $ids );
935 +
936 + $query = "SELECT
937 + p.ID id,
938 + p.post_title title,
939 + p.post_content description,
940 + p.post_excerpt caption,
941 + pm.meta_value alt,
942 + pm2.meta_value meta
943 +
944 + FROM $wpdb->posts p
945 + LEFT JOIN $wpdb->postmeta pm ON pm.post_id = p.ID AND pm.meta_key = '_wp_attachment_image_alt'
946 + LEFT JOIN $wpdb->postmeta pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_wp_attachment_metadata'
947 +
948 + WHERE p.post_type = 'attachment'
949 +
950 + AND p.ID IN (" . $ids_str . ")
951 + ";
952 +
953 + $res = $wpdb->get_results( $query );
954 +
955 + $ids = explode( ',', $ids_str );
956 + $images = [];
957 + foreach ( $res as $r ) {
958 + $images[$r->id] = [
959 + 'caption' => $this->get_caption_from_source( $r ),
960 + 'meta' => unserialize( $r->meta ),
961 + ];
962 + }
963 + $cleanIds = [];
964 + foreach ( $ids as $id ) {
965 + if ( isset( $images[$id] ) )
966 + array_push( $cleanIds, $id );
967 + }
968 + $ids = apply_filters( 'mgl_sort', $cleanIds, $images, $layout, $atts );
969 +
970 + if ($layout === 'map') {
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;
975 + }
976 +
977 + $result = [];
978 + foreach ($ids as $index => $id) {
979 + $image = $images[$id];
980 +
981 + // Determine orientation if layout is 'tiles'
982 + $orientation = [];
983 + if ($layout === 'tiles') {
984 + $orientation = [
985 + 'orientation' => ($image['meta']['width'] > $image['meta']['height'] ? 'o' : 'i')
986 + ];
987 + }
988 +
989 + $default_link = $this->get_option( 'link', null );
990 + $link_attr = $this->get_link_attributes( $id, $atts['link'] ?? $default_link, $image );
991 + $no_lightbox = $link_attr['type'] === 'link';
992 +
993 + $mergedArray = [
994 + 'id' => $id,
995 + 'caption' => wp_kses(
996 + html_entity_decode(apply_filters( 'mgl_caption', $image['caption'], $id ), ENT_QUOTES),
997 + [
998 + 'strong' => [], // Bold
999 + 'b' => [], // Bold alternative
1000 + 'em' => [], // Italic
1001 + 'i' => [] // Italic alternative
1002 + ]
1003 + ),
1004 + 'img_html' => apply_filters( 'mgl_gallery_written',
1005 + $this->get_img_html( $id, $size, $layout, $atts, $image, $no_lightbox ),
1006 + $layout
1007 + ),
1008 + 'link_href' => $link_attr['href'] ?? null,
1009 + 'link_target' => $link_attr['target'] ?? null,
1010 + 'link_rel' => $link_attr['rel'] ?? null,
1011 + 'attributes' => $this->get_attributes( $id, $image, $layout ),
1012 + ];
1013 +
1014 + if( !empty( $posts_ids ) && isset( $atts['hero'] ) && $atts['hero'] ) {
1015 +
1016 + $post_id = $posts_ids[$index];
1017 + $post = get_post( $post_id );
1018 +
1019 + $mergedArray['featured_post_id'] = $post_id;
1020 + $mergedArray['featured_post_title'] = $post->post_title;
1021 + $mergedArray['featured_post_excerpt'] = $post->post_excerpt;
1022 + $mergedArray['featured_post_url'] = get_permalink( $post_id );
1023 +
1024 + }
1025 +
1026 + $result[] = array_merge( $image, $mergedArray, $orientation );
1027 + }
1028 +
1029 + $this->gallery_process = $previous_gallery_process;
1030 + $this->gallery_layout = $previous_gallery_layout;
1031 +
132 1032 return $result;
133 1033 }
1034 +
1035 + private function get_image_class( $id, $layout, $noLightbox ) {
1036 + $base_class = 'wp-image-' . $id;
1037 +
1038 + if( $layout === 'carousel' ) {
1039 + $base_class .= ' skip-lazy';
1040 + }
1041 +
1042 + if ( $noLightbox ) {
1043 + $base_class .= ' no-lightbox';
1044 + }
1045 + return $base_class;
1046 + }
1047 +
1048 + private function get_img_html( $id, $size, $layout, $atts, $data, $noLightbox ) {
1049 +
1050 + //check if the media is a video
1051 + $media_type = get_post_mime_type( $id );
1052 + if ( strpos( $media_type, 'video' ) !== false ) {
1053 + $video = wp_get_attachment_url( $id );
1054 + $video = apply_filters( 'mgl_video', $video, $id, $data );
1055 + if ( !empty( $video ) ) {
1056 + return '<video class="wp-video-'. $id .'" controls="controls" onclick="() => this.play();"><source src="' . $video . '" type="' . $media_type . '"></video>';
1057 + }
1058 + }
1059 +
1060 + $image_size = $this->get_option( 'image_size', 'srcset' );
1061 + $img_html = null;
1062 + if ( empty( $image_size ) || $image_size === 'srcset' ) {
1063 + $img_html = wp_get_attachment_image( $id, $size, false, [
1064 + 'class' => $this->get_image_class( $id, $layout, $noLightbox ),
1065 + 'draggable' => $layout === 'carousel' ? 'false' : null,
1066 + ]);
1067 + }
1068 + else {
1069 + $info = wp_get_attachment_image_src( $id, $image_size );
1070 + $alt_text = get_post_meta( $id, '_wp_attachment_image_alt', true );
1071 +
1072 + $img_html = '<img loading="lazy" src="' . $info[0] . '" class="' . $this->get_image_class( $id, $layout, $noLightbox ) . '" alt="' . esc_attr( $alt_text ) . '" />';
1073 + }
1074 +
1075 + if ( $layout === 'masonry' ) {
1076 + $masonry_column = $this->get_option( 'masonry_column', 3 );
1077 + $columns = ( isset( $atts['columns'] ) ? $atts['columns'] : $masonry_column ) + 1;
1078 + $img_html = str_replace( '100vw', 100 / $columns . 'vw', $img_html );
1079 + }
1080 + else if ( $layout === 'square' ) {
1081 + $square_column = $this->get_option( 'square_columns', 5 );
1082 + $columns = ( isset( $atts['columns'] ) ? $atts['columns'] : $square_column ) + 1;
1083 + $img_html = str_replace( '100vw', 100 / $columns . 'vw', $img_html );
1084 + }
1085 + else if ( $layout === 'cascade' ) {
1086 + $img_html = str_replace( '100vw', 100 / 3 . 'vw', $img_html );
1087 + }
1088 +
1089 + return wp_kses( $img_html, [
1090 + 'img' => [
1091 + 'src' => true,
1092 + 'srcset' => true,
1093 + 'loading' => true,
1094 + 'tabindex' => true,
1095 + 'sizes' => true,
1096 + 'class' => true,
1097 + 'id' => true,
1098 + 'width' => true,
1099 + 'height' => true,
1100 + 'alt' => true,
1101 + 'align' => true,
1102 + 'draggable' => true,
1103 + ]
1104 + ]
1105 + );
1106 + }
1107 +
1108 + private function get_link_attributes( $id, $link, $data ) {
1109 + $link_url = null;
1110 + $type = 'media';
1111 + $rel = null;
1112 + $target = '_self';
1113 +
1114 + if ( $link === 'attachment' ) {
1115 + $link_url = get_permalink( (int)$id );
1116 + }
1117 + else if ( $link === 'media' || $link === 'file' ) {
1118 + $wpUploadDir = wp_upload_dir();
1119 + $updir = trailingslashit( $wpUploadDir['baseurl'] );
1120 + if ( isset( $data['meta']['file'] ) ) {
1121 + $link_url = $updir . $data['meta']['file'];
1122 + } else {
1123 + $link_url = get_permalink( (int)$id );
1124 + }
1125 + }
1126 + else if ( $link === null ){
1127 + $link_url = get_post_meta( $id, '_gallery_link_url', true );
1128 + if ( !empty( $link_url ) ) {
1129 + $type = 'link';
1130 + $target = get_post_meta( $id, '_gallery_link_target', true );
1131 + }
1132 + }
1133 +
1134 + $link_attr = [
1135 + 'href' => !empty( $link_url ) ? esc_url( $link_url ) : null,
1136 + 'target' => $target,
1137 + 'type' => $type,
1138 + 'rel' => $rel,
1139 + ];
1140 +
1141 +
1142 + return apply_filters( 'mgl_link_attributes', $link_attr, (int)$id, $data );
1143 + }
1144 +
1145 + private function get_attributes( $id, $data, $layout ) {
1146 + $attributes = '';
1147 + if ( $layout === 'raw' ) {
1148 + if ( isset( $data['meta'] ) && isset( $data['meta']['width'] ) && isset( $data['meta']['height'] ) ) {
1149 + $attributes = 'data-mgl-id=' . $id . ' data-mgl-width=' . $data['meta']['width'] . ' data-mgl-height=' . $data['meta']['height'];
1150 + }
1151 + }
1152 + elseif ( $layout === 'tiles' ) {
1153 + if ( isset( $data['meta'] ) && isset( $data['meta']['width'] ) && isset( $data['meta']['height'] ) ) {
1154 + $attributes = 'data-mgl-id=' . $id . ' data-mgl-width=' . $data['meta']['width'] . ' data-mgl-height=' . $data['meta']['height'];
1155 + }
1156 + }
1157 + $attributes = apply_filters( 'mgl_attributes', $attributes, $id, $data );
1158 + if ( $attributes === '' ) {
1159 + return [];
1160 + }
1161 +
1162 + $attribute_list = explode( ' ', $attributes );
1163 + $attributes = [];
1164 + foreach ( $attribute_list as $attribute ) {
1165 + list( $key, $value ) = explode( '=', $attribute );
1166 + $attributes[$key] = $value;
1167 + }
1168 + return $attributes;
1169 + }
1170 +
1171 + private function get_map_images( $ids, $images, $atts = [] ) {
1172 + $map_images = array_map( function ( $id ) use ( $images, $atts ) {
1173 +
1174 + $image = $images[$id];
1175 + $default_link = $this->get_option( 'link', null );
1176 + $link_attr = $this->get_link_attributes( $id, $atts['link'] ?? $default_link, $image );
1177 +
1178 + $geo_coordinates = MeowPro_MGL_Exif::get_gps_data( $id, $image['meta'] );
1179 + if ( empty( $geo_coordinates ) ) {
1180 + return null;
1181 + }
1182 + $callback = function ( &$value, $key ) use ( $id ) {
1183 + $imgsrc = wp_get_attachment_image_src( $id, $key );
1184 + $value = $imgsrc[0];
1185 + };
1186 + array_walk( $image['meta']['sizes'], $callback );
1187 + return array_merge(
1188 + $image,
1189 + [
1190 + 'id' => $id,
1191 + 'file' => $image['meta']['file'],
1192 + 'file_full' => wp_get_attachment_url( $id ),
1193 + 'file_srcset' => wp_get_attachment_image_srcset( $id, 'full' ),
1194 + 'file_sizes' => wp_get_attachment_image_sizes( $id, 'full' ),
1195 + 'dimension' => [
1196 + 'width' => $image['meta']['width'],
1197 + 'height' => $image['meta']['height'],
1198 + ],
1199 + 'sizes' => $image['meta']['sizes'],
1200 + 'data' => [
1201 + 'caption' => $image['meta']['image_meta']['caption'],
1202 + 'gps' => $geo_coordinates,
1203 + ],
1204 + 'link' => $link_attr,
1205 + ]
1206 + );
1207 + }, $ids );
1208 + return array_values( array_filter( $map_images ) );
1209 + }
1210 +
1211 + public function get_mgl_root_class( $atts, $classes = ['mgl-root'] ) {
1212 + $classes[] = isset( $atts['align'] ) ? 'align' . $atts['align'] : '';
1213 + return trim( implode( ' ', $classes ) );
1214 + }
1215 +
1216 + public function get_data_as_json( $data ) {
1217 + return esc_attr( htmlspecialchars( wp_json_encode( $data ), ENT_QUOTES, 'UTF-8' ) );
1218 + }
1219 +
1220 + public function generate_uniqid($length = 13) {
1221 + // Use WordPress function
1222 + if ( function_exists( 'wp_unique_id' ) ) {
1223 + $prefix = uniqid();
1224 + return wp_unique_id( $prefix );
1225 + }
1226 + // Fall back
1227 + else {
1228 + if ( function_exists( "random_bytes" ) ) {
1229 + $bytes = random_bytes( ceil( $length / 2 ) );
1230 + }
1231 + elseif ( function_exists( "openssl_random_pseudo_bytes" ) ) {
1232 + $bytes = openssl_random_pseudo_bytes(ceil($length / 2));
1233 + }
1234 + else {
1235 + throw new Exception( "No cryptographically secure random function available." );
1236 + }
1237 + return substr( bin2hex( $bytes ), 0, $length );
1238 + }
1239 + }
1240 +
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 +
1294 + public function get_gallery_by_id( $id ) {
1295 + global $wpdb;
1296 + $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1297 + $gallery = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $shortcodes_table WHERE id = %s", $id ), ARRAY_A );
1298 +
1299 + if ( !$gallery ) {
1300 + throw new Exception( __( 'Gallery not found.', MGL_DOMAIN ));
1301 + }
1302 + $gallery['medias'] = self::normalize_medias( maybe_unserialize( $gallery['medias'] ) );
1303 + $gallery['posts'] = $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null;
1304 + $gallery['tags'] = $gallery['tags'] ? unserialize( $gallery['tags'] ) : null;
1305 +
1306 + return $gallery;
1307 + }
1308 +
1309 + public function get_galleries_by_ids( $ids ){
1310 + global $wpdb;
1311 + $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1312 + $galleries = [];
1313 + $ids_str = "'" . implode( "','", array_map( 'esc_sql', $ids )) . "'";
1314 + $query = "SELECT * FROM $shortcodes_table WHERE id IN ( $ids_str )";
1315 + $results = $wpdb->get_results( $query, ARRAY_A );
1316 + foreach ( $results as $gallery ) {
1317 + $galleries[$gallery['id']] = [
1318 + 'name' => $gallery['name'],
1319 + 'description' => $gallery['description'],
1320 + 'layout' => $gallery['layout'],
1321 + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ),
1322 + 'lead_image_id' => $gallery['lead_image_id'],
1323 + 'order_by' => $gallery['order_by'],
1324 + 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1325 + 'dynamic_source' => $gallery['dynamic_source'],
1326 + 'hero' => ( bool )$gallery['is_hero_mode'],
1327 + 'posts' => $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null,
1328 + 'latest_posts' => $gallery['latest_posts'],
1329 + 'tags' => $gallery['tags'] ? unserialize( $gallery['tags'] ) : null,
1330 + 'dynamic_source' => $gallery['dynamic_source'],
1331 + 'rml' => $gallery['rml'] ?? null,
1332 + 'updated' => strtotime( $gallery['updated_at'] )
1333 + ];
1334 + }
1335 + return $galleries;
1336 + }
1337 +
1338 + public function get_galleries( $offset = 0, $limit = 10, $order = 'DESC', $sort = null, $page = 1, $search = '' ) {
1339 + global $wpdb;
1340 + $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1341 + Meow_MGL_Migrations::check_db();
1342 +
1343 + // Get total count
1344 + $total = 0;
1345 + if ( empty( $search ) ) {
1346 + $total = $wpdb->get_var( "SELECT COUNT( * ) FROM $shortcodes_table" );
1347 + } else {
1348 + $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $shortcodes_table WHERE name LIKE %s", '%' . $wpdb->esc_like( $search ) . '%' ) );
1349 + }
1350 +
1351 + // Calculate offset based on page if provided
1352 + if ($page > 1 && $offset === 0) {
1353 + $offset = ($page - 1) * $limit;
1354 + }
1355 +
1356 + // Get shortcodes with pagination and sorting
1357 + $sort_accessor_to_column = [
1358 + 'info' => 'name',
1359 + 'updated' => 'updated_at',
1360 + 'rank' => 'pref_rank',
1361 + ];
1362 +
1363 + $sort = $sort_accessor_to_column[$sort] ?? 'pref_rank';
1364 + $order = in_array( strtoupper( $order ), ['ASC', 'DESC'] ) ? strtoupper( $order ) : 'DESC';
1365 +
1366 + // Sort by rank DESC first, then by updated_at for equal ranks
1367 + $query = $wpdb->prepare(
1368 + "SELECT * FROM $shortcodes_table WHERE name LIKE %s ORDER BY $sort $order, updated_at DESC LIMIT %d, %d",
1369 + '%' . $wpdb->esc_like( $search ) . '%', $offset, $limit
1370 + );
1371 +
1372 + $results = $wpdb->get_results( $query, ARRAY_A );
1373 + $shortcodes = [];
1374 +
1375 + foreach ( $results as $gallery ) {
1376 + // Transform database format to match expected format
1377 + $shortcodes[$gallery['id']] = [
1378 + 'name' => $gallery['name'],
1379 + 'description' => $gallery['description'],
1380 + 'layout' => $gallery['layout'],
1381 + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
1382 + 'lead_image_id' => $gallery['lead_image_id'],
1383 + 'order_by' => $gallery['order_by'],
1384 + 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1385 + 'hero' => ( bool )$gallery['is_hero_mode'],
1386 + 'posts' => $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null,
1387 + 'latest_posts' => $gallery['latest_posts'],
1388 + 'tags' => $gallery['tags'] ? unserialize( $gallery['tags'] ) : null,
1389 + 'dynamic_source' => $gallery['dynamic_source'],
1390 + 'rml' => $gallery['rml'] ?? null,
1391 + 'rank' => intval( $gallery['pref_rank'] ?? 0 ),
1392 + 'updated' => strtotime( $gallery['updated_at'] )
1393 + ];
1394 + }
1395 +
1396 + return [
1397 + 'total' => $total,
1398 + 'galleries' => $shortcodes
1399 + ];
1400 + }
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 +
1433 + public function get_collection_by_id( $id ) {
1434 + global $wpdb;
1435 + $collections_table = $wpdb->prefix . 'mgl_collections';
1436 + $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1437 +
1438 + // Get the collection
1439 + $collection = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $collections_table WHERE id = %d", $id ), ARRAY_A );
1440 + if ( !$collection ) {
1441 + throw new Exception( __( 'Collection not found.', MGL_DOMAIN ));
1442 + }
1443 + $collection['galleries_ids'] = unserialize( $collection['galleries_ids'] );
1444 +
1445 + // Get the associated galleries
1446 + $galleries = [];
1447 + if ( !empty( $collection['galleries_ids'] )) {
1448 + $galleries_ids_str = "'" . implode( "','", array_map( 'esc_sql', $collection['galleries_ids'] )) . "'";
1449 + $galleries_query = "SELECT * FROM $shortcodes_table WHERE id IN ( $galleries_ids_str )";
1450 + $galleries_data = $wpdb->get_results( $galleries_query, ARRAY_A );
1451 +
1452 + foreach ( $galleries_data as $gallery ) {
1453 + // Transform database format to match expected format
1454 + $galleries[] = [
1455 + 'id' => $gallery['id'],
1456 + 'name' => $gallery['name'],
1457 + 'description' => $gallery['description'],
1458 + 'layout' => $gallery['layout'],
1459 + 'medias' => self::normalize_medias( maybe_unserialize( $gallery['medias'] ) ),
1460 + 'lead_image_id' => $gallery['lead_image_id'],
1461 + 'order_by' => $gallery['order_by'],
1462 + 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1463 + 'hero' => ( bool )$gallery['is_hero_mode'],
1464 + 'posts' => $gallery['posts'] ? unserialize( $gallery['posts'] ) : null,
1465 + 'latest_posts' => $gallery['latest_posts'],
1466 + 'updated' => strtotime( $gallery['updated_at'] )
1467 + ];
1468 + }
1469 +
1470 + // Format collection data
1471 + $collection['galleries'] = $galleries;
1472 + }
1473 +
1474 + return $collection;
1475 + }
1476 +
1477 + public function get_collections( $offset = 0, $limit = 10, $order = 'DESC', $page = 1, $search = '' ) {
1478 + global $wpdb;
1479 + $collections_table = $wpdb->prefix . 'mgl_collections';
1480 + $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1481 + Meow_MGL_Migrations::check_db();
1482 +
1483 + // Get total count
1484 + if( empty( $search ) ) {
1485 + $total = $wpdb->get_var( "SELECT COUNT( * ) FROM $collections_table" );
1486 + } else {
1487 + $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $collections_table WHERE name LIKE %s", '%' . $wpdb->esc_like( $search ) . '%' ) );
1488 + }
1489 + // Calculate offset based on page if provided
1490 + if ($page > 1 && $offset === 0) {
1491 + $offset = ($page - 1) * $limit;
1492 + }
1493 +
1494 + // Get collections with pagination and sorting
1495 + $query = $wpdb->prepare(
1496 + "SELECT * FROM $collections_table WHERE name LIKE %s ORDER BY updated_at $order LIMIT %d, %d",
1497 + '%' . $wpdb->esc_like( $search ) . '%', $offset, $limit
1498 + );
1499 +
1500 + $collections = $wpdb->get_results( $query, ARRAY_A );
1501 + $result = [];
1502 +
1503 + foreach ( $collections as $collection ) {
1504 + $collection_id = $collection['id'];
1505 + $galleries_ids = unserialize( $collection['galleries_ids'] );
1506 +
1507 + // Get the associated galleries
1508 + $galleries = [];
1509 + if ( !empty( $galleries_ids )) {
1510 + $galleries_ids_str = "'" . implode( "','", array_map( 'esc_sql', $galleries_ids )) . "'";
1511 + $galleries_query = "SELECT * FROM $shortcodes_table WHERE id IN ( $galleries_ids_str )";
1512 + $galleries_data = $wpdb->get_results( $galleries_query, ARRAY_A );
1513 +
1514 + foreach ( $galleries_data as $gallery ) {
1515 + // Transform database format to match expected format
1516 + $gallery_item = [
1517 + 'id' => $gallery['id'],
1518 + 'name' => $gallery['name'],
1519 + 'description' => $gallery['description'],
1520 + 'layout' => $gallery['layout'],
1521 + 'medias' => self::hydrate_medias( maybe_unserialize( $gallery['medias'] ) ),
1522 + 'lead_image_id' => $gallery['lead_image_id'],
1523 + 'order_by' => $gallery['order_by'],
1524 + 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1525 + 'hero' => ( bool )$gallery['is_hero_mode'],
1526 + 'posts' => $gallery['posts'] ? unserialize( $gallery['posts'] ) : null,
1527 + 'latest_posts' => $gallery['latest_posts'],
1528 + 'tags' => $gallery['tags'] ? unserialize( $gallery['tags'] ) : null,
1529 + 'dynamic_source' => $gallery['dynamic_source'],
1530 + 'updated' => strtotime( $gallery['updated_at'] )
1531 + ];
1532 + $galleries[] = $gallery_item;
1533 + }
1534 + }
1535 +
1536 + // Format collection data
1537 + $result[$collection_id] = [
1538 + 'name' => $collection['name'],
1539 + 'description' => $collection['description'],
1540 + 'layout' => $collection['layout'],
1541 + 'galleries_ids' => $galleries_ids,
1542 + 'galleries' => $galleries,
1543 + 'updated' => strtotime( $collection['updated_at'] )
1544 + ];
1545 + }
1546 +
1547 + return [
1548 + 'total' => $total,
1549 + 'collections' => $result
1550 + ];
1551 + }
1552 +
134 1553 }
135 1554
136 1555 ?>