PluginProbe
Meow Gallery / 5.5.1
Meow Gallery v5.5.1
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.1, at classes/core.php

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