PluginProbe
Meow Gallery / trunk
Meow Gallery vtrunk
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 4.3.1 All 156 releases
meow-gallery / classes / core.php

core.php in Meow Gallery trunk, at classes/core.php

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