PluginProbe
Meow Gallery / 5.4.9
Meow Gallery v5.4.9
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
meow-gallery / classes / core.php

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

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