PluginProbe
Photo Gallery by Re Gallery: Responsive Image Gallery with AI SEO / trunk
Photo Gallery by Re Gallery: Responsive Image Gallery with AI SEO vtrunk
1.20.14 1.20.13 1.20.12 1.20.11 1.20.10 1.20.9 1.20.8 1.20.7 1.20.6 1.20.5 1.20.4 1.20.3 1.20.2 1.20.1 1.20.0 1.19.2 1.19.1 1.19.0 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 All 135 releases
regallery / includes / gallery.php

gallery.php in Photo Gallery by Re Gallery: Responsive Image Gallery with AI SEO trunk, at includes/gallery.php

2,203 lines 78.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined('ABSPATH') || die('Access Denied');
3
4 class REACG_Gallery {
5 private $obj;
6
7 private $default_max_dimension = 2304;
8
9 public function __construct($that, $register = TRUE) {
10 $this->obj = $that;
11 if ( !$register ) {
12 return;
13 }
14 $this->register_post_type();
15 add_action('admin_menu', [ $this, 'add_submenu' ]);
16 add_action('admin_head', [ $this, 'modify_external_submenu_url']);
17 add_action('admin_footer', [ $this, 'opt_in' ]);
18
19 add_action( 'wp_dashboard_setup', [ $this, 'register_dashboard_widgets' ] );
20
21 // Add columns to the custom post list.
22 add_filter('manage_' . REACG_CUSTOM_POST_TYPE . '_posts_columns' , [ $this, 'custom_columns' ]);
23 add_action('manage_' . REACG_CUSTOM_POST_TYPE . '_posts_custom_column', [ $this, 'custom_columns_content' ], 10, 2);
24 add_filter('manage_edit-' . REACG_CUSTOM_POST_TYPE . '_sortable_columns', [ $this, 'make_images_count_column_sortable' ]);
25 add_action('pre_get_posts', [ $this, 'images_count_column_orderby' ]);
26
27 // Add duplicate link to the list.
28 add_filter('post_row_actions', [$this, 'duplicate_link'], 10, 2 );
29 add_action('admin_action_reacg_duplicate_gallery', [ $this, 'duplicate_gallery' ]);
30
31 add_action('save_post', [ $this, 'save_post' ], 10, 2);
32 add_action('delete_post', [ $this, 'delete_post' ], 10, 2);
33
34 // Register an ajax action to save images to the gallery.
35 add_action('wp_ajax_reacg_save_images', [ $this, 'save_images' ]);
36 add_action('wp_ajax_reacg_save_thumbnail', [ $this, 'save_thumbnail' ]);
37 add_action('wp_ajax_reacg_delete_thumbnail', [ $this, 'delete_thumbnail' ]);
38 add_action('wp_ajax_reacg_auto_upload_video_cover', [ $this, 'upload_video_cover' ]);
39
40 add_filter('wp_generate_attachment_metadata', [ $this, 'generate_attachment_metadata' ], 10, 2);
41 // Add custom field to the media uploader.
42 add_filter('attachment_fields_to_edit', [ $this, 'attachment_field' ], 10, 2);
43 // Save custom field when an image is updated.
44 add_filter('attachment_fields_to_save', [ $this, 'save_attachment_field' ], 10, 2);
45 add_filter('wp_prepare_attachment_for_js', [ $this, 'prepare_attachment_for_js' ], 10, 3);
46
47 // Register an ajax action to save a gallery (need for builders).
48 add_action('wp_ajax_reacg_save_gallery', [ $this, 'save_gallery' ]);
49 // Register an ajax action to get the gallery images (need for builders).
50 add_action('wp_ajax_reacg_get_images', [ $this, 'meta_box_images' ]);
51
52 // Register a route to make the gallery images data available with the API.
53 add_action( 'rest_api_init', function () {
54 register_rest_route( $this->obj->prefix . '/v1', '/gallery/(?P<id>-?\d+)/images', array(
55 'methods' => WP_REST_Server::READABLE,
56 'callback' => [ $this, 'get_images_rout'],
57 'args' => array(
58 'id' => array(
59 'validate_callback' => function($param, $request, $key) {
60 return is_numeric( $param );
61 }
62 ),
63 ),
64 'permission_callback' => '__return_true',
65 ) );
66 } );
67
68 // Register a route to get custom templates.
69 add_action( 'rest_api_init', function () {
70 register_rest_route( $this->obj->prefix . '/v1', '/templates', array(
71 'methods' => WP_REST_Server::READABLE,
72 'callback' => [ $this, 'get_custom_templates'],
73 'permission_callback' => '__return_true',
74 ) );
75 } );
76
77 // Register a route to get/set/delete options for the gallery with the API.
78 add_action( 'rest_api_init', function () {
79 require_once REACG()->plugin_dir . "/includes/options.php";
80 $options_args = array(
81 'gallery_id' => array(
82 'validate_callback' => function($param, $request, $key) {
83 return is_numeric( $param );
84 }
85 ),
86 );
87 $options = new REACG_Options(true);
88 register_rest_route( $this->obj->prefix . '/v1', '/options/(?P<gallery_id>-?\d+)', array(
89 array(
90 'methods' => WP_REST_Server::READABLE,
91 'callback' => [ $options, 'options'],
92 'args' => $options_args,
93 'permission_callback' => '__return_true',
94 ),
95 array(
96 'methods' => WP_REST_Server::DELETABLE . ", " . WP_REST_Server::EDITABLE,
97 'callback' => [ $options, 'options'],
98 'args' => $options_args,
99 'permission_callback' => [$this, 'privileged_permission'],
100 ),
101 ) );
102 } );
103
104 // Register a route to get Google fonts with the API.
105 add_action( 'rest_api_init', function () {
106 register_rest_route( $this->obj->prefix . '/v1', '/google-fonts', array(
107 'methods' => WP_REST_Server::READABLE,
108 'callback' => [ 'REACGLibrary', 'get_fonts'],
109 'permission_callback' => [$this, 'privileged_permission'],
110 ) );
111 } );
112
113 add_filter( 'wp_handle_upload_prefilter', [ $this, 'handle_upload_prefilter' ] );
114 }
115
116 /**
117 * @param $file
118 *
119 * @return mixed
120 */
121 public function handle_upload_prefilter( $file ) {
122 // Only for gallery uploads.
123 if ( !REACGLibrary::verify_nonce() ) {
124 return $file;
125 }
126 if ( empty( $_POST['reacg'] )
127 || sanitize_text_field(wp_unslash($_POST['reacg'])) !== 'gallery' ) {
128 return $file;
129 }
130 if ( ! current_user_can( 'upload_files' ) ) {
131 return $file;
132 }
133
134 $ext = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );
135 // Check HEIC/HEIF.
136 if ( in_array( $ext, [ 'heic', 'heif' ], TRUE ) ) {
137 // Imagick extension missing.
138 if ( !extension_loaded( 'imagick' ) || !class_exists( 'Imagick' ) ) {
139 $file['error'] = __('HEIC images cannot be uploaded because the Imagick PHP extension is not installed on this server. Please contact your hosting provider.', 'regallery');
140 return $file;
141 }
142 // HEIC not supported.
143 if ( ! in_array( 'HEIC', Imagick::queryFormats(), TRUE ) ) {
144 $file['error'] = __('HEIC images are not supported by the server’s ImageMagick configuration. Please contact your hosting provider.', 'regallery');
145 }
146 }
147
148 return $file;
149 }
150
151 public function generate_attachment_metadata($metadata, $attachment_id) {
152 $mime = get_post_mime_type($attachment_id);
153
154 if ( strpos($mime, 'image/') === 0 && !empty($metadata['image_meta']) ) {
155 $exif = $this->get_exif($metadata['image_meta']);
156 if ( !empty($exif) ) {
157 update_post_meta($attachment_id, 'exif', sanitize_textarea_field($exif));
158 }
159 }
160
161 return $metadata;
162 }
163
164 /**
165 * Build a readable summary of EXIF data.
166 *
167 * @param $im
168 *
169 * @return string
170 */
171 private function get_exif($im) {
172 $lines = [];
173 if ( !empty($im['camera']) ) {
174 $lines[] = 'Camera: ' . $im['camera'];
175 }
176 if ( !empty($im['lens']) ) {
177 $lines[] = 'Lens: ' . $im['lens'];
178 }
179 if ( !empty($im['focal_length']) ) {
180 $lines[] = 'Focal Length: ' . rtrim($im['focal_length'], 'mm') . 'mm';
181 }
182 if ( !empty($im['aperture']) ) {
183 $lines[] = 'Aperture: f/' . $im['aperture'];
184 }
185 if ( !empty($im['shutter_speed']) ) {
186 $lines[] = 'Shutter: ' . $im['shutter_speed'] . 's';
187 }
188 if ( !empty($im['iso']) ) {
189 $lines[] = 'ISO: ' . $im['iso'];
190 }
191 if ( !empty($im['copyright']) ) {
192 $lines[] = 'Copyright: ' . $im['copyright'];
193 }
194
195 return implode("\n", $lines);
196 }
197
198 /**
199 * Add custom field to the media uploader.
200 *
201 * @param $form_fields
202 * @param $post
203 *
204 * @return mixed
205 */
206 public function attachment_field($form_fields, $post) {
207 $form_fields['action_url'] = [
208 'label' => __('Action URL', 'regallery'),
209 'input' => 'text',
210 'value' => get_post_meta($post->ID, 'action_url', TRUE),
211 ];
212
213 $exif = get_post_meta($post->ID, 'exif', TRUE);
214 if ( empty($exif) ) {
215 $metadata = wp_get_attachment_metadata($post->ID);
216 if ( !empty($metadata['image_meta']) ) {
217 $exif = $this->get_exif($metadata['image_meta']);
218 }
219 }
220 $form_fields['exif'] = [
221 'label' => __('Metadata (EXIF)', 'regallery'),
222 'input' => 'textarea',
223 'value' => $exif,
224 'helps' => __('Auto-generated from image metadata on upload. You can edit this text if needed.', 'regallery'),
225 ];
226
227 return $form_fields;
228 }
229
230 /**
231 * Save custom field when an image is updated.
232 *
233 * @param $post
234 * @param $attachment
235 *
236 * @return mixed
237 */
238 public function save_attachment_field($post, $attachment) {
239 if ( isset($attachment['action_url']) ) {
240 update_post_meta($post['ID'], 'action_url', sanitize_url($attachment['action_url']));
241 }
242 if (isset($attachment['exif'])) {
243 update_post_meta($post['ID'], 'exif', sanitize_textarea_field($attachment['exif']));
244 }
245
246 return $post;
247 }
248
249 /**
250 * Add plugin-specific attachment fields to the JS response.
251 *
252 * @param array $response
253 * @param WP_Post $attachment
254 * @param array|false $meta
255 *
256 * @return array
257 */
258 public function prepare_attachment_for_js($response, $attachment, $meta) {
259 if ( empty($response) || !is_array($response) ) {
260 return $response;
261 }
262
263 $response['action_url'] = esc_url_raw(get_post_meta($attachment->ID, 'action_url', TRUE));
264
265 $exif = get_post_meta($attachment->ID, 'exif', TRUE);
266 if ( empty($exif) && !empty($meta['image_meta']) ) {
267 $exif = $this->get_exif($meta['image_meta']);
268 }
269
270 $response['exif'] = html_entity_decode($exif);
271
272 return $response;
273 }
274
275 /**
276 * Add submenu.
277 *
278 * @return void
279 */
280 public function add_submenu() {
281 // add_submenu_page('edit.php?post_type=reacg', __('Talk to a Gallery Expert', 'regallery'), __('Talk to a Gallery Expert', 'regallery'), 'manage_options', 'reacg-book-a-call');
282 add_submenu_page('edit.php?post_type=reacg', __('Support', 'regallery'), __('Support', 'regallery'), 'manage_options', 'reacg-support');
283 add_submenu_page('edit.php?post_type=reacg', __('About Us', 'regallery'), __('About Us', 'regallery'), 'manage_options', 'reacg-external-link');
284 if ( !REACG_PLAYGROUND ) {
285 add_submenu_page('edit.php?post_type=reacg', REACG_BUY_NOW_TEXT, REACG_BUY_NOW_TEXT, 'manage_options', 'reacg-upgrade');
286 }
287 }
288
289 /**
290 * Modify external submenu URL.
291 *
292 * @return void
293 */
294 public function modify_external_submenu_url() {
295 global $submenu;
296 $parent_slug = 'edit.php?post_type=reacg';
297
298 if ( isset( $submenu[$parent_slug] ) ) {
299 foreach ( $submenu[$parent_slug] as $index => $menu_item ) {
300 // Check for the temporary menu slug.
301 if ( $menu_item[2] === 'reacg-external-link' ) {
302 // Replace with the external URL.
303 $submenu[$parent_slug][$index][2] = esc_url(add_query_arg(['utm_medium' => 'submenu', 'utm_campaign' => 'about_us'], REACG_WEBSITE_URL_UTM));
304 }
305 elseif ( $menu_item[2] === 'reacg-upgrade' ) {
306 // Replace with the external URL.
307 $submenu[$parent_slug][$index][2] = esc_url(add_query_arg(['utm_medium' => 'submenu', 'utm_campaign' => 'upgrade'], REACG_PRICING_URL_UTM));
308 if ( isset( $submenu[ $parent_slug ][ $index ][4] ) ) {
309 $submenu[ $parent_slug ][ $index ][4] .= ' reacg-sidebar-upgrade-pro reacg-hidden';
310 } else {
311 $submenu[ $parent_slug ][ $index ][] = 'reacg-sidebar-upgrade-pro reacg-hidden';
312 }
313 echo '<script>jQuery(document).ready(function () { if (!JSON.parse(localStorage.getItem("reacg-pro"))) { jQuery(".reacg-sidebar-upgrade-pro").removeClass("reacg-hidden"); } });</script>';
314 }
315 }
316 }
317 }
318
319 /**
320 * @param WP_REST_Request $request
321 *
322 * @return bool
323 */
324 public function privileged_permission( WP_REST_Request $request ) {
325 if ( !is_user_logged_in() ) {
326 return FALSE;
327 }
328
329 $gallery_id = intval( $request->get_param( 'gallery_id' ) );
330 if ( $gallery_id > 0 ) {
331 return current_user_can( 'edit_post', $gallery_id );
332 }
333
334 return current_user_can( 'edit_posts' );
335 }
336
337 /**
338 * Add columns to the custom post list.
339 *
340 * @param $columns
341 *
342 * @return array
343 */
344 public function custom_columns($columns) {
345 wp_enqueue_style($this->obj->prefix . '_admin');
346
347 $columns = array_merge(array_slice($columns, 0, 1), array('reacg_thumbnail' => __('Thumbnail', 'regallery')), array_slice($columns, 1));
348 $columns = array_merge(array_slice($columns, 0, 3), array('reacg_shortcode' => __('Shortcode', 'regallery'), 'reacg_images_count' => __('Items', 'regallery')), array_slice($columns, 3));
349
350 return $columns;
351 }
352
353 /**
354 * Maker the Images count columns sortable.
355 *
356 * @param $columns
357 *
358 * @return mixed
359 */
360 public function make_images_count_column_sortable($columns) {
361 $columns['reacg_images_count'] = 'reacg_images_count';
362 return $columns;
363 }
364
365 /**
366 * Images count column ordering.
367 *
368 * @param $query
369 *
370 * @return void
371 */
372 public function images_count_column_orderby($query) {
373 if ( !is_admin() ) {
374 return;
375 }
376
377 if ( 'reacg_images_count' === $query->get('orderby') ) {
378 $query->set('meta_key', 'images_count');
379 $query->set('orderby', 'meta_value_num');
380 }
381 }
382
383 private function get_smallest_image_url($item) {
384 if ( !empty($item['sizes']) && !empty($item['sizes'][0]['url']) ) {
385 return $item['sizes'][0]['url'];
386 }
387
388 if ( !empty($item['original']) && !empty($item['original']['url']) ) {
389 return $item['original']['url'];
390 }
391
392 return $this->obj->plugin_url . $this->obj->no_image;
393 }
394
395 private function get_preview_url($post_id) {
396 $images_ids = get_post_meta( $post_id, 'images_ids', TRUE );
397 $images_ids_arr = !empty($images_ids) ? json_decode($images_ids, TRUE) : [];
398 $url = "";
399 if ( !empty($images_ids_arr) ) {
400 $is_deleted_attachment = FALSE;
401 foreach ( $images_ids_arr as $key => $images_id ) {
402 $item = $this->get_item_data($images_id);
403 // The post type is disabled.
404 if ( !$item ) {
405 // The attachment doesn't exist.
406 if ( $item === FALSE ) {
407 unset($images_ids_arr[$key]);
408 $is_deleted_attachment = TRUE;
409 }
410 }
411 elseif ( $url === "" ) {
412 // Get the smallest available image URL.
413 $url = $this->get_smallest_image_url($item);
414 }
415 }
416
417 // Remove attachment ID from the gallery if it doesn't exist anymore.
418 if ( $is_deleted_attachment ) {
419 update_post_meta($post_id, 'images_ids', json_encode(array_values($images_ids_arr)));
420 }
421 }
422
423 if ( $url === "" ) {
424 $url = $this->obj->plugin_url . $this->obj->no_image;
425 }
426
427 return $url;
428 }
429 /**
430 * Add columns content to the custom post list.
431 *
432 * @param $column_id
433 * @param $post_id
434 *
435 * @return void
436 */
437 public function custom_columns_content($column_id, $post_id) {
438 switch ( $column_id ) {
439 case 'reacg_thumbnail': {
440 $url = $this->get_preview_url($post_id);
441 if ( $url ) {
442 ?><div style='background-image: url("<?php echo esc_url($url); ?>")'></div><?php
443 }
444 else {
445 esc_html_e('No image', 'regallery');
446 }
447 break;
448 }
449 case 'reacg_images_count': {
450 $images_count = get_post_meta( $post_id, 'images_count', TRUE );
451 // Count the images ids in case of empty meta.
452 if ( empty($images_count) ) {
453 $images_ids = get_post_meta( $post_id, 'images_ids', TRUE );
454 $images_ids_arr = !empty($images_ids) ? json_decode($images_ids, TRUE) : [];
455 $images_count = count($images_ids_arr);
456 update_post_meta($post_id, 'images_count', $images_count);
457 }
458 echo esc_html($images_count);
459 break;
460 }
461 case 'reacg_shortcode': {
462 ?><code><?php echo esc_html(REACGLibrary::get_shortcode($this->obj, $post_id)); ?></code><?php
463 break;
464 }
465 }
466 }
467
468 /**
469 * Add a Duplicate link to the quick actions list.
470 *
471 * @param $actions
472 * @param $post
473 *
474 * @return mixed
475 */
476 public function duplicate_link( $actions, $post ) {
477 if ( current_user_can( 'edit_posts' ) && $post->post_type === REACG_CUSTOM_POST_TYPE ) {
478 $url = wp_nonce_url(
479 admin_url( 'admin.php?action=reacg_duplicate_gallery&post=' . $post->ID ),
480 REACG_NONCE,
481 REACG_NONCE
482 );
483
484 /* translators: %s: original post title */
485 $title = sprintf(esc_html__('Duplicate “%s”', 'regallery'), empty( $post->post_title ) ? esc_html__('(no title)', 'regallery') : $post->post_title);
486
487 $actions['duplicate'] = '<a href="' . esc_url( $url ) . '" title="' . esc_html($title) . '" aria-label="' . esc_html($title) . '">' . esc_html__('Duplicate', 'regallery') . '</a>';
488 }
489
490 return $actions;
491 }
492
493 /**
494 * Duplicate the gallery.
495 *
496 * @return void
497 */
498 public function duplicate_gallery() {
499 if ( !REACGLibrary::verify_nonce() ) {
500 wp_die(esc_html__('Nonce verification failed!', 'regallery'));
501 }
502
503 if ( !isset($_GET['post']) ) {
504 wp_die(esc_html__('No gallery to duplicate has been provided!', 'regallery'));
505 }
506
507 $post_id = intval($_GET['post']);
508
509 if ( !current_user_can('edit_post', $post_id) ) {
510 wp_die(esc_html__('You are not allowed to duplicate this gallery.', 'regallery'));
511 }
512
513 $post = get_post($post_id);
514 if ( !empty($post) && $post->post_type === REACG_CUSTOM_POST_TYPE ) {
515 $new_post = array(
516 'post_title' => $post->post_title . ' ' . __('(Copy)', 'regallery'),
517 'post_content' => $post->post_content,
518 'post_status' => 'draft',
519 'post_type' => $post->post_type,
520 'post_author' => get_current_user_id(),
521 );
522 $new_post_id = wp_insert_post($new_post);
523 if ( $new_post_id && !is_wp_error($new_post_id) ) {
524 // Copy taxonomy terms.
525 $taxonomies = get_object_taxonomies($post->post_type);
526 foreach ( $taxonomies as $taxonomy ) {
527 $post_terms = wp_get_object_terms($post_id, $taxonomy, array( 'fields' => 'slugs' ));
528 wp_set_object_terms($new_post_id, $post_terms, $taxonomy, FALSE);
529 }
530
531 // Copy post meta.
532 $post_meta = get_post_meta($post_id);
533 foreach ( $post_meta as $meta_key => $meta_values ) {
534 foreach ( $meta_values as $meta_value ) {
535 add_post_meta($new_post_id, $meta_key, maybe_unserialize($meta_value));
536 }
537 }
538
539 // Copy options.
540 add_option('reacg_options' . $new_post_id, get_option('reacg_options' . $post_id, FALSE));
541 }
542
543 // Redirect to the edit screen.
544 wp_safe_redirect(admin_url('edit.php?post_type=' . REACG_CUSTOM_POST_TYPE));
545 exit;
546 }
547 else {
548 wp_die('Gallery creation failed, could not find original gallery.');
549 }
550 }
551
552 /**
553 * Get images rout.
554 *
555 * @param WP_REST_Request $request
556 *
557 * @return WP_REST_Response
558 */
559 public function get_images_rout( WP_REST_Request $request ) {
560 $data = $this->get_images(REACGLibrary::get_gallery_id($request, 'id'));
561 $response = new WP_REST_Response($data['images'], 200);
562 $response->header('X-Images-Count', $data['count']);
563
564 return $response;
565 }
566
567 /**
568 * Get images data for the specific gallery.
569 *
570 * @param $gallery_id
571 * @param $gallery_options
572 *
573 * @return array
574 */
575 public function get_images( $gallery_id, $gallery_options = FALSE ) {
576 $data = [];
577 $all_images_count = 0;
578 $images_ids_arr = [];
579
580 if ( $gallery_id == -1 ) {
581 // To get all galleries images.
582 $gallery_ids = REACGLibrary::get_galleries();
583 foreach ( $gallery_ids as $galleryId ) {
584 $gallery_images_ids = get_post_meta($galleryId, 'images_ids', TRUE);
585 if ( !empty($gallery_images_ids) ) {
586 $images_ids_arr = array_merge($images_ids_arr, json_decode($gallery_images_ids, TRUE));
587 }
588 }
589 }
590 else {
591 $images_ids = get_post_meta($gallery_id, 'images_ids', TRUE);
592 if ( !empty($images_ids) ) {
593 $images_ids_arr = json_decode($images_ids, TRUE);
594 }
595 }
596
597 if ( !empty($images_ids_arr) ) {
598 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required.
599 $order_by = !empty($gallery_options['general']['orderBy']) ? sanitize_text_field($gallery_options['general']['orderBy']) : (isset($_GET['order_by']) ? sanitize_text_field(wp_unslash($_GET['order_by'])) : '');
600 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required.
601 $order = !empty($gallery_options['general']['orderDirection']) ? sanitize_text_field($gallery_options['general']['orderDirection']) : (isset($_GET['order']) ? sanitize_text_field(wp_unslash($_GET['order'])) : 'asc');
602 $is_deleted_attachment = FALSE;
603 $dynamic_exists = FALSE;
604 foreach ( $images_ids_arr as $key => $images_id ) {
605 $item = $this->get_item_data($images_id);
606
607 // The attachment doesn't exist or the post type is disabled.
608 if ( !$item ) {
609 // The attachment doesn't exist.
610 if ( $item === FALSE ) {
611 unset($images_ids_arr[$key]);
612 $is_deleted_attachment = TRUE;
613 }
614 continue;
615 }
616
617 $item['id'] = $gallery_id . $images_id;
618
619 if ( strpos($images_id, "dynamic") !== FALSE ) {
620 $dynamic_exists = TRUE;
621 $post_type = str_replace('dynamic', '', $images_id);
622
623 $gallery_data = $this->get_posts( $gallery_id, $post_type, $order_by, $order );
624 if ( !empty($gallery_data['posts']) ) {
625 $exclude_without_image = !empty($gallery_data['additional_data']['exclude_without_image']);
626 foreach ( $gallery_data['posts'] as $post ) {
627 if ( $exclude_without_image && empty(get_post_thumbnail_id($post->ID)) ) {
628 // When some posts have invalid or orphaned _thumbnail_id values.
629 continue;
630 }
631 $item = $this->get_item_data($post_type . $post->ID);
632 // The post type is disabled.
633 if ( !$item ) {
634 continue;
635 }
636 $item['id'] = $gallery_id . $images_id . $post->ID;
637 $item += $this->get_item_extra_data($post_type, $post);
638 $item['type'] = 'image'; // Overwrite type to show post as image in the gallery.
639 $description = !empty($post->post_excerpt) ? $post->post_excerpt : $post->post_content;
640 $item['description'] = html_entity_decode(wp_kses_post(strip_shortcodes(wp_strip_all_tags($description))));
641 $item['date'] = $post->post_date;
642 $item = $this->add_image_source_metas($item, $post);
643 $data[] = $item;
644 }
645 }
646 }
647 else {
648 if ( preg_match('/^(' . implode('|', array_keys(REACG_ALLOWED_POST_TYPES)) . ')(\d+)$/', $images_id, $matches) ) {
649 $images_id = $matches[2];
650 $post = get_post($images_id);
651 $description = !empty($post->post_excerpt) ? $post->post_excerpt : $post->post_content;
652 $item += $this->get_item_extra_data($matches[1], $post);
653 $item['type'] = 'image'; // Overwrite type to show post as image in the gallery.
654 }
655 else {
656 $post = get_post($images_id);
657 $description = $post->post_content;
658 $item += $this->get_item_extra_data($item['type'], $post);
659 }
660 $item['description'] = html_entity_decode(wp_kses_post(strip_shortcodes(wp_strip_all_tags($description))));
661 $item['date'] = $post->post_date;
662 $item = $this->add_image_source_metas($item, $post);
663 $data[] = $item;
664 }
665 }
666
667 $all_images_count = count($data);
668
669 // Update images count if there is selected dynamic posts.
670 if ( $dynamic_exists &&
671 get_post_meta( $gallery_id, 'images_count', TRUE ) != $all_images_count ) {
672 update_post_meta($gallery_id, 'images_count', $all_images_count);
673 }
674
675 // Remove attachment ID from the gallery if it doesn't exist anymore.
676 if ( $is_deleted_attachment ) {
677 update_post_meta($gallery_id, 'images_ids', json_encode(array_values($images_ids_arr)));
678 }
679
680 // Filter the data by title, description, alt and caption.
681 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required.
682 $filter = !empty($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : '';
683 if ( $filter) {
684 // Normalize input: lowercase and remove punctuation.
685 $normalized = preg_replace('/[^\p{L}\p{N}\s,]/u', '', strtolower($filter));
686
687 // Split by commas or spaces.
688 $keywords = preg_split('/[\s,]+/', $normalized, -1, PREG_SPLIT_NO_EMPTY);
689
690 $data = array_values(array_filter($data, function ($item) use ($keywords) {
691
692 // Combine searchable fields.
693 $haystack = strtolower(
694 (!empty($item['title']) ? $item['title'] : '') . ' ' .
695 (!empty($item['description']) ? $item['description'] : '') . ' ' .
696 (!empty($item['alt']) ? $item['alt'] : '') . ' ' .
697 (!empty($item['caption']) ? $item['caption'] : '')
698 );
699
700 // ANY keyword can match.
701 foreach ($keywords as $keyword) {
702 if (preg_match('/\b' . preg_quote($keyword, '/') . '\b/u', $haystack)) {
703 return true;
704 }
705 }
706
707 return false;
708 }));
709 }
710
711 // Images count after filter.
712 $all_images_count = count($data);
713
714 // Order the data.
715 if ( in_array($order_by, array('title', 'caption', 'description', 'date')) ) {
716 // For ascending order.
717 usort($data, function($a, $b) use ($order_by) {
718 return strtolower($a[$order_by]) <=> strtolower($b[$order_by]);
719 });
720 }
721
722 // For descending order.
723 if ( $order == 'desc' ) {
724 $data = array_reverse($data);
725 }
726
727 $per_page = '';
728 $views_with_pagination = ['thumbnails', 'mosaic', 'masonry', 'justified', 'blog'];
729 if ( !empty($gallery_options['general']['itemsPerPage'])
730 && in_array($gallery_options['type'], $views_with_pagination)
731 && !empty($gallery_options[$gallery_options['type']]['paginationType'])
732 && $gallery_options[$gallery_options['type']]['paginationType'] !== 'none' ) {
733 $per_page = sanitize_text_field($gallery_options['general']['itemsPerPage']);
734 }
735 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required.
736 elseif ( !empty($_GET['per_page']) ) {
737 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required.
738 $per_page = sanitize_text_field(wp_unslash($_GET['per_page']));
739 }
740 // Run pagination on the data.
741 if ( !empty($per_page) ) {
742 $per_page = (int) $per_page;
743 // We need one of these two parameters (page or offset, where offset is at which element to start).
744 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required.
745 if ( isset($_GET['page']) ) {
746 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required.
747 $page = $_GET['page'] > 1 ? (int) $_GET['page'] : 1;
748 $offset = ($page - 1) * $per_page;
749 }
750 else {
751 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required.
752 $offset = isset($_GET['offset']) && $_GET['offset'] < count($data) ? (int) $_GET['offset'] : 0;
753 }
754 $data = array_slice($data, $offset, $per_page);
755 }
756 }
757
758 return ['images' => $data, 'count' => $all_images_count];
759 }
760
761 /**
762 * Return the product formatted price based on WooCommerce option.
763 *
764 * @param $product
765 *
766 * @return string
767 */
768 private function get_product_price($product) {
769 $decimals = wc_get_price_decimals();
770 $price = number_format( $product->get_price(), $decimals, wc_get_price_decimal_separator(), wc_get_price_thousand_separator() );
771 $currency = get_woocommerce_currency();
772 $currency_symbol = get_woocommerce_currency_symbol( $currency );
773 $currency_pos = get_option( 'woocommerce_currency_pos' );
774
775 // Format price based on position.
776 switch ( $currency_pos ) {
777 case 'left':
778 $formatted_price = $currency_symbol . $price;
779 break;
780 case 'right':
781 $formatted_price = $price . $currency_symbol;
782 break;
783 case 'left_space':
784 $formatted_price = $currency_symbol . ' ' . $price;
785 break;
786 case 'right_space':
787 $formatted_price = $price . ' ' . $currency_symbol;
788 break;
789 default:
790 $formatted_price = $price . ' ' . $currency_symbol;
791 break;
792 }
793
794 return $formatted_price;
795 }
796
797 /**
798 * Return the Action URL depends on type.
799 *
800 * @param $type
801 * @param $post
802 *
803 * @return array
804 */
805 private function get_item_extra_data($type, $post) {
806 $data = [
807 'title' => '',
808 'caption' => '',
809 'author' => '',
810 'date_created' => '',
811 'exif' => '',
812 'action_url' => '',
813 'item_url' => '',
814 'checkout_url' => '',
815 'price' => '',
816 ];
817 switch ($type) {
818 case "image":
819 case "video": {
820 $data['title'] = html_entity_decode(get_the_title($post->ID));
821 $data['caption'] = html_entity_decode(wp_get_attachment_caption($post->ID));
822 $data['author'] = html_entity_decode(get_the_author_meta('display_name', get_post_field('post_author', $post->ID)));
823 $data['date_created'] = html_entity_decode(get_the_date( '', $post->ID ));
824 $data['exif'] = html_entity_decode(get_post_meta($post->ID, 'exif', TRUE));
825
826 if ( $type === 'image' ) {
827 $metadata = wp_get_attachment_metadata($post->ID);
828 if ( !empty($metadata['image_meta']) ) {
829 if ( !empty($metadata['image_meta']['credit']) ) {
830 $data['author'] = html_entity_decode($metadata['image_meta']['credit']);
831 }
832 if ( !empty($metadata['image_meta']['created_timestamp']) ) {
833 $data['date_created'] = html_entity_decode(date_i18n(get_option('date_format'), (int) $metadata['image_meta']['created_timestamp']));
834 }
835 if ( empty($data['exif'])) {
836 $data['exif'] = html_entity_decode($this->get_exif($metadata['image_meta']));
837 }
838 }
839 }
840
841 $data['action_url'] = esc_url_raw(get_post_meta($post->ID, 'action_url', TRUE));
842 $data['item_url'] = esc_url_raw(get_attachment_link($post->ID));
843 break;
844 }
845 case "post":
846 case "page":
847 case "product": {
848 $data['title'] = html_entity_decode(get_the_title($post->ID));
849 $data['caption'] = html_entity_decode(wp_kses_post(strip_shortcodes(wp_strip_all_tags($post->post_excerpt))));
850 $data['author'] = html_entity_decode(get_the_author_meta('display_name', get_post_field('post_author', $post->ID)));
851 $data['date_created'] = html_entity_decode(get_the_date( '', $post->ID ));
852 $data['action_url'] = esc_url_raw(get_permalink($post->ID));
853 $data['item_url'] = $data['action_url'];
854 if ( $type === 'product' && $this->obj->woocommerce_is_active ) {
855 $product = wc_get_product( $post->ID );
856 if ( $product && $product->is_type( 'simple' ) ) {
857 $data['checkout_url'] = esc_url_raw(add_query_arg('add-to-cart', $post->ID, wc_get_cart_url()));
858 if ( $product->get_price() ) {
859 $data['price'] = html_entity_decode($this->get_product_price($product));
860 }
861 }
862 }
863 break;
864 }
865 }
866
867 return $data;
868 }
869
870 /**
871 * Get posts.
872 *
873 * @param $gallery_id
874 * @param $post_type
875 * @param $order_by
876 * @param $order
877 *
878 * @return array
879 */
880 private function get_posts($gallery_id, $post_type, $order_by, $order) {
881 $args = [
882 'post_type' => $post_type,
883 'posts_per_page' => -1,
884 ];
885
886 // To match gallery order by values to posts order by values.
887 $args['orderby'] = str_replace(['default', 'caption', 'description'], ['ID', 'name', 'ID'], $order_by);
888 $args['order'] = $order;
889
890 $additional_data = get_post_meta( $gallery_id, 'additional_data', TRUE );
891 $additional_data_arr = [];
892 if ( !empty($additional_data) ) {
893 $additional_data_arr = json_decode($additional_data, TRUE);
894 $tax_query = [
895 'relation' => !empty($additional_data_arr['relation']) && $additional_data_arr['relation'] == "and" ? 'AND' : 'OR',
896 ];
897 foreach ( $additional_data_arr['taxonomies'] as $taxonomy_string ) {
898 $term = explode(':', $taxonomy_string);
899 if ( count($term) === 2 ) {
900 $tax_query[] = [
901 'taxonomy' => $term[0],
902 'field' => 'term_id',
903 'terms' => [ (int) $term[1] ],
904 ];
905 }
906 }
907 if ( count($tax_query) > 1 ) {
908 // Make sure at least one condition exists.
909 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Tax filtering required for gallery feature.
910 $args['tax_query'] = $tax_query;
911 }
912 if ( !empty($additional_data_arr['exclude']) ) {
913 // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Exclusion list provided by user settings.
914 $args['post__not_in'] = $additional_data_arr['exclude'];
915 }
916 if ( !empty($additional_data_arr['exclude_without_image']) ) {
917 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required to filter posts with featured image.
918 $args['meta_query'] = [
919 'relation' => 'AND',
920 [
921 'key' => '_thumbnail_id',
922 'value' => '',
923 'compare' => '!=',
924 ],
925 [
926 'key' => '_thumbnail_id',
927 'compare' => 'EXISTS',
928 ],
929 ];
930 }
931 if ( !empty($additional_data_arr['count']) ) {
932 $args['posts_per_page'] = intval($additional_data_arr['count']);
933 }
934 }
935
936 return ['posts' => get_posts( $args ), 'additional_data' => $additional_data_arr];
937 }
938
939 /**
940 * Save images.
941 *
942 * @return void
943 */
944 public function save_images() {
945 if ( !REACGLibrary::verify_nonce() ) {
946 wp_die();
947 }
948
949 if ( isset($_POST['post_id']) && isset($_POST['images_ids']) ) {
950 $post_id = (int) $_POST['post_id'];
951 if ( !current_user_can('edit_post', $post_id) ) {
952 wp_die();
953 }
954
955 $images_ids = sanitize_text_field(wp_unslash($_POST['images_ids']));
956 update_post_meta($post_id, 'images_ids', $images_ids);
957
958 if ( isset($_POST['additional_data']) ) {
959 $additional_data = sanitize_text_field(wp_unslash($_POST['additional_data']));
960 update_post_meta($post_id, 'additional_data', $additional_data);
961 }
962
963 /* Update the gallery timestamp on images save to prevent data from being read from the cache.*/
964 if ( isset($_POST['gallery_timestamp']) ) {
965 $timestamp = sanitize_text_field(wp_unslash($_POST['gallery_timestamp']));
966 update_post_meta($post_id, 'gallery_timestamp', $timestamp);
967 }
968
969 $images_ids_arr = !empty($images_ids) ? json_decode($images_ids, TRUE) : [];
970 $images_count = count($images_ids_arr);
971 update_post_meta($post_id, 'images_count', $images_count);
972 }
973
974 wp_die();
975 }
976
977 /**
978 * Save thumbnail.
979 *
980 * @return void
981 */
982 public function save_thumbnail() {
983 if ( !REACGLibrary::verify_nonce() ) {
984 wp_die();
985 }
986
987 if ( isset($_POST['id']) && isset($_POST['thumbnail_id']) ) {
988 $id = (int) $_POST['id'];
989 $thumbnail_id = (int) $_POST['thumbnail_id'];
990
991 $metadata = wp_get_attachment_metadata($id);
992 $metadata['thumbnail_id'] = $thumbnail_id;
993 wp_update_attachment_metadata( $id, $metadata );
994 }
995
996 wp_die();
997 }
998
999 /**
1000 * Delete thumbnail.
1001 *
1002 * @return void
1003 */
1004 public function delete_thumbnail() {
1005 if ( !REACGLibrary::verify_nonce() ) {
1006 wp_die();
1007 }
1008
1009 if ( isset($_POST['id']) ) {
1010 $id = (int) $_POST['id'];
1011
1012 $metadata = wp_get_attachment_metadata($id);
1013 unset($metadata['thumbnail_id']);
1014 wp_update_attachment_metadata( $id, $metadata );
1015 }
1016
1017 wp_die();
1018 }
1019
1020 /**
1021 * Upload a browser-generated cover image and assign it to the video attachment.
1022 *
1023 * @return void
1024 */
1025 public function upload_video_cover() {
1026 if ( !REACGLibrary::verify_nonce() ) {
1027 wp_die();
1028 }
1029
1030 $video_id = isset($_POST['video_id']) ? (int) $_POST['video_id'] : 0;
1031 if ( $video_id <= 0 || !wp_attachment_is('video', $video_id) ) {
1032 wp_send_json_error([ 'message' => __('Invalid video attachment.', 'regallery') ], 400);
1033 }
1034
1035 if ( !current_user_can('edit_post', $video_id) || !current_user_can('upload_files') ) {
1036 wp_send_json_error([ 'message' => __('You are not allowed to upload video covers.', 'regallery') ], 403);
1037 }
1038
1039 $metadata = wp_get_attachment_metadata($video_id);
1040 // If the video already has a thumbnail, return it without uploading a new one.
1041 if ( !empty($metadata['thumbnail_id']) && get_post((int) $metadata['thumbnail_id']) ) {
1042 $thumbnail_url = wp_get_attachment_image_url((int) $metadata['thumbnail_id'], 'thumbnail');
1043 if ( empty($thumbnail_url) ) {
1044 $thumbnail_url = wp_get_attachment_url((int) $metadata['thumbnail_id']);
1045 }
1046
1047 wp_send_json_success([
1048 'thumbnail_id' => (int) $metadata['thumbnail_id'],
1049 'thumbnail_url' => $thumbnail_url,
1050 ]);
1051 }
1052
1053 if ( empty($_FILES['cover']) || empty($_FILES['cover']['tmp_name']) ) {
1054 wp_send_json_error([ 'message' => __('No cover image was uploaded.', 'regallery') ], 400);
1055 }
1056
1057 require_once ABSPATH . 'wp-admin/includes/file.php';
1058 require_once ABSPATH . 'wp-admin/includes/image.php';
1059 require_once ABSPATH . 'wp-admin/includes/media.php';
1060
1061 $filename = !empty($_POST['filename'])
1062 ? sanitize_file_name(wp_unslash($_POST['filename']))
1063 : 'video-' . $video_id . '-cover.jpg';
1064 $_FILES['cover']['name'] = $filename;
1065
1066 $attachment_id = media_handle_upload(
1067 'cover',
1068 $video_id,
1069 [
1070 'post_title' => sanitize_text_field(pathinfo($filename, PATHINFO_FILENAME)),
1071 'post_status' => 'inherit',
1072 ],
1073 [
1074 'test_form' => FALSE,
1075 'mimes' => [
1076 'jpg|jpeg' => 'image/jpeg',
1077 'png' => 'image/png',
1078 'webp' => 'image/webp',
1079 ],
1080 ]
1081 );
1082
1083 if ( is_wp_error($attachment_id) ) {
1084 wp_send_json_error([ 'message' => $attachment_id->get_error_message() ], 500);
1085 }
1086
1087 if ( !is_array($metadata) ) {
1088 $metadata = [];
1089 }
1090 $metadata['thumbnail_id'] = (int) $attachment_id;
1091 wp_update_attachment_metadata($video_id, $metadata);
1092
1093 $thumbnail_url = wp_get_attachment_image_url($attachment_id, 'thumbnail');
1094 if ( empty($thumbnail_url) ) {
1095 $thumbnail_url = wp_get_attachment_url($attachment_id);
1096 }
1097
1098 wp_send_json_success([
1099 'thumbnail_id' => (int) $attachment_id,
1100 'thumbnail_url' => $thumbnail_url,
1101 ]);
1102 }
1103
1104 /**
1105 * Register an ajax action to save a gallery (need for builders).
1106 *
1107 * @return void
1108 */
1109 public function save_gallery() {
1110 if ( !REACGLibrary::verify_nonce() ) {
1111 wp_die();
1112 }
1113 $new_post = [
1114 'post_title' => !empty($_GET['gallery_title']) ? sanitize_text_field(wp_unslash($_GET['gallery_title'])) : '(no title)',
1115 'post_status' => 'publish',
1116 'post_type' => 'reacg',
1117 ];
1118 $post_id = wp_insert_post($new_post, TRUE);
1119 if ( $post_id ) {
1120 wp_update_post([
1121 'ID' => $post_id,
1122 'post_content' => REACGLibrary::get_shortcode($this->obj, $post_id),
1123 ]);
1124 }
1125
1126 echo json_encode($post_id);
1127 wp_die();
1128 }
1129
1130 /**
1131 * Save the metadata on saving the custom post and insert the shortcode as a content.
1132 *
1133 * @param $post_id
1134 * @param $post
1135 *
1136 * @return void
1137 */
1138 public function save_post($post_id, $post) {
1139 if ( is_null($post) || 'reacg' !== $post->post_type ) {
1140 return;
1141 }
1142
1143 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is not required.
1144 if ( isset($_POST['images_ids']) ) {
1145 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is not required.
1146 $images_ids = sanitize_text_field(wp_unslash($_POST['images_ids']));
1147 update_post_meta($post_id, 'images_ids', $images_ids);
1148
1149 // Save images count as a separate meta to make it possible to order galleries by images count.
1150 $images_ids_arr = json_decode($images_ids, TRUE);
1151 $images_count = is_array($images_ids_arr) ? count($images_ids_arr) : 0;
1152 update_post_meta($post_id, 'images_count', $images_count);
1153 }
1154
1155 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is not required.
1156 if ( isset($_POST['additional_data']) ) {
1157 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is not required.
1158 $additional_data = sanitize_text_field(wp_unslash($_POST['additional_data']));
1159 update_post_meta($post_id, 'additional_data', $additional_data);
1160 }
1161
1162 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is not required.
1163 if ( isset($_POST['custom_css']) ) {
1164 $options = get_option('reacg_options' . $post_id, FALSE);
1165 if ( !empty($options) ) {
1166 $data = json_decode($options, TRUE);
1167 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is not required.
1168 $data['custom_css'] = sanitize_text_field(wp_unslash($_POST['custom_css']));
1169 $data['custom_css'] = preg_replace('/\s+/', ' ', $data['custom_css']);
1170 update_option('reacg_options' . $post_id, json_encode($data));
1171 }
1172 }
1173
1174 remove_action( 'save_post', [ $this, 'save_post' ] );
1175 // Save the shortcode as the post content.
1176 wp_update_post([
1177 'ID' => $post_id,
1178 'post_title' => sanitize_text_field($post->post_title),
1179 'post_content' => REACGLibrary::get_shortcode($this->obj, $post_id),
1180 ]);
1181 add_action( 'save_post', [ $this, 'save_post' ], 10, 2 );
1182 }
1183
1184 /**
1185 * Delete the post metas and options on the post delete.
1186 *
1187 * @param $post_id
1188 * @param $post
1189 *
1190 * @return void
1191 */
1192 public function delete_post($post_id, $post) {
1193 if ( is_null($post) || 'reacg' !== $post->post_type ) {
1194 return;
1195 }
1196
1197 // Delete the post metas.
1198 delete_post_meta($post_id, 'images_ids');
1199 delete_post_meta($post_id, 'additional_data');
1200 delete_post_meta($post_id, 'gallery_timestamp');
1201 delete_post_meta($post_id, 'options_timestamp');
1202 delete_post_meta($post_id, 'images_count');
1203
1204 // Delete all options connected with the gallery.
1205 require_once REACG()->plugin_dir . "/includes/options.php";
1206 new REACG_Options(FALSE, $post_id);
1207 }
1208
1209 /**
1210 * Register custom post.
1211 *
1212 * @return void
1213 */
1214 private function register_post_type() {
1215 $args = array(
1216 'label' => $this->obj->nicename,
1217 'labels' => array(
1218 'add_new' => __('Add New Gallery', 'regallery'),
1219 'add_new_item' => __('Add New Gallery', 'regallery'),
1220 'edit_item' => __('Edit Gallery', 'regallery'),
1221 'new_item' => __('New Gallery', 'regallery'),
1222 'view_item' => __('View Gallery', 'regallery'),
1223 'view_items' => __('View Galleries', 'regallery'),
1224 'search_items' => __('Search Galleries', 'regallery'),
1225 'not_found' => __('No galleries found', 'regallery'),
1226 'not_found_in_trash' => __('No galleries found in Trash', 'regallery'),
1227 'all_items' => __('All Galleries', 'regallery'),
1228 'filter_items_list' => __('Filter galleries list', 'regallery'),
1229 'items_list_navigation' => __('Galleries list navigation', 'regallery'),
1230 'items_list' => __('Galleries list', 'regallery'),
1231 'archives' => __('Gallery Archives', 'regallery'),
1232 'attributes' => __('Gallery Attributes', 'regallery'),
1233 'insert_into_item' => __('Insert into gallery', 'regallery'),
1234 'uploaded_to_this_item' => __('Uploaded to this gallery', 'regallery'),
1235 'item_published' => __('Gallery published.', 'regallery'),
1236 'item_published_privately' => __('Gallery published privately.', 'regallery'),
1237 'item_reverted_to_draft' => __('Gallery reverted to draft.', 'regallery'),
1238 'item_trashed' => __('Gallery trashed.', 'regallery'),
1239 'item_scheduled' => __('Gallery scheduled.', 'regallery'),
1240 'item_updated' => __('Gallery updated.', 'regallery'),
1241 'item_link' => __('Gallery Link', 'regallery'),
1242 'item_link_description' => __('A link to a gallery', 'regallery'),
1243 ),
1244 'public' => TRUE,
1245 'exclude_from_search' => TRUE,
1246 'publicly_queryable' => TRUE,
1247 'show_ui' => TRUE,
1248 'show_in_menu' => TRUE,
1249 'show_in_nav_menus' => TRUE,
1250 'permalink_epmask' => TRUE,
1251 'rewrite' => TRUE,
1252 // Editor is not used, but added to avoid a bug connected with preview. It is hidden with CSS (admin.css)
1253 'supports' => array('title', 'editor'),
1254 );
1255 register_post_type( REACG_CUSTOM_POST_TYPE, $args );
1256 add_action( 'add_meta_boxes_reacg', [ $this, 'add_meta_boxes' ], 1 );
1257 }
1258
1259 /**
1260 * Add metaboxes to the post type.
1261 *
1262 * @param $post
1263 *
1264 * @return void
1265 */
1266 public function add_meta_boxes($post) {
1267 if ( 'reacg' !== $post->post_type ) {
1268 return;
1269 }
1270
1271 wp_enqueue_media();
1272 wp_enqueue_script($this->obj->prefix . '_admin');
1273 wp_enqueue_style($this->obj->prefix . '_admin');
1274
1275 // Remove all unnecessary metaboxes from the post type screen.
1276 $this->remove_all_the_metaboxes();
1277
1278 // Metabox for adding images.
1279 add_meta_box( 'gallery-images', __( 'Media', 'regallery' ), [ $this, 'meta_box_images' ], 'reacg', 'normal', 'high' );
1280 add_meta_box( 'gallery-settings', __( 'Settings', 'regallery' ), [ $this, 'meta_box_settings' ], 'reacg', 'normal', 'high' );
1281 add_meta_box( 'gallery-preview', __( 'Preview', 'regallery' ), [ $this, 'meta_box_preview' ], 'reacg', 'normal', 'low' );
1282
1283 if ( !REACG_PLAYGROUND ) {
1284 // Metabox to activate/deactivate pro version.
1285 add_meta_box('gallery-license', __('License', 'regallery'), [
1286 $this,
1287 'meta_box_license'
1288 ], 'reacg', 'side', 'low');
1289 }
1290
1291 // Metabox to display the available publishing methods.
1292 add_meta_box( 'gallery-help', __( 'Help', 'regallery' ), [ $this, 'meta_box_help' ], 'reacg', 'side', 'low' );
1293
1294 add_meta_box( 'gallery-custom-css', __('Custom CSS', 'regallery') . wp_kses( REACGLibrary::$pro_icon, REACGLibrary::allowed_svg() ), [$this, 'meta_box_custom_css'], 'reacg', 'side', 'low' );
1295
1296 add_meta_box( 'reacg-metabox-widget-main-features', __( 'Main features', 'regallery' ), [ $this, 'widget_main_features' ], 'reacg', 'side', 'low' );
1297 add_meta_box( 'reacg-metabox-widget-why-upgrade', __( 'Why upgrade', 'regallery' ), [ $this, 'render_metabox_widget_why_upgrade' ], 'reacg', 'side', 'low' );
1298
1299 $this->move_yoast_metabox_to_end();
1300 }
1301
1302 /**
1303 * Move the Yoast SEO metabox to the bottom of the main column.
1304 *
1305 * @return void
1306 */
1307 private function move_yoast_metabox_to_end() {
1308 global $wp_meta_boxes;
1309
1310 if ( empty( $wp_meta_boxes[ REACG_CUSTOM_POST_TYPE ] ) ) {
1311 return;
1312 }
1313
1314 $yoast_metabox = NULL;
1315 $contexts = [ 'normal', 'advanced', 'side' ];
1316 $priorities = [ 'high', 'core', 'default', 'low', 'sorted' ];
1317
1318 foreach ( $contexts as $context ) {
1319 foreach ( $priorities as $priority ) {
1320 if ( empty( $wp_meta_boxes[ REACG_CUSTOM_POST_TYPE ][ $context ][ $priority ]['wpseo_meta'] ) ) {
1321 continue;
1322 }
1323
1324 $yoast_metabox = $wp_meta_boxes[ REACG_CUSTOM_POST_TYPE ][ $context ][ $priority ]['wpseo_meta'];
1325 unset( $wp_meta_boxes[ REACG_CUSTOM_POST_TYPE ][ $context ][ $priority ]['wpseo_meta'] );
1326 }
1327 }
1328
1329 if ( !empty( $yoast_metabox ) ) {
1330 $wp_meta_boxes[ REACG_CUSTOM_POST_TYPE ]['normal']['low']['wpseo_meta'] = $yoast_metabox;
1331 }
1332 }
1333
1334 public function register_dashboard_widgets() {
1335 wp_add_dashboard_widget( 'reacg-dashboard-widget-main-features', __( 'Main features', 'regallery' ), [ $this, 'render_dashboard_widget_main_features' ] );
1336 wp_add_dashboard_widget( 'reacg-dashboard-widget-why-upgrade', __( 'Why upgrade', 'regallery' ), [ $this, 'render_dashboard_widget_why_upgrade' ] );
1337 }
1338
1339 public function render_dashboard_widget_main_features() {
1340 $links = [
1341 [
1342 'title' => __('Blog', 'regallery'),
1343 'url' => add_query_arg(['utm_medium' => 'dashboard', 'utm_campaign' => 'gallery_layouts'], REACG_BLOG_URL_UTM),
1344 ],
1345 [
1346 'title' => __('Gallery layouts', 'regallery'),
1347 'url' => add_query_arg(['utm_medium' => 'dashboard', 'utm_campaign' => 'gallery_layouts'], REACG_WEBSITE_URL_UTM . '#gallery_layouts'),
1348 ],
1349 [
1350 'title' => __('Support', 'regallery'),
1351 'url' => REACG_WP_PLUGIN_SUPPORT_URL,
1352 ],
1353 [
1354 'id' => 'reacg-dashboard-upgrade-link',
1355 'title' => __('Upgrade', 'regallery'),
1356 'url' => add_query_arg(['utm_medium' => 'dashboard', 'utm_campaign' => 'upgrade'], REACG_PRICING_URL_UTM),
1357 ],
1358 ];
1359 ?>
1360 <div class="reacg-dashboard-widget">
1361 <?php $this->widget_main_features(); ?>
1362 <div class="reacg-dashboard-widget-footer">
1363 <?php
1364 foreach ( $links as $link ) {
1365 ?>
1366 <a <?php if (!empty($link["id"])) { ?>id="<?php echo esc_attr($link["id"]); ?>"<?php } ?> href="<?php echo esc_url($link['url']); ?>" target="_blank">
1367 <?php echo esc_html($link['title']); ?>
1368 <span class="screen-reader-text"> (opens in a new tab)</span>
1369 <span class="dashicons dashicons-external"></span>
1370 </a>
1371 <?php
1372 }
1373 ?>
1374 </div>
1375 </div>
1376 <?php
1377 }
1378
1379 public function render_metabox_widget_why_upgrade() {
1380 $this->widget_why_upgrade( 'metabox' );
1381 }
1382 public function render_dashboard_widget_why_upgrade() {
1383 $this->widget_why_upgrade( 'dashboard' );
1384 }
1385
1386 public function meta_box_preview($post) {
1387 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentional output of trusted HTML and JS generated by the plugin.
1388 echo REACGLibrary::get_rest_routs($post->ID);
1389 }
1390
1391 /**
1392 * Display the available publishing methods.
1393 *
1394 * @param $post
1395 *
1396 * @return void
1397 */
1398 public function meta_box_help( $post ) {
1399 $available_builders = [];
1400 if ( function_exists( 'register_block_type' ) ) {
1401 $available_builders['gutenberg'] = [
1402 'title' => __('Gutenberg Block', 'regallery'),
1403 'video' => 'https://www.youtube.com/watch?v=6AVPJ2fjaYs',
1404 ];
1405 }
1406 if ( class_exists( '\Elementor\Plugin' ) ) {
1407 $available_builders['elementor'] = [
1408 'title' => __('Elementor Widget', 'regallery'),
1409 'video' => 'https://www.youtube.com/watch?v=GedxyRxQ02A',
1410 ];
1411 }
1412 if ( class_exists( 'ET_Builder_Module' ) ) {
1413 $available_builders['divi'] = [
1414 'title' => __('Divi Builder Module', 'regallery'),
1415 'video' => 'https://www.youtube.com/watch?v=Z69eZOoWJi0',
1416 ];
1417 }
1418 if ( function_exists( 'vc_map' ) ) {
1419 $available_builders['wpbakery'] = [
1420 'title' => __('WPBakery Builder Element', 'regallery'),
1421 'video' => 'https://www.youtube.com/watch?v=FClpKpREzPQ',
1422 ];
1423 }
1424 if ( class_exists( 'FLBuilder' ) ) {
1425 $available_builders['beaverbuilder'] = [
1426 'title' => __('Beaver Builder Module', 'regallery'),
1427 'video' => 'https://www.youtube.com/watch?v=A5U2ghLKYNg',
1428 ];
1429 }
1430 if ( class_exists( '\Bricks\Elements' ) ) {
1431 $available_builders['bricks'] = [
1432 'title' => __('Bricks Builder Element', 'regallery'),
1433 'video' => 'https://www.youtube.com/watch?v=aiYdYAn1D_8',
1434 ];
1435 }
1436 if ( !empty($available_builders) ) {
1437 ?>
1438 <p>
1439 <?php esc_html_e( 'You can insert the gallery into a post or page using:', 'regallery' ); ?>
1440 </p>
1441 <ul>
1442 <?php
1443 foreach ( $available_builders as $builder ) {
1444 ?>
1445 <li>
1446 <?php
1447 if ( !empty($builder['video']) ) {
1448 ?>
1449 <a href="<?php echo esc_url($builder['video']); ?>" target="_blank" title="<?php esc_html_e( 'How to', 'regallery' ); ?>">
1450 <?php
1451 }
1452 ?>
1453 <strong><?php echo esc_html($builder['title']); ?></strong>
1454 <?php
1455 if ( !empty($builder['video']) ) {
1456 ?>
1457 </a>
1458 <?php
1459 }
1460 ?>
1461 </li>
1462 <?php
1463 }
1464 ?>
1465 </ul>
1466 <?php esc_html_e( 'Or just paste the shortcode into any builder:', 'regallery' ); ?>
1467 <?php
1468 }
1469 else {
1470 esc_html_e( 'Paste the shortcode into a post or page to show the gallery:', 'regallery' );
1471 }
1472 ?>
1473 <p class="reacg_shortcode">
1474 <code><?php echo esc_html(REACGLibrary::get_shortcode($this->obj, $post->ID)); ?></code>
1475 </p>
1476 <div class="reacg_help_icon">
1477 <svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" viewBox="0 0 24 24">
1478 <path d="M12,19c-.829,0-1.5-.672-1.5-1.5,0-1.938,1.352-3.709,3.909-5.118,1.905-1.05,2.891-3.131,2.51-5.301-.352-2.003-1.997-3.648-4-4-1.445-.254-2.865,.092-4.001,.974-1.115,.867-1.816,2.164-1.922,3.559-.063,.825-.785,1.445-1.609,1.382-.826-.063-1.445-.783-1.382-1.609,.17-2.237,1.29-4.315,3.073-5.7C8.89,.278,11.149-.275,13.437,.126c3.224,.566,5.871,3.213,6.437,6.437,.597,3.399-1.018,6.794-4.017,8.447-1.476,.813-2.357,1.744-2.357,2.49,0,.828-.671,1.5-1.5,1.5Zm-1.5,3.5c0,.828,.672,1.5,1.5,1.5s1.5-.672,1.5-1.5-.672-1.5-1.5-1.5-1.5,.672-1.5,1.5Z"/>
1479 </svg>
1480 </div>
1481 <?php
1482 }
1483
1484 /**
1485 * Add meta box to activate/deactivate Pro version.
1486 *
1487 * @return void
1488 */
1489 public function meta_box_license() {
1490 ?>
1491 <div class="reacg-pro-not-active" style="display: none;">
1492 <p>
1493 <?php
1494 /* translators: %s: plugin name */
1495 echo sprintf(esc_html__( "You're using the free version of %s - no license needed. Enjoy!", 'regallery' ), esc_html(REACG_NICENAME));
1496 ?>
1497 🙂
1498 </p>
1499 <p>
1500 <?php
1501 /* translators: %s: HTML link to upgrade to PRO wrapped in strong tags */
1502 echo sprintf(esc_html__( "To unlock more features consider %s.", 'regallery' ), '<strong><a href="' . esc_url(add_query_arg(['utm_medium' => 'license', 'utm_campaign' => 'upgrade'], REACG_PRICING_URL_UTM)) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('upgrading to PRO', 'regallery') . '</a></strong>');
1503 ?>
1504 </p>
1505 <label for="reacg-license-key">
1506 <p class="description">
1507 <?php
1508 /* translators: %s: plugin name */
1509 echo sprintf(esc_html__( "Already purchased? Simply enter your license key below to activate %s PRO!", 'regallery' ), esc_html(REACG_NICENAME));
1510 ?>
1511 </p>
1512 </label>
1513 <input placeholder="<?php esc_html_e( "Paste license key here", 'regallery' ); ?>" type="text" id="reacg-license-key" class="reacg-license-key" />
1514 <p class="reacg-error description hidden"></p>
1515 <div class="reacg-activate-action">
1516 <span class="spinner"></span>
1517 <button type="button" class="button button-primary button-large reacg-primary-button reacg-license-activate-button" data-activate="true">
1518 <?php esc_html_e( "Activate Pro version", 'regallery' ); ?>
1519 </button>
1520 </div>
1521 </div>
1522 <div class="reacg-pro-active" style="display: none;">
1523 <p class="reacg-success">
1524 <?php
1525 /* translators: %s: plugin name */
1526 echo sprintf(esc_html__( "%s PRO version is active!", 'regallery' ), esc_html(REACG_NICENAME));
1527 ?>
1528 </p>
1529 <div class="reacg-activate-action">
1530 <span class="spinner"></span>
1531 <button type="button" class="button button-secondary button-large reacg-license-activate-button" data-activate="false">
1532 <?php esc_html_e( "Deactivate Pro version", 'regallery' ); ?>
1533 </button>
1534 </div>
1535 </div>
1536 <?php
1537 }
1538
1539 /**
1540 * Add meta box for adding Custom CSS.
1541 *
1542 * @param $post
1543 *
1544 * @return void
1545 */
1546 public function meta_box_custom_css($post) {
1547 $css = "";
1548 $options = get_option('reacg_options' . $post->ID, FALSE);
1549 if ( !empty($options) ) {
1550 $data = json_decode($options, TRUE);
1551 if ( !empty($data['custom_css']) ) {
1552 $css = $data['custom_css'];
1553 $css = preg_replace('/;/', ";\n ", $css); // Add newline after each semicolon
1554 $css = preg_replace('/{/', " {\n ", $css); // Add newline and indentation after each opening brace
1555 $css = preg_replace('/ }/', "}\n", $css); // Add newline before and after each closing brace
1556 }
1557 }
1558 ?>
1559 <textarea name="custom_css" rows="20"><?php echo esc_attr($css); ?></textarea>
1560 <?php
1561 }
1562
1563 /**
1564 * Return the given image all size URLs.
1565 *
1566 * @param $id
1567 *
1568 * @return
1569 */
1570 private function get_image_urls($id) {
1571 if ( $id === 0 ) {
1572 $no_image = [
1573 'url' => $this->obj->plugin_url . $this->obj->no_image,
1574 'width' => 700,
1575 'height' => 700,
1576 ];
1577 return [
1578 'original' => $no_image,
1579 'sizes' => [$no_image],
1580 ];
1581
1582 }
1583
1584 $url = wp_get_attachment_url($id);
1585 $meta = wp_get_attachment_metadata($id);
1586
1587 if ( !$url || !$meta ) {
1588 return FALSE;
1589 }
1590
1591 $basename = basename($url);
1592 $original = [
1593 'url' => str_replace($basename, urlencode($basename), $url),
1594 'width' => !empty($meta['width']) ? $meta['width'] : 0,
1595 'height' => !empty($meta['height']) ? $meta['height'] : 0,
1596 ];
1597
1598 $sizes = [];
1599 $seen = [];
1600
1601 // Get actual generated sizes for this image
1602 $meta_sizes = !empty($meta['sizes']) ? $meta['sizes'] : [];
1603
1604 // Loop only through sizes that actually exist
1605 foreach ($meta_sizes as $size_name => $data) {
1606 if (empty($data['file']) || empty($data['width']) || empty($data['height'])) {
1607 continue;
1608 }
1609
1610 if ($this->is_cropped_generated_size($data, $original)) {
1611 continue;
1612 }
1613
1614 if ($data['width'] > $this->default_max_dimension
1615 || $data['height'] > $this->default_max_dimension) {
1616 continue;
1617 }
1618
1619 $data_url = str_replace($basename, urlencode($data['file']), $url);
1620
1621 // Avoid duplicates
1622 if (in_array($data_url, $seen, true)) {
1623 continue;
1624 }
1625
1626 $seen[] = $data_url;
1627
1628 $sizes[] = [
1629 'url' => $data_url,
1630 'width' => $data['width'],
1631 'height' => $data['height'],
1632 ];
1633 }
1634
1635 if ( ($original['width'] <= $this->default_max_dimension
1636 && $original['height'] <= $this->default_max_dimension
1637 && !in_array($original['url'], $seen, true) ) || empty($sizes) ) {
1638 // Include original image if it doesn't exceed the default max dimensions and is not already included as a generated size, or if no other sizes are available.
1639 $sizes[] = $original;
1640 }
1641
1642 usort($sizes, function($a, $b) {
1643 return (int) $a['width'] <=> (int) $b['width'];
1644 });
1645
1646 return [
1647 'original' => $original,
1648 'sizes' => $sizes,
1649 ];
1650 }
1651
1652 private function is_cropped_generated_size($size, $original) {
1653 $size_width = !empty($size['width']) ? (int) $size['width'] : 0;
1654 $size_height = !empty($size['height']) ? (int) $size['height'] : 0;
1655 $original_width = !empty($original['width']) ? (int) $original['width'] : 0;
1656 $original_height = !empty($original['height']) ? (int) $original['height'] : 0;
1657
1658 if ($size_width <= 0 || $size_height <= 0 || $original_width <= 0 || $original_height <= 0) {
1659 return false;
1660 }
1661
1662 $size_ratio = $size_width / $size_height;
1663 $original_ratio = $original_width / $original_height;
1664
1665 // Allow a tiny difference to account for WordPress rounding during resize.
1666 return abs($size_ratio - $original_ratio) > 0.01;
1667 }
1668
1669 /**
1670 * Return the given item data.
1671 *
1672 * @param $id
1673 *
1674 * @return array|array[]|false
1675 */
1676 private function get_item_data($id) {
1677 if ( strpos($id, "dynamic") !== FALSE ) {
1678 if ( !array_key_exists($id, REACG_ALLOWED_POST_TYPES) ) {
1679 // If is post (is not image or video), but the post type is disabled (e.g. deactivated WooCommerce plugin after adding products).
1680 return "";
1681 }
1682 $data = [
1683 "type" => $id,
1684 "title" => REACG_ALLOWED_POST_TYPES[$id]['title'],
1685 'thumbnail' => ['url' => ''],
1686 ];
1687
1688 return $data;
1689 }
1690
1691 if ( preg_match('/^(' . implode('|', array_keys(REACG_ALLOWED_POST_TYPES)) . ')(\d+)$/', $id, $matches) ) {
1692 $id = $matches[2];
1693 if ( 'publish' !== get_post_status( $id ) ) {
1694 return FALSE;
1695 }
1696 $post_thumbnail_id = get_post_thumbnail_id($id);
1697 $data = $this->get_image_urls($post_thumbnail_id);
1698 $data['type'] = $matches[1];
1699 $data['title'] = html_entity_decode(get_the_title($id));
1700 // Get the post featured image alt as image alt.
1701 $data['alt'] = html_entity_decode($this->get_the_alt($post_thumbnail_id));
1702
1703 return $data;
1704 }
1705 elseif ( !is_int($id) ) {
1706 // If is post (is not image or video), but the post type is disabled (e.g. deactivated WooCommerce plugin after adding products).
1707 return "";
1708 }
1709
1710 $meta = wp_get_attachment_metadata($id);
1711
1712 if ( isset($meta['mime_type']) && strpos($meta['mime_type'], "video") !== -1 ) {
1713 $url = wp_get_attachment_url($id);
1714
1715 // If the attachment doesn't exist.
1716 if ( !$url ) {
1717 return FALSE;
1718 }
1719
1720 // Get Video URL as an original URL and selected image URL as a thumbnail URL.
1721 $thumbnail_id = isset($meta['thumbnail_id']) ? $meta['thumbnail_id'] : 0;
1722 $data = $this->get_image_urls($thumbnail_id);
1723 if ( !$data ) {
1724 // If the attachment which is selected as a thumbnail doesn't exist, get no image as a thumbnail.
1725 $data = $this->get_image_urls(0);
1726 }
1727
1728 $basename = basename($url);
1729 $data['original']['url'] = str_replace($basename, urlencode($basename), $url);
1730 $data['original']['width'] = !empty($meta['width']) ? $meta['width'] : 0;
1731 $data['original']['height'] = !empty($meta['height']) ? $meta['height'] : 0;
1732
1733 $data['type'] = "video";
1734 $data['title'] = html_entity_decode(get_the_title($id));
1735 // Get the video cover image alt as video alt.
1736 $data['alt'] = html_entity_decode($this->get_the_alt($thumbnail_id));
1737 }
1738 else {
1739 $data = $this->get_image_urls($id);
1740
1741 // If attachment doesn't exist.
1742 if ( !$data ) {
1743 return FALSE;
1744 }
1745
1746 $data['type'] = "image";
1747 $data['alt'] = html_entity_decode($this->get_the_alt($id));
1748 $data['title'] = html_entity_decode(get_the_title($id));
1749 }
1750
1751 return $data;
1752 }
1753
1754 /**
1755 * Get image ALT text with WPML support.
1756 *
1757 * - Retrieves ALT text from the translated attachment when WPML is active.
1758 * - Falls back to the original attachment ALT if the translated one is empty.
1759 * - Safe to use even when WPML is not installed.
1760 *
1761 * @param int $id Original attachment ID.
1762 * @return string Image ALT text.
1763 */
1764 private function get_the_alt($id) {
1765 // Get translated attachment ID if WPML is active.
1766 // If WPML is not installed, this returns the original ID.
1767 $attachment_id = apply_filters(
1768 'wpml_object_id', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- not defining a hook — calling a hook provided by WPML.
1769 $id,
1770 'attachment',
1771 TRUE
1772 );
1773
1774 // Try to get ALT text from the translated attachment.
1775 $alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', TRUE );
1776
1777 // Fallback: if translated ALT is empty, use the original attachment ALT.
1778 if ( empty( $alt ) && $attachment_id !== $id ) {
1779 $alt = get_post_meta( $id, '_wp_attachment_image_alt', TRUE );
1780 }
1781
1782 return $alt;
1783 }
1784
1785 /**
1786 * Resolve image-specific source metas (Image title, Image caption, Image alt text, Image description).
1787 *
1788 * @param array $item The item array.
1789 * @param WP_Post $post The post object.
1790 * @return array The item array with image metadata.
1791 */
1792 private function add_image_source_metas($item, $post) {
1793 if ( !empty($post) && $post->post_type === 'attachment' ) {
1794 $item['image_title'] = isset($item['title']) ? $item['title'] : '';
1795 $item['image_caption'] = isset($item['caption']) ? $item['caption'] : '';
1796 $item['image_alt'] = isset($item['alt']) ? $item['alt'] : '';
1797 $item['image_description'] = isset($item['description']) ? $item['description'] : '';
1798 } else {
1799 $post_thumbnail_id = !empty($post) ? get_post_thumbnail_id($post->ID) : 0;
1800 if ( !empty($post_thumbnail_id) ) {
1801 $item['image_title'] = html_entity_decode(get_the_title($post_thumbnail_id));
1802 $item['image_caption'] = html_entity_decode(wp_get_attachment_caption($post_thumbnail_id));
1803 $item['image_alt'] = html_entity_decode($this->get_the_alt($post_thumbnail_id));
1804 $thumbnail_post = get_post($post_thumbnail_id);
1805 $thumbnail_description = !empty($thumbnail_post) ? $thumbnail_post->post_content : '';
1806 $item['image_description'] = html_entity_decode(wp_kses_post(strip_shortcodes(wp_strip_all_tags($thumbnail_description))));
1807 } else {
1808 $item['image_title'] = '';
1809 $item['image_caption'] = '';
1810 $item['image_alt'] = '';
1811 $item['image_description'] = '';
1812 }
1813 }
1814 return $item;
1815 }
1816
1817 /**
1818 * Metabox to show settings separately.
1819 *
1820 * @return void
1821 */
1822 public function meta_box_settings() {
1823 ?><div class="reacg-wrapper" id="reacg_settings"></div>
1824 <?php
1825 }
1826 /**
1827 * Metabox to add and show added images.
1828 *
1829 * @param $post
1830 *
1831 * @return void
1832 */
1833 public function meta_box_images($post) {
1834 // Verify if the request is an AJAX call and ensure its validity.
1835 $valid_ajax_call = REACGLibrary::verify_nonce()
1836 && isset($_GET['id'])
1837 && isset($_GET['action'])
1838 && sanitize_text_field(wp_unslash($_GET['action'])) === 'reacg_get_images';
1839
1840 // Get the ID depending on whether it is called from builder or the gallery edit page.
1841 $post_id = isset($_GET['id']) ? (int) $_GET['id'] : (!empty($post->ID) ? (int) $post->ID : 0);
1842 $images_ids = get_post_meta( $post_id, 'images_ids', true );
1843 $additional_data = get_post_meta( $post_id, 'additional_data', true );
1844
1845 // Calculate edits count.
1846 $edit_count = get_post_meta($post_id, 'edit_count', true);
1847 $edit_count = $edit_count ? (int) $edit_count + 1 : 1;
1848 update_post_meta($post_id, 'edit_count', $edit_count);
1849
1850 if ( $valid_ajax_call ) {
1851 ob_start();
1852 }
1853 ?><div class="reacg_items"
1854 data-post-id="<?php echo esc_attr($post_id); ?>"
1855 data-edit-count="<?php echo esc_attr($edit_count); ?>"
1856 data-ajax-url="<?php echo esc_url(wp_nonce_url(admin_url('admin-ajax.php'), REACG_NONCE, REACG_NONCE)); ?>">
1857 <div class="reacg_item reacg_item_new">
1858 <div class="reacg_item_image"></div>
1859 </div><?php
1860 if ( !empty($images_ids) ) {
1861 $images_ids_arr = json_decode($images_ids, true);
1862 foreach ($images_ids_arr as $image_id) {
1863 $item = $this->get_item_data($image_id);
1864
1865 // The attachment doesn't exist or the post type is disabled.
1866 if ( !$item ) {
1867 continue;
1868 }
1869
1870 $data = [
1871 "id" => $image_id,
1872 "type" => $item['type'],
1873 "title" => $item['title'],
1874 "url" => $this->get_smallest_image_url($item),
1875 ];
1876 $this->image_item($data);
1877 }
1878 }
1879 $this->image_item();
1880 ?><input class="images_ids" id="images_ids" name="images_ids" type="hidden" value="<?php echo esc_attr($images_ids); ?>" />
1881 <input class="additional_data" id="additional_data" name="additional_data" type="hidden" value="<?php echo esc_attr($additional_data); ?>" />
1882 </div><?php
1883
1884 if ( $valid_ajax_call ) {
1885 echo json_encode(ob_get_clean());
1886 wp_die();
1887 }
1888 }
1889
1890 private function image_item($data = FALSE) {
1891 $template = FALSE;
1892 if ( $data === FALSE ) {
1893 $data = [
1894 "id" => 0,
1895 "title" => '',
1896 "type" => 'image',
1897 "url" => '',
1898 ];
1899 $template = TRUE;
1900 }
1901
1902 $edit_btn = ""; // Available for images and dynamic posts.
1903 $thumbnail_edit_btn = "reacg-hidden"; // Available for videos.
1904 $cover_icon = "reacg-hidden"; // Available for all types except for images.
1905
1906 if ( $data['type'] === "video" ) {
1907 $thumbnail_edit_btn = "";
1908 $cover_icon = "dashicons dashicons-controls-play";
1909 }
1910 elseif ( array_key_exists($data['type'], REACG_ALLOWED_POST_TYPES) ) {
1911 $cover_icon = "dashicons " . REACG_ALLOWED_POST_TYPES[$data['type']]['class'];
1912 if ( strpos($data['type'], 'dynamic') === FALSE ) {
1913 $edit_btn = "reacg-hidden";
1914 }
1915 }
1916
1917 ?><div data-id="<?php echo esc_attr($data['id']); ?>"
1918 data-type="<?php echo esc_attr($data['type']); ?>"
1919 class="reacg_item <?php echo esc_attr($template ? "reacg-template reacg-hidden" : "reacg-sortable"); ?>">
1920 <div class="reacg_item_image"
1921 title="<?php echo esc_attr($data['title']); ?>">
1922 <?php if ( !empty($data['url']) ) : ?>
1923 <img src="<?php echo esc_url($data['url']); ?>" alt="" />
1924 <?php endif; ?>
1925 <div class="reacg-cover <?php echo esc_attr($cover_icon); ?>">
1926 </div>
1927 <div class="reacg-overlay">
1928 <div class="reacg-hover-buttons">
1929 <span class="reacg-edit-thumbnail dashicons dashicons-cover-image <?php echo esc_attr($thumbnail_edit_btn); ?>" title="<?php esc_html_e('Edit video cover', 'regallery'); ?>"></span>
1930 <span class="reacg-edit dashicons dashicons-edit <?php echo esc_attr($edit_btn); ?>" title="<?php esc_html_e('Edit', 'regallery'); ?>"></span>
1931 <span class="reacg-delete dashicons dashicons-trash" title="<?php esc_html_e('Remove', 'regallery'); ?>"></span>
1932 </div>
1933 </div>
1934 </div>
1935 </div><?php
1936 }
1937
1938 /**
1939 * Remove all unnecessary metaboxes from the post type.
1940 *
1941 * @return void
1942 */
1943 public function remove_all_the_metaboxes() {
1944 global $wp_meta_boxes;
1945
1946 // These are the metabox IDs you want to pass over. They don't have to match exactly. preg_match will be run on them.
1947 $pass_over = [ 'submitdiv', 'wpseo_meta' ];
1948
1949 // All the metabox contexts you want to check.
1950 $contexts = [ 'normal', 'advanced', 'side' ];
1951
1952 // All the priorities you want to check.
1953 $priorities = [ 'high', 'core', 'default', 'low' ];
1954
1955 // Loop through and target each context.
1956 foreach ( $contexts as $context ) {
1957 // Now loop through each priority and start the purging process.
1958 foreach ( $priorities as $priority ) {
1959 if ( isset( $wp_meta_boxes[ REACG_CUSTOM_POST_TYPE ][ $context ][ $priority ] ) ) {
1960 foreach ( (array) $wp_meta_boxes[ REACG_CUSTOM_POST_TYPE ][ $context ][ $priority ] as $id => $metabox_data ) {
1961 // If the metabox ID to pass over matches the ID given, remove it from the array and continue.
1962 if ( in_array( $id, $pass_over, true ) ) {
1963 unset( $pass_over[ $id ] );
1964 continue;
1965 }
1966
1967 // Otherwise, loop through the pass_over IDs and if we have a match, continue.
1968 foreach ( $pass_over as $to_pass ) {
1969 if ( preg_match( '#^' . $id . '#i', $to_pass ) ) {
1970 continue;
1971 }
1972 }
1973
1974 // If we reach this point, remove the metabox completely.
1975 unset( $wp_meta_boxes[ REACG_CUSTOM_POST_TYPE ][ $context ][ $priority ][ $id ] );
1976 }
1977 }
1978 }
1979 }
1980 }
1981
1982 public function get_custom_templates( WP_REST_Request $request ) {
1983 if ( !is_null($request) ) {
1984 $posts = get_posts(array(
1985 'posts_per_page' => -1,
1986 'post_type' => REACG_CUSTOM_POST_TYPE,
1987 'post_status' => [ 'publish', 'private', 'draft', 'pending' ],
1988 ));
1989 $data = [];
1990 foreach ( $posts as $key => $post ) {
1991 $data[$key] = [];
1992 $data[$key]['id'] = $post->ID;
1993 $data[$key]['title'] = $post->post_title ? $post->post_title : __('(no title)', 'regallery');
1994 $data[$key]['thumbnail'] = $this->get_preview_url($post->ID);
1995
1996 $data[$key]['type'] = $this->get_gallery_type($post->ID);
1997 }
1998 if (!empty($data)) {
1999 $data[] = [
2000 'id' => -1,
2001 'title' => __('All Images Template', 'regallery'),
2002 'thumbnail' => $this->obj->plugin_url . $this->obj->no_image,
2003 'type' => $this->get_gallery_type(-1),
2004 ];
2005 }
2006
2007 return new WP_REST_Response( wp_send_json($data, 200), 200 );
2008 }
2009
2010 return wp_send_json(new WP_Error( 'wrong_template', __( 'There is no such a template.', 'regallery' ), array( 'status' => 400 ) ), 400);
2011 }
2012
2013 /**
2014 * @param $id
2015 *
2016 * @return mixed|string
2017 */
2018 private function get_gallery_type($id) {
2019 $options = new REACG_Options(TRUE);
2020 $gallery_options = $options->get_options($id);
2021
2022 $type = '';
2023 if ( !empty($gallery_options['type']) ) {
2024 if ( $gallery_options['type'] === 'thumbnails' ) {
2025 $type = 'grid';
2026 }
2027 elseif ( $gallery_options['type'] === 'slideshow' ) {
2028 $type = 'slider';
2029 }
2030 else {
2031 $type = $gallery_options['type'];
2032 }
2033 }
2034
2035 return $type;
2036 }
2037
2038 /**
2039 * Show the opt-in popup to collect email.
2040 *
2041 * @return void
2042 */
2043 public function opt_in() {
2044 // Do not show the popup in the playground mode.
2045 if ( REACG_PLAYGROUND ) {
2046 return;
2047 }
2048 $screen = get_current_screen();
2049 // Show the popup only on the Re Gallery edit and list screens.
2050 if ( $screen->id !== 'edit-reacg' && $screen->id !== 'reacg' ) {
2051 return;
2052 }
2053 // If the popup was already shown, do not show it again.
2054 if ( get_option('reacg_optin_shown', false) ) {
2055 return;
2056 }
2057 $current_user = wp_get_current_user();
2058 $email = $current_user->exists() ? $current_user->user_email : "";
2059 ?>
2060 <div id="reacg-optin-popup-overlay" class="reacg-form-popup-overlay reacg-optin-popup-overlay" style="display: none;">
2061 <div class="reacg-popup">
2062 <span class="reacg-popup-close dashicons dashicons-no"></span>
2063 <div class="reacg-popup-body">
2064 <div class="reacg-logo"></div>
2065 <div class="reacg-popup-title">
2066 <?php esc_html_e("Important updates & improvements", 'regallery'); ?>
2067 </div>
2068 <div class="reacg-popup-description">
2069 <?php esc_html_e("Opt in to receive email notifications about security and feature updates, helpful tips, and occasional offers. Sharing basic WordPress environment info helps us improve compatibility and deliver a better gallery experience.", 'regallery'); ?>
2070 </div>
2071 </div>
2072 <div class="reacg-popup-footer">
2073 <div class="reacg-email-wrapper">
2074 <input type="email" name="reacg-email" placeholder="<?php esc_html_e("Please enter your email", 'regallery'); ?>" value="<?php echo esc_attr(sanitize_email($email)); ?>" />
2075 </div>
2076 <div class="reacg-buttons-wrapper">
2077 <a class="button button-primary reacg-submit">
2078 <?php esc_html_e("Allow & Continue", 'regallery'); ?>
2079 </a>
2080 <span class="spinner"></span>
2081 <a class="button reacg-skip">
2082 <?php esc_html_e("Skip", 'regallery'); ?>
2083 </a>
2084 </div>
2085 </div>
2086 </div>
2087 </div>
2088 <?php
2089 // Mark that the popup was shown to avoid showing it again.
2090 update_option('reacg_optin_shown', true);
2091 }
2092
2093 public function widget_main_features() {
2094 $features = [
2095 [
2096 'title' => esc_html__('Responsive & Mobile-Optimized Galleries', 'regallery'),
2097 ],
2098 [
2099 'title' => esc_html__('Multiple Gallery Layouts', 'regallery'),
2100 ],
2101 [
2102 'title' => esc_html__('Live Preview Builder', 'regallery'),
2103 ],
2104 [
2105 'title' => esc_html__('Lightbox Viewing Experience', 'regallery'),
2106 ],
2107 [
2108 'title' => esc_html__('SEO-Friendly & Performance-Focused', 'regallery'),
2109 ],
2110 [
2111 'title' => esc_html__('Drag-and-Drop Simplicity', 'regallery'),
2112 ],
2113 ];
2114 wp_localize_script($this->obj->prefix . '_widget_box', 'reacg_widget_box', [
2115 'ajax_url' => wp_nonce_url(admin_url('admin-ajax.php'), REACG_NONCE, REACG_NONCE),
2116 ]);
2117 wp_enqueue_script($this->obj->prefix . '_widget_box');
2118 wp_enqueue_style($this->obj->prefix . '_widget_box');
2119 ?>
2120 <div class="reacg-widget-main-features reacg-box">
2121 <div class="reacg-box__logo">
2122 <img src="<?php echo esc_url(REACG_PLUGIN_URL . "/assets/images/icon.svg"); ?>" />
2123 </div>
2124 <div class="reacg-box__heading">
2125 <?php esc_html_e('Main Features & Solutions', 'regallery'); ?>
2126 </div>
2127 <div class="reacg-box__details">
2128 <?php
2129 foreach ( $features as $feature ) {
2130 ?>
2131 <a <?php if (!empty($feature["url"])) { ?>href="<?php echo esc_url($feature["url"]); ?>"<?php } ?> target="_blank" class="reacg-box__item">
2132 <span class="dashicons dashicons-yes"></span>
2133 <span><?php echo esc_html($feature["title"]); ?></span>
2134 </a>
2135 <?php
2136 }
2137 ?>
2138 </div>
2139 <div class="reacg-box__button-wrapper">
2140 <span class="spinner"></span>
2141 <a id="reacg-box__create-demo" class="button button-primary button-large reacg-box__button">
2142 <?php esc_html_e('Create demo gallery', 'regallery'); ?>
2143 </a>
2144 </div>
2145 </div>
2146 <?php
2147 }
2148
2149 public function widget_why_upgrade($utm_medium) {
2150 $features = [
2151 [
2152 'title' => esc_html__('Pre-Built Templates & Template Library', 'regallery'),
2153 ],
2154 [
2155 'title' => esc_html__('AI Automation for Text & Metadata', 'regallery'),
2156 ],
2157 [
2158 'title' => esc_html__('Advanced Lightbox Options', 'regallery'),
2159 ],
2160 [
2161 'title' => esc_html__('eCommerce & Mixed Galleries', 'regallery'),
2162 ],
2163 [
2164 'title' => esc_html__('Branding, Watermarks & White Labeling', 'regallery'),
2165 ],
2166 [
2167 'title' => esc_html__('Advanced Customization & Premium Support', 'regallery'),
2168 ],
2169 ];
2170 wp_enqueue_style($this->obj->prefix . '_widget_box');
2171 ?>
2172 <div class="reacg-widget-why-upgrade reacg-box">
2173 <div class="reacg-box__logo reacg__pro-logo">
2174 <?php echo wp_kses( REACGLibrary::$pro_icon, REACGLibrary::allowed_svg() ); ?>
2175 </div>
2176 <div class="reacg-box__heading">
2177 <?php
2178 /* translators: %s: the plugin name */
2179 echo sprintf(esc_html__('Why upgrade to %s Pro?', 'regallery'), esc_html(REACG_NICENAME));
2180 ?>
2181 </div>
2182 <div class="reacg-box__details">
2183 <?php
2184 foreach ( $features as $feature ) {
2185 ?>
2186 <a <?php if (!empty($feature["url"])) { ?>href="<?php echo esc_url($feature["url"]); ?>"<?php } ?> target="_blank" class="reacg-box__item">
2187 <span class="dashicons dashicons-yes"></span>
2188 <span><?php echo esc_html($feature["title"]); ?></span>
2189 </a>
2190 <?php
2191 }
2192 ?>
2193 </div>
2194 <div class="reacg-box__button-wrapper">
2195 <a href="<?php echo esc_url(add_query_arg(['utm_medium' => $utm_medium, 'utm_campaign' => 'upgrade'], REACG_PRICING_URL_UTM)); ?>" target="_blank" class="button button-primary button-large reacg-box__button reacg-box__upgrade-button">
2196 <?php esc_html_e('Upgrade', 'regallery'); ?>
2197 </a>
2198 </div>
2199 </div>
2200 <?php
2201 }
2202 }
2203