PluginProbe
Meow Gallery / 5.5.2
Meow Gallery v5.5.2
5.5.4 5.5.3 5.5.2 5.5.1 5.5.0 5.4.9 5.4.8 5.4.7 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 All 156 releases
meow-gallery / classes / core.php

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

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