PluginProbe
Meow Gallery / 5.5.5
Meow Gallery v5.5.5
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.5, at classes/core.php

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