PluginProbe
Meow Gallery / 5.5.0
Meow Gallery v5.5.0
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.5.0, at classes/core.php

1,353 lines 44.0 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, '-' ) != false ) {
341 $left = explode( '-', $orderby )[0];
342 $right = explode( '-', $orderby )[1];
343
344 $orderby = $left;
345 $order = $right;
346 }
347
348
349 $image_ids = explode( ',', $image_ids );
350 $image_ids = Meow_MGL_OrderBy::run( $image_ids, $orderby, $order );
351 $image_ids = implode( ',', $image_ids );
352 }
353
354 // Layout
355 if ( isset( $atts['layout'] ) && $atts['layout'] != 'default' ) {
356 $layout = $atts['layout'];
357 }
358 else if ( isset( $atts['mgl-layout'] ) && $atts['mgl-layout'] != 'default' ) {
359 $layout = $atts['mgl-layout'];
360 $atts['layout'] = $layout;
361 } else {
362 $layout = $this->get_option( 'layout', 'tiles' );
363 $atts['layout'] = $layout;
364 }
365
366
367 if ( $layout === 'none' || $layout === '' ) {
368 $layout = $this->get_option( 'layout', 'tiles' );
369 }
370
371
372 $layoutClass = 'Meow_MGL_Builders_' . ucfirst( $layout );
373 if ( !class_exists( $layoutClass ) ) {
374 error_log( "Meow Gallery: Class $layoutClass does not exist." );
375 return "<p class='meow-error'><b>Meow Gallery:</b> The layout $layout is not available in this version.</p>";
376 }
377
378 // Captions
379 if ( isset( $atts['captions'] ) && ( $atts['captions'] === false || $atts['captions'] === 'false' ) ) {
380 // This is to avoid issues linked to the old block editor for the Meow Gallery
381 $atts['captions'] = 'none';
382 }
383
384 // apply filter 'mgl_sort_ahead' to sort the images before anything else
385 if ( !empty( $image_ids ) ){
386 $image_ids = implode( ',', apply_filters( 'mgl_sort_ahead', explode( ',', $image_ids ), $layout, $atts ) );
387 }
388
389 //DEBUG: Display $atts
390 //error_log( print_r( $atts, 1 ) );
391
392 // Start the process of building the gallery
393 $this->gallery_process = true;
394 $this->gallery_layout = $layout;
395
396 // This should be probably removed.
397 // wp_enqueue_style( 'mgl-css' );
398
399 $infinite = $this->get_option( 'infinite', false ) && class_exists( 'MeowPro_MGL_Core' );
400
401 // $gen = new $layoutClass( $atts, !$isPreview && $infinite, $isPreview );
402 // $result = $gen->build( $image_ids );
403
404 do_action( 'mgl_' . $layout . '_gallery_created', $layout );
405 //$result = apply_filters( 'post_gallery', $result, $atts, null );
406
407 $this->rewrittenMwlData = apply_filters('mgl_force_rewrite_mwl_data', explode( ',', $image_ids ) );
408 do_action( 'mgl_gallery_created', $atts, explode( ',', $image_ids ), $layout );
409
410 $gallery_options = $this->get_gallery_options( $image_ids, $atts, $infinite, $isPreview, $layout );
411
412 // If infinite scroll option was enabled, get the images up to 12 at first.
413 $loading_image_ids = explode(',', $image_ids);
414 $loading_image_ids = apply_filters( 'mgl_sort', $loading_image_ids, [], $layout, $atts );
415
416 // Only add the carousel to the infinite layouts if the option is enabled
417 if( $infinite && $this->get_option( 'carousel_infinite', false ) ) {
418 $this->infinite_layouts[] = 'carousel';
419 }
420
421 if (!$isPreview && $infinite && in_array( $layout, $this->infinite_layouts ) ) {
422 $loading_image_ids = array_slice( $loading_image_ids, 0, 12 );
423 }
424
425 $gallery_images = $this->get_gallery_images( $loading_image_ids, $atts, $layout, $gallery_options['size'], $posts_ids );
426
427 // Get the class and data attributes
428 $class = $this->get_mgl_root_class( $atts );
429 $data_atts = $this->get_data_as_json( $atts );
430 $data_gallery_options = $this->get_data_as_json( $gallery_options );
431 $data_gallery_images = $this->get_data_as_json( $gallery_images );
432
433 $html = sprintf(
434 '<div class="%s" data-gallery-options="%s" data-gallery-images="%s" data-atts="%s">',
435 esc_attr( $class ),
436 $data_gallery_options,
437 $data_gallery_images,
438 $data_atts
439 );
440
441 // Run at /wp-includes/formatting.php on line 6037
442 // WordPress returns early if the text is simple ASCII without emoji-like content,
443 // so we only need to check preg_split if WordPress would actually run it.
444 $needs_emoji_check = str_contains( $html, '&#x' ) ||
445 !( ( function_exists( 'mb_check_encoding' ) && mb_check_encoding( $html, 'ASCII' ) ) || ! preg_match( '/[^\x00-\x7F]/', $html ) );
446
447 if ( $needs_emoji_check ) {
448 $textarr = preg_split( '/(<.*>)/U', $html , -1, PREG_SPLIT_DELIM_CAPTURE);
449 if ( $textarr === false ) {
450 $error = preg_last_error();
451 error_log( "[MEOW GALLERY] Regex: " . preg_last_error_msg() . " (Code $error)" );
452 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.";
453 }
454 }
455
456 //The Gallery Container is where the images in the right layout will be rendered.
457 $html .= '<div class="mgl-gallery-container"></div>';
458
459 // Add skeleton loading placeholder to prevent layout shift
460 $skeleton_loading = $this->get_option( 'skeleton_loading', true );
461 if ( $skeleton_loading ) {
462 $image_count = count( $gallery_images );
463 $html .= $this->skeleton_handler->get_skeleton_html( $layout, $gallery_options, $image_count );
464 }
465
466 // Use the DOM to generate the images (so that lightboxes can hook into them, and for better SEO)
467 // If there are no images, the JS will look for the img_html and build the gallery from there.
468 // TODO: We should check why it's not working with the carousel (for map, it's normal).
469 if ( $layout !== 'map' && $layout !== 'carousel' && $this->get_option( 'rendering_mode', 'dom' ) === 'dom' ) {
470 $html .= '<div class="mgl-gallery-images">';
471
472 foreach ( $gallery_images as $image ) {
473 if ( !empty( $image['link_href'] ) ) {
474 // If there is a link, we will get the alt from the image id so we have a proper aria-label
475 $aria = get_post_meta( $image['id'], '_wp_attachment_image_alt', true );
476
477 $custom_link_classes = apply_filters( 'mgl_custom_link_classes', '', $image );
478 $html .= '<a class="' . $custom_link_classes . '" href="' . $image['link_href'] . '" target="' . $image['link_target'] . '" rel="' . $image['link_rel'] .
479 '" aria-label="' . $aria . '">';
480 $html .= $image['img_html'];
481 $html .= '</a>';
482 }
483 else {
484 $html .= $image['img_html'];
485 }
486 }
487 $html .= '</div>';
488 }
489
490 $html .= '</div>';
491
492 $this->gallery_process = false;
493
494 return $html;
495 }
496
497 public function get_gallery_options(string $image_ids, array $atts, bool $infinite, bool $is_preview, string $layout) {
498 $image_ids = explode(',', $image_ids);
499 $wp_upload_dir = wp_upload_dir();
500 $options = $this->get_all_options();
501 $id = uniqid();
502 $size = isset( $atts['size'] ) ? $atts['size'] : 'large';
503 $size = apply_filters( 'mgl_media_size', $size );
504 $custom_class = isset( $atts['custom-class'] ) ? $atts['custom-class'] : null;
505 $link = isset( $atts['link'] ) ? $atts['link'] : ( $options['link'] ?? null );
506 $updir = trailingslashit( $wp_upload_dir['baseurl'] );
507 $captions = isset( $atts['captions'] ) ? $atts['captions'] : ( $options['captions'] ?? 'none' );
508 $animation = null;
509 if ( isset( $atts['animation'] ) && $atts['animation'] != 'default' ) {
510 $animation = $atts['animation'];
511 } else {
512 $animation = $options['animation'] ?? null;
513 }
514 $class_id = 'mgl-gallery-' . $id;
515 $layouts = [];
516
517 // Justified
518 $justified_row_height = $options['justified_row_height'];
519 $justified_gutter = $options['justified_gutter'];
520 if ( $layout === 'justified' ) {
521 $justified_row_height = $atts['row-height'] ?? $options['justified_row_height'];
522 $justified_gutter = $atts['gutter'] ?? $options['justified_gutter'];
523 }
524 // Masonry
525 $masonry_gutter = $options['masonry_gutter'];
526 $masonry_columns = $options['masonry_columns'];
527 if ( $layout === 'masonry' ) {
528 $masonry_gutter = $atts['gutter'] ?? $options['masonry_gutter'];
529 $masonry_columns = $atts['columns'] ?? $options['masonry_columns'];
530 }
531 // Square
532 $square_gutter = $options['square_gutter'];
533 $square_columns = $options['square_columns'];
534 if ( $layout === 'square' ) {
535 $square_gutter = $atts['gutter'] ?? $options['square_gutter'];
536 $square_columns = $atts['columns'] ?? $options['square_columns'];
537 }
538 // Cascade
539 $cascade_gutter = $options['cascade_gutter'];
540 if ( $layout === 'cascade' ) {
541 $layouts = [ 'o', 'i', 'ii' ];
542 $cascade_gutter = $atts['gutter'] ?? $options['cascade_gutter'];
543 }
544 // Tiles
545 $tiles_gutter = $options['tiles_gutter'];
546 $tiles_gutter_tablet = $options['tiles_gutter_tablet'];
547 $tiles_gutter_mobile = $options['tiles_gutter_mobile'];
548 $tiles_density = $options['tiles_density'];
549 $tiles_density_tablet = $options['tiles_density_tablet'];
550 $tiles_density_mobile = $options['tiles_density_mobile'];
551 if ( $layout === 'tiles' ) {
552 $tiles_gutter = $atts['gutter'] ?? $options['tiles_gutter'];
553 $tiles_gutter_tablet = $atts['gutter'] ?? $options['tiles_gutter_tablet'];
554 $tiles_gutter_mobile = $atts['gutter'] ?? $options['tiles_gutter_mobile'];
555 $tiles_density = $atts['density'] ?? $options['tiles_density'];
556 $tiles_density_tablet = $atts['density'] ?? $options['tiles_density_tablet'];
557 $tiles_density_mobile = $atts['density'] ?? $options['tiles_density_mobile'];
558 }
559 // Horizontal
560 $horizontal_gutter = $options['horizontal_gutter'];
561 $horizontal_image_height = $options['horizontal_image_height'];
562 $horizontal_hide_scrollbar = $options['horizontal_hide_scrollbar'];
563 if ( $layout === 'horizontal' ) {
564 $horizontal_gutter = $atts['gutter'] ?? $options['horizontal_gutter'];
565 $horizontal_image_height = $atts['image_height'] ?? $options['horizontal_image_height'];
566 $horizontal_hide_scrollbar = $atts['hide_scrollbar'] ?? $options['horizontal_hide_scrollbar'];
567 }
568 // Carousel
569 $carousel_gutter = $options['carousel_gutter'];
570 $carousel_arrow_nav_enabled = $options['carousel_arrow_nav_enabled'];
571 $carousel_dot_nav_enabled = $options['carousel_dot_nav_enabled'];
572 $carousel_image_height = $options['carousel_image_height'];
573 $carousel_keep_aspect_ratio = $options['carousel_aspect_ratio'] ?? false;
574 if ( $layout === 'carousel' ) {
575 $carousel_gutter = $atts['gutter'] ?? $options['carousel_gutter'];
576 $carousel_arrow_nav_enabled = $atts['arrow_nav_enabled'] ?? $options['carousel_arrow_nav_enabled'];
577 $carousel_dot_nav_enabled = $atts['dot_nav_enabled'] ?? $options['carousel_dot_nav_enabled'];
578 $carousel_image_height = $atts['image_height'] ?? $options['carousel_image_height'];
579 $carousel_keep_aspect_ratio = array_key_exists( 'keep-aspect-ratio', $atts ) ? $atts['keep-aspect-ratio'] : 1;
580 }
581 // Map
582 $map_gutter = $options['map_gutter'];
583 $map_height = $options['map_height'];
584 if ( $layout === 'map' ) {
585 $map_gutter = $atts['gutter'] ?? $options['map_gutter'];
586 $map_height = $atts['map_height'] ?? $options['map_height'];
587 }
588
589 return compact(
590 'image_ids',
591 'id',
592 'size',
593 'infinite',
594 'custom_class',
595 'link',
596 'is_preview',
597 'updir',
598 'captions',
599 'animation',
600 'layout',
601 'justified_row_height',
602 'justified_gutter',
603 'masonry_gutter',
604 'masonry_columns',
605 'square_gutter',
606 'square_columns',
607 'cascade_gutter',
608 'class_id',
609 'layouts',
610 'tiles_gutter',
611 'tiles_gutter_tablet',
612 'tiles_gutter_mobile',
613 'tiles_density',
614 'tiles_density_tablet',
615 'tiles_density_mobile',
616 'horizontal_gutter',
617 'horizontal_image_height',
618 'horizontal_hide_scrollbar',
619 'carousel_gutter',
620 'carousel_arrow_nav_enabled',
621 'carousel_dot_nav_enabled',
622 'carousel_image_height',
623 'carousel_keep_aspect_ratio',
624 'map_gutter',
625 'map_height',
626 );
627 }
628
629 // #region Options
630
631 static function get_plugin_option_name() {
632 return self::$plugin_option_name;
633 }
634
635 static function get_plugin_option( $option_name, $default = null ) {
636 $options = get_option( self::$plugin_option_name, null );
637 if ( !empty( $options ) && array_key_exists( $option_name, $options ) ) {
638 return $options[$option_name];
639 }
640 return $default;
641 }
642
643 function get_option( $option_name, $default = null ) {
644 $options = $this->get_all_options();
645 if ( array_key_exists( $option_name, $options ) ) {
646 return $options[$option_name];
647 }
648 return $default;
649 }
650
651 function reset_options() {
652 delete_option( 'mgl_db_version');
653 delete_option( $this->option_name );
654 }
655
656 function list_options() {
657 return array(
658 'layout' => 'tiles',
659 'ç' => 'none',
660 'link' => null,
661 'caption_source' => 'caption',
662 'captions_alignment' => 'center',
663 'captions_background' => 'fade-black',
664 'animation' => false,
665 'image_size' => 'srcset',
666 'truncate_on_listing' => true,
667 'truncate_count' => 4,
668
669 'rendering_mode' => 'dom', // Can be 'dom' or 'js'
670 'tiles_gutter' => 10,
671 'tiles_gutter_tablet' => 10,
672 'tiles_gutter_mobile' => 10,
673 'tiles_density' => 'high',
674 'tiles_density_tablet' => 'medium',
675 'tiles_density_mobile' => 'low',
676
677 'justified_density' => 'low',
678 'justified_density_tablet' => 'medium',
679 'justified_density_mobile' => 'low',
680
681 'masonry_gutter' => 5,
682 'masonry_columns' => 3,
683 'masonry_left_to_right' => false,
684 'justified_gutter' => 5,
685 'justified_row_height' => 200,
686 'square_gutter' => 5,
687 'square_columns' => 5,
688 'cascade_gutter' => 10,
689 'horizontal_gutter' => 10,
690 'horizontal_image_height' => 500,
691 'horizontal_hide_scrollbar' => false,
692 'carousel_gutter' => 5,
693 'carousel_image_height' => 500,
694 'carousel_arrow_nav_enabled' => true,
695 'carousel_dot_nav_enabled' => true,
696 'carousel_infinite' => false,
697 'map_engine' => 'googlemaps',
698 'map_height' => 500,
699 'map_zoom' => 10,
700 'map_gutter' => 10,
701 'googlemaps_token' => '',
702 'googlemaps_style' => '[]',
703 'mapbox_token' => '',
704 'mapbox_style' => '{"username":"", "style_id":""}',
705 'maptiler_token' => '',
706
707 // Stylish effect options
708 'stylish_enabled' => false,
709 'stylish_border_radius' => 6,
710 'stylish_shadow_opacity' => 0.08,
711 'stylish_shadow_opacity_hover' => 0.12,
712 'stylish_transition_speed' => 250,
713
714 //PRO OPTIONS
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 list_pro_options() {
724 return array(
725 'infinite' => false,
726 'infinite_buffer' => 0,
727 'right_click' => false,
728 'gallery_shortcode_override_disabled' => false,
729 'skeleton_loading' => false,
730 );
731 }
732
733 function get_all_options() {
734 $options = get_option( $this->option_name, null );
735 $options = $this->check_options( $options );
736 return $options;
737 }
738
739 // Upgrade from the old way of storing options to the new way.
740
741 function check_options( $options = [] ) {
742 $plugin_options = $this->list_options();
743 $pro_options = $this->list_pro_options();
744
745 $options = empty( $options ) ? [] : $options;
746 $hasChanges = false;
747
748 foreach ( $plugin_options as $option => $default ) {
749 // The option already exists
750 if ( isset( $options[$option] ) ) {
751 continue;
752 }
753 // The option does not exist, so we need to add it.
754 // Let's use the old value if any, or the default value.
755 $options[$option] = get_option( 'mgl_' . $option, $default );
756 delete_option( 'mgl_' . $option );
757 $hasChanges = true;
758 }
759
760 if( !$this->pro_module ) {
761 foreach ( $pro_options as $pro_option => $default ) {
762 if ( $options[$pro_option] !== $default ) {
763 $options[$pro_option] = $default;
764 $hasChanges = true;
765 }
766 }
767 }
768
769 if ( $hasChanges ) {
770 update_option( $this->option_name , $options );
771 }
772 return $options;
773 }
774
775 function update_options( $options ) {
776 if ( !update_option( $this->option_name, $options, false ) ) {
777 return false;
778 }
779 $options = $this->sanitize_options();
780 return $options;
781 }
782
783
784 // Validate and keep the options clean and logical.
785 function sanitize_options() {
786 $options = $this->get_all_options();
787 // something to do
788 return $options;
789 }
790
791 # endregion
792
793 function get_caption_from_source( $image ) {
794 $caption_source = $this->get_option( 'caption_source', 'caption' );
795 $caption = '';
796
797 switch ( $caption_source ) {
798 case 'title':
799 $caption = $image->title;
800 break;
801 case 'caption':
802 $caption = $image->caption;
803 break;
804 case 'description':
805 $caption = $image->description;
806 break;
807 case 'alt':
808 $caption = $image->alt;
809 break;
810 default:
811 $caption = $image->caption;
812 break;
813 }
814
815 return $caption;
816 }
817
818 function get_gallery_images( array $image_ids, array $atts, string $layout, string $size, array $posts_ids = []) {
819 global $wpdb;
820
821 // Escape the array of IDs for SQL
822 $ids = array_map( 'intval', $image_ids );
823 $ids_str = implode( ',', $ids );
824
825 $query = "SELECT
826 p.ID id,
827 p.post_title title,
828 p.post_content description,
829 p.post_excerpt caption,
830 pm.meta_value alt,
831 pm2.meta_value meta
832
833 FROM $wpdb->posts p
834 LEFT JOIN $wpdb->postmeta pm ON pm.post_id = p.ID AND pm.meta_key = '_wp_attachment_image_alt'
835 LEFT JOIN $wpdb->postmeta pm2 ON pm2.post_id = p.ID AND pm2.meta_key = '_wp_attachment_metadata'
836
837 WHERE p.post_type = 'attachment'
838
839 AND p.ID IN (" . $ids_str . ")
840 ";
841
842 $res = $wpdb->get_results( $query );
843
844 $ids = explode( ',', $ids_str );
845 $images = [];
846 foreach ( $res as $r ) {
847 $images[$r->id] = [
848 'caption' => $this->get_caption_from_source( $r ),
849 'meta' => unserialize( $r->meta ),
850 ];
851 }
852 $cleanIds = [];
853 foreach ( $ids as $id ) {
854 if ( isset( $images[$id] ) )
855 array_push( $cleanIds, $id );
856 }
857 $ids = apply_filters( 'mgl_sort', $cleanIds, $images, $layout, $atts );
858
859 if ($layout === 'map') {
860 return $this->get_map_images( $ids, $images, $atts );
861 }
862
863 $result = [];
864 foreach ($ids as $index => $id) {
865 $image = $images[$id];
866
867 // Determine orientation if layout is 'tiles'
868 $orientation = [];
869 if ($layout === 'tiles') {
870 $orientation = [
871 'orientation' => ($image['meta']['width'] > $image['meta']['height'] ? 'o' : 'i')
872 ];
873 }
874
875 $default_link = $this->get_option( 'link', null );
876 $link_attr = $this->get_link_attributes( $id, $atts['link'] ?? $default_link, $image );
877 $no_lightbox = $link_attr['type'] === 'link';
878
879 $mergedArray = [
880 'id' => $id,
881 'caption' => wp_kses(
882 html_entity_decode(apply_filters( 'mgl_caption', $image['caption'], $id ), ENT_QUOTES),
883 [
884 'strong' => [], // Bold
885 'b' => [], // Bold alternative
886 'em' => [], // Italic
887 'i' => [] // Italic alternative
888 ]
889 ),
890 'img_html' => apply_filters( 'mgl_gallery_written',
891 $this->get_img_html( $id, $size, $layout, $atts, $image, $no_lightbox ),
892 $layout
893 ),
894 'link_href' => $link_attr['href'] ?? null,
895 'link_target' => $link_attr['target'] ?? null,
896 'link_rel' => $link_attr['rel'] ?? null,
897 'attributes' => $this->get_attributes( $id, $image, $layout ),
898 ];
899
900 if( !empty( $posts_ids ) && isset( $atts['hero'] ) && $atts['hero'] ) {
901
902 $post_id = $posts_ids[$index];
903 $post = get_post( $post_id );
904
905 $mergedArray['featured_post_id'] = $post_id;
906 $mergedArray['featured_post_title'] = $post->post_title;
907 $mergedArray['featured_post_excerpt'] = $post->post_excerpt;
908 $mergedArray['featured_post_url'] = get_permalink( $post_id );
909
910 }
911
912 $result[] = array_merge( $image, $mergedArray, $orientation );
913 }
914
915 return $result;
916 }
917
918 private function get_image_class( $id, $layout, $noLightbox ) {
919 $base_class = 'wp-image-' . $id;
920
921 if( $layout === 'carousel' ) {
922 $base_class .= ' skip-lazy';
923 }
924
925 if ( $noLightbox ) {
926 $base_class .= ' no-lightbox';
927 }
928 return $base_class;
929 }
930
931 private function get_img_html( $id, $size, $layout, $atts, $data, $noLightbox ) {
932
933 //check if the media is a video
934 $media_type = get_post_mime_type( $id );
935 if ( strpos( $media_type, 'video' ) !== false ) {
936 $video = wp_get_attachment_url( $id );
937 $video = apply_filters( 'mgl_video', $video, $id, $data );
938 if ( !empty( $video ) ) {
939 return '<video class="wp-video-'. $id .'" controls="controls" onclick="() => this.play();"><source src="' . $video . '" type="' . $media_type . '"></video>';
940 }
941 }
942
943 $image_size = $this->get_option( 'image_size', 'srcset' );
944 $img_html = null;
945 if ( empty( $image_size ) || $image_size === 'srcset' ) {
946 $img_html = wp_get_attachment_image( $id, $size, false, [
947 'class' => $this->get_image_class( $id, $layout, $noLightbox ),
948 'draggable' => $layout === 'carousel' ? 'false' : null,
949 ]);
950 }
951 else {
952 $info = wp_get_attachment_image_src( $id, $image_size );
953 $alt_text = get_post_meta( $id, '_wp_attachment_image_alt', true );
954
955 $img_html = '<img loading="lazy" src="' . $info[0] . '" class="' . $this->get_image_class( $id, $layout, $noLightbox ) . '" alt="' . esc_attr( $alt_text ) . '" />';
956 }
957
958 if ( $layout === 'masonry' ) {
959 $masonry_column = $this->get_option( 'masonry_column', 3 );
960 $columns = ( isset( $atts['columns'] ) ? $atts['columns'] : $masonry_column ) + 1;
961 $img_html = str_replace( '100vw', 100 / $columns . 'vw', $img_html );
962 }
963 else if ( $layout === 'square' ) {
964 $square_column = $this->get_option( 'square_columns', 5 );
965 $columns = ( isset( $atts['columns'] ) ? $atts['columns'] : $square_column ) + 1;
966 $img_html = str_replace( '100vw', 100 / $columns . 'vw', $img_html );
967 }
968 else if ( $layout === 'cascade' ) {
969 $img_html = str_replace( '100vw', 100 / 3 . 'vw', $img_html );
970 }
971
972 return wp_kses( $img_html, [
973 'img' => [
974 'src' => true,
975 'srcset' => true,
976 'loading' => true,
977 'tabindex' => true,
978 'sizes' => true,
979 'class' => true,
980 'id' => true,
981 'width' => true,
982 'height' => true,
983 'alt' => true,
984 'align' => true,
985 'draggable' => true,
986 ]
987 ]
988 );
989 }
990
991 private function get_link_attributes( $id, $link, $data ) {
992 $link_url = null;
993 $type = 'media';
994 $rel = null;
995 $target = '_self';
996
997 if ( $link === 'attachment' ) {
998 $link_url = get_permalink( (int)$id );
999 }
1000 else if ( $link === 'media' || $link === 'file' ) {
1001 $wpUploadDir = wp_upload_dir();
1002 $updir = trailingslashit( $wpUploadDir['baseurl'] );
1003 if ( isset( $data['meta']['file'] ) ) {
1004 $link_url = $updir . $data['meta']['file'];
1005 } else {
1006 $link_url = get_permalink( (int)$id );
1007 }
1008 }
1009 else if ( $link === null ){
1010 $link_url = get_post_meta( $id, '_gallery_link_url', true );
1011 if ( !empty( $link_url ) ) {
1012 $type = 'link';
1013 $target = get_post_meta( $id, '_gallery_link_target', true );
1014 }
1015 }
1016
1017 $link_attr = [
1018 'href' => !empty( $link_url ) ? esc_url( $link_url ) : null,
1019 'target' => $target,
1020 'type' => $type,
1021 'rel' => $rel,
1022 ];
1023
1024
1025 return apply_filters( 'mgl_link_attributes', $link_attr, (int)$id, $data );
1026 }
1027
1028 private function get_attributes( $id, $data, $layout ) {
1029 $attributes = '';
1030 if ( $layout === 'raw' ) {
1031 if ( isset( $data['meta'] ) && isset( $data['meta']['width'] ) && isset( $data['meta']['height'] ) ) {
1032 $attributes = 'data-mgl-id=' . $id . ' data-mgl-width=' . $data['meta']['width'] . ' data-mgl-height=' . $data['meta']['height'];
1033 }
1034 }
1035 elseif ( $layout === 'tiles' ) {
1036 if ( isset( $data['meta'] ) && isset( $data['meta']['width'] ) && isset( $data['meta']['height'] ) ) {
1037 $attributes = 'data-mgl-id=' . $id . ' data-mgl-width=' . $data['meta']['width'] . ' data-mgl-height=' . $data['meta']['height'];
1038 }
1039 }
1040 $attributes = apply_filters( 'mgl_attributes', $attributes, $id, $data );
1041 if ( $attributes === '' ) {
1042 return [];
1043 }
1044
1045 $attribute_list = explode( ' ', $attributes );
1046 $attributes = [];
1047 foreach ( $attribute_list as $attribute ) {
1048 list( $key, $value ) = explode( '=', $attribute );
1049 $attributes[$key] = $value;
1050 }
1051 return $attributes;
1052 }
1053
1054 private function get_map_images( $ids, $images, $atts = [] ) {
1055 $map_images = array_map( function ( $id ) use ( $images, $atts ) {
1056
1057 $image = $images[$id];
1058 $default_link = $this->get_option( 'link', null );
1059 $link_attr = $this->get_link_attributes( $id, $atts['link'] ?? $default_link, $image );
1060
1061 $geo_coordinates = MeowPro_MGL_Exif::get_gps_data( $id, $image['meta'] );
1062 if ( empty( $geo_coordinates ) ) {
1063 return null;
1064 }
1065 $callback = function ( &$value, $key ) use ( $id ) {
1066 $imgsrc = wp_get_attachment_image_src( $id, $key );
1067 $value = $imgsrc[0];
1068 };
1069 array_walk( $image['meta']['sizes'], $callback );
1070 return array_merge(
1071 $image,
1072 [
1073 'id' => $id,
1074 'file' => $image['meta']['file'],
1075 'file_full' => wp_get_attachment_url( $id ),
1076 'file_srcset' => wp_get_attachment_image_srcset( $id, 'full' ),
1077 'file_sizes' => wp_get_attachment_image_sizes( $id, 'full' ),
1078 'dimension' => [
1079 'width' => $image['meta']['width'],
1080 'height' => $image['meta']['height'],
1081 ],
1082 'sizes' => $image['meta']['sizes'],
1083 'data' => [
1084 'caption' => $image['meta']['image_meta']['caption'],
1085 'gps' => $geo_coordinates,
1086 ],
1087 'link' => $link_attr,
1088 ]
1089 );
1090 }, $ids );
1091 return array_values( array_filter( $map_images ) );
1092 }
1093
1094 public function get_mgl_root_class( $atts, $classes = ['mgl-root'] ) {
1095 $classes[] = isset( $atts['align'] ) ? 'align' . $atts['align'] : '';
1096 return trim( implode( ' ', $classes ) );
1097 }
1098
1099 public function get_data_as_json( $data ) {
1100 return esc_attr( htmlspecialchars( wp_json_encode( $data ), ENT_QUOTES, 'UTF-8' ) );
1101 }
1102
1103 public function generate_uniqid($length = 13) {
1104 // Use WordPress function
1105 if ( function_exists( 'wp_unique_id' ) ) {
1106 $prefix = uniqid();
1107 return wp_unique_id( $prefix );
1108 }
1109 // Fall back
1110 else {
1111 if ( function_exists( "random_bytes" ) ) {
1112 $bytes = random_bytes( ceil( $length / 2 ) );
1113 }
1114 elseif ( function_exists( "openssl_random_pseudo_bytes" ) ) {
1115 $bytes = openssl_random_pseudo_bytes(ceil($length / 2));
1116 }
1117 else {
1118 throw new Exception( "No cryptographically secure random function available." );
1119 }
1120 return substr( bin2hex( $bytes ), 0, $length );
1121 }
1122 }
1123
1124
1125 public function get_gallery_by_id( $id ) {
1126 global $wpdb;
1127 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1128 $gallery = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $shortcodes_table WHERE id = %s", $id ), ARRAY_A );
1129
1130 if ( !$gallery ) {
1131 throw new Exception( __( 'Gallery not found.', MGL_DOMAIN ));
1132 }
1133 $gallery['medias'] = maybe_unserialize( $gallery['medias'] );
1134 $gallery['posts'] = $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null;
1135 $gallery['tags'] = $gallery['tags'] ? unserialize( $gallery['tags'] ) : null;
1136
1137 return $gallery;
1138 }
1139
1140 public function get_galleries_by_ids( $ids ){
1141 global $wpdb;
1142 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1143 $galleries = [];
1144 $ids_str = "'" . implode( "','", array_map( 'esc_sql', $ids )) . "'";
1145 $query = "SELECT * FROM $shortcodes_table WHERE id IN ( $ids_str )";
1146 $results = $wpdb->get_results( $query, ARRAY_A );
1147 foreach ( $results as $gallery ) {
1148 $galleries[$gallery['id']] = [
1149 'name' => $gallery['name'],
1150 'description' => $gallery['description'],
1151 'layout' => $gallery['layout'],
1152 'medias' => maybe_unserialize( $gallery['medias'] ),
1153 'lead_image_id' => $gallery['lead_image_id'],
1154 'order_by' => $gallery['order_by'],
1155 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1156 'dynamic_source' => $gallery['dynamic_source'],
1157 'hero' => ( bool )$gallery['is_hero_mode'],
1158 'posts' => $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null,
1159 'latest_posts' => $gallery['latest_posts'],
1160 'tags' => $gallery['tags'] ? unserialize( $gallery['tags'] ) : null,
1161 'updated' => strtotime( $gallery['updated_at'] )
1162 ];
1163 }
1164 return $galleries;
1165 }
1166
1167 public function get_galleries( $offset = 0, $limit = 10, $order = 'DESC', $sort = null, $page = 1, $search = '' ) {
1168 global $wpdb;
1169 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1170 Meow_MGL_Migrations::check_db();
1171
1172 // Get total count
1173 $total = 0;
1174 if ( empty( $search ) ) {
1175 $total = $wpdb->get_var( "SELECT COUNT( * ) FROM $shortcodes_table" );
1176 } else {
1177 $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $shortcodes_table WHERE name LIKE %s", '%' . $wpdb->esc_like( $search ) . '%' ) );
1178 }
1179
1180 // Calculate offset based on page if provided
1181 if ($page > 1 && $offset === 0) {
1182 $offset = ($page - 1) * $limit;
1183 }
1184
1185 // Get shortcodes with pagination and sorting
1186 $sort_accessor_to_column = [
1187 'info' => 'name',
1188 'updated' => 'updated_at',
1189 'rank' => 'pref_rank',
1190 ];
1191
1192 $sort = $sort_accessor_to_column[$sort] ?? 'pref_rank';
1193 $order = in_array( strtoupper( $order ), ['ASC', 'DESC'] ) ? strtoupper( $order ) : 'DESC';
1194
1195 // Sort by rank DESC first, then by updated_at for equal ranks
1196 $query = $wpdb->prepare(
1197 "SELECT * FROM $shortcodes_table WHERE name LIKE %s ORDER BY $sort $order, updated_at DESC LIMIT %d, %d",
1198 '%' . $wpdb->esc_like( $search ) . '%', $offset, $limit
1199 );
1200
1201 $results = $wpdb->get_results( $query, ARRAY_A );
1202 $shortcodes = [];
1203
1204 foreach ( $results as $gallery ) {
1205 // Transform database format to match expected format
1206 $shortcodes[$gallery['id']] = [
1207 'name' => $gallery['name'],
1208 'description' => $gallery['description'],
1209 'layout' => $gallery['layout'],
1210 'medias' => maybe_unserialize( $gallery['medias'] ),
1211 'lead_image_id' => $gallery['lead_image_id'],
1212 'order_by' => $gallery['order_by'],
1213 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1214 'hero' => ( bool )$gallery['is_hero_mode'],
1215 'posts' => $gallery['posts'] ? maybe_unserialize( $gallery['posts'] ) : null,
1216 'latest_posts' => $gallery['latest_posts'],
1217 'tags' => $gallery['tags'] ? unserialize( $gallery['tags'] ) : null,
1218 'dynamic_source' => $gallery['dynamic_source'],
1219 'rank' => intval( $gallery['pref_rank'] ?? 0 ),
1220 'updated' => strtotime( $gallery['updated_at'] )
1221 ];
1222 }
1223
1224 return [
1225 'total' => $total,
1226 'galleries' => $shortcodes
1227 ];
1228 }
1229
1230 public function get_collection_by_id( $id ) {
1231 global $wpdb;
1232 $collections_table = $wpdb->prefix . 'mgl_collections';
1233 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1234
1235 // Get the collection
1236 $collection = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $collections_table WHERE id = %d", $id ), ARRAY_A );
1237 if ( !$collection ) {
1238 throw new Exception( __( 'Collection not found.', MGL_DOMAIN ));
1239 }
1240 $collection['galleries_ids'] = unserialize( $collection['galleries_ids'] );
1241
1242 // Get the associated galleries
1243 $galleries = [];
1244 if ( !empty( $collection['galleries_ids'] )) {
1245 $galleries_ids_str = "'" . implode( "','", array_map( 'esc_sql', $collection['galleries_ids'] )) . "'";
1246 $galleries_query = "SELECT * FROM $shortcodes_table WHERE id IN ( $galleries_ids_str )";
1247 $galleries_data = $wpdb->get_results( $galleries_query, ARRAY_A );
1248
1249 foreach ( $galleries_data as $gallery ) {
1250 // Transform database format to match expected format
1251 $galleries[] = [
1252 'id' => $gallery['id'],
1253 'name' => $gallery['name'],
1254 'description' => $gallery['description'],
1255 'layout' => $gallery['layout'],
1256 'medias' => unserialize( $gallery['medias'] ),
1257 'lead_image_id' => $gallery['lead_image_id'],
1258 'order_by' => $gallery['order_by'],
1259 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1260 'hero' => ( bool )$gallery['is_hero_mode'],
1261 'posts' => $gallery['posts'] ? unserialize( $gallery['posts'] ) : null,
1262 'latest_posts' => $gallery['latest_posts'],
1263 'updated' => strtotime( $gallery['updated_at'] )
1264 ];
1265 }
1266
1267 // Format collection data
1268 $collection['galleries'] = $galleries;
1269 }
1270
1271 return $collection;
1272 }
1273
1274 public function get_collections( $offset = 0, $limit = 10, $order = 'DESC', $page = 1, $search = '' ) {
1275 global $wpdb;
1276 $collections_table = $wpdb->prefix . 'mgl_collections';
1277 $shortcodes_table = $wpdb->prefix . 'mgl_gallery_shortcodes';
1278 Meow_MGL_Migrations::check_db();
1279
1280 // Get total count
1281 if( empty( $search ) ) {
1282 $total = $wpdb->get_var( "SELECT COUNT( * ) FROM $collections_table" );
1283 } else {
1284 $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $collections_table WHERE name LIKE %s", '%' . $wpdb->esc_like( $search ) . '%' ) );
1285 }
1286 // Calculate offset based on page if provided
1287 if ($page > 1 && $offset === 0) {
1288 $offset = ($page - 1) * $limit;
1289 }
1290
1291 // Get collections with pagination and sorting
1292 $query = $wpdb->prepare(
1293 "SELECT * FROM $collections_table WHERE name LIKE %s ORDER BY updated_at $order LIMIT %d, %d",
1294 '%' . $wpdb->esc_like( $search ) . '%', $offset, $limit
1295 );
1296
1297 $collections = $wpdb->get_results( $query, ARRAY_A );
1298 $result = [];
1299
1300 foreach ( $collections as $collection ) {
1301 $collection_id = $collection['id'];
1302 $galleries_ids = unserialize( $collection['galleries_ids'] );
1303
1304 // Get the associated galleries
1305 $galleries = [];
1306 if ( !empty( $galleries_ids )) {
1307 $galleries_ids_str = "'" . implode( "','", array_map( 'esc_sql', $galleries_ids )) . "'";
1308 $galleries_query = "SELECT * FROM $shortcodes_table WHERE id IN ( $galleries_ids_str )";
1309 $galleries_data = $wpdb->get_results( $galleries_query, ARRAY_A );
1310
1311 foreach ( $galleries_data as $gallery ) {
1312 // Transform database format to match expected format
1313 $gallery_item = [
1314 'id' => $gallery['id'],
1315 'name' => $gallery['name'],
1316 'description' => $gallery['description'],
1317 'layout' => $gallery['layout'],
1318 'medias' => unserialize( $gallery['medias'] ),
1319 'lead_image_id' => $gallery['lead_image_id'],
1320 'order_by' => $gallery['order_by'],
1321 'is_post_mode' => ( bool )$gallery['is_post_mode'],
1322 'hero' => ( bool )$gallery['is_hero_mode'],
1323 'posts' => $gallery['posts'] ? unserialize( $gallery['posts'] ) : null,
1324 'latest_posts' => $gallery['latest_posts'],
1325 'tags' => $gallery['tags'] ? unserialize( $gallery['tags'] ) : null,
1326 'dynamic_source' => $gallery['dynamic_source'],
1327 'updated' => strtotime( $gallery['updated_at'] )
1328 ];
1329 $galleries[] = $gallery_item;
1330 }
1331 }
1332
1333 // Format collection data
1334 $result[$collection_id] = [
1335 'name' => $collection['name'],
1336 'description' => $collection['description'],
1337 'layout' => $collection['layout'],
1338 'galleries_ids' => $galleries_ids,
1339 'galleries' => $galleries,
1340 'updated' => strtotime( $collection['updated_at'] )
1341 ];
1342 }
1343
1344 return [
1345 'total' => $total,
1346 'collections' => $result
1347 ];
1348 }
1349
1350 }
1351
1352 ?>
1353