HEX
Server: LiteSpeed
System: Linux s166.bitcommand.com 4.18.0-513.11.1.lve.el8.x86_64 #1 SMP Thu Jan 18 16:21:02 UTC 2024 x86_64
User: h340499 (1922)
PHP: 8.2.16
Disabled: exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Upload Files
File: /home/h340499/public_html/wp-content/plugins/tp-portfolio/inc/class-tp-meta-box.php
<?php
if ( ! class_exists( 'Thim_Meta_Box' ) ) {
	/**
	 * Thim Theme - Meta Box Management
	 *
	 * Handles dynamic meta box rendering and field management in WordPress admin.
	 *
	 * @class Thim_Meta_Box
	 * @package thimpress
	 * @since 1.0
	 * @author kien16
	 */
	class Thim_Meta_Box {
		/**
		 * Meta box configuration
		 *
		 * @var array
		 */
		public $meta_box;

		/**
		 * Constructor
		 *
		 * @param array $args Meta box configuration arguments.
		 */
		public function __construct( $args ) {
			$this->meta_box = $args;

			// Register meta box and save hooks.
			add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
			add_action( 'save_post', array( $this, 'save_data' ) );

			// Image field Ajax handlers.
			add_action( 'wp_ajax_thim_attach_media', array( $this, 'wp_ajax_attach_media' ) );
			add_action( 'wp_ajax_thim_edit_media', array( $this, 'wp_ajax_edit_media' ) );
			add_action( 'wp_ajax_thim_reorder_images', array( $this, 'wp_ajax_reorder_images' ) );
			add_action( 'wp_ajax_thim_delete_file', array( $this, 'wp_ajax_delete_file' ) );

			// Image-video field Ajax handlers.
			add_action( 'wp_ajax_thim_attach_image_video', array( $this, 'wp_ajax_attach_image_video' ) );
			add_action( 'wp_ajax_thim_edit_image_video', array( $this, 'wp_ajax_edit_image_video' ) );
			add_action( 'wp_ajax_thim_reorder_image_video', array( $this, 'wp_ajax_reorder_image_video' ) );
			add_action( 'wp_ajax_thim_delete_image_video', array( $this, 'wp_ajax_delete_image_video' ) );
		}

		/**
		 * Enqueue admin styles and scripts
		 *
		 * @return void
		 */
		public function admin_enqueue_scripts() {
		}


		/**
		 * Register meta boxes
		 *
		 * @return void
		 */
		public function add_meta_boxes() {
			foreach ( $this->meta_box['pages'] as $page ) {
				add_meta_box(
					$this->meta_box['id'],
					$this->meta_box['title'],
					array( $this, 'meta_boxes_callback' ),
					$page,
					$this->meta_box['context'] ?? 'normal',
					$this->meta_box['priority'] ?? 'default',
					$this->meta_box['fields']
				);
			}
		}

		/**
		 * Render meta box content
		 *
		 * @param WP_Post $post Post object.
		 * @param array $fields Fields configuration.
		 *
		 * @return void
		 */
		public function meta_boxes_callback( $post, $fields ) {
			// Output nonce field for verification
			echo '<input type="hidden" name="thim_meta_box_nonce" value="' . esc_attr( wp_create_nonce( 'thim_meta_box_save' ) ) . '" />';
			echo '<div class="thim-metabox-tabs">';
			echo '<ul class="thim-tab-nav">';

			$tabs = $this->organize_fields_by_tabs( $fields['args'] );
			echo '</ul>';
			foreach ( $tabs as $tab => $tab_fields ) {
				echo '<div id="tab-' . esc_attr( $tab ) . '" class="thim-tab-content">';
				foreach ( $tab_fields as $field ) {
					$this->render_field( $field, $post->ID );
				}
				echo '</div>';
			}

			echo '</div>';
		}

		/**
		 * Organize fields by tab
		 *
		 * @param array $fields Fields list.
		 *
		 * @return array Organized fields by tab.
		 */
		private function organize_fields_by_tabs( $fields ) {
			$tabs = array();

			foreach ( $fields as $field ) {
				$tab  = $field['tab'] ?? 'default';
				$icon = $field['icon'] ?? 'dashicons-admin-generic';

				if ( ! isset( $tabs[ $tab ] ) ) {
					$tabs[ $tab ] = array();
					echo '<li><a href="#tab-' . esc_attr( $tab ) . '"><span class="dashicons ' . esc_attr( $icon ) . '"></span> ' . esc_html( $tab ) . '</a></li>';
				}

				$tabs[ $tab ][] = $field;
			}

			return $tabs;
		}

		/**
		 * Render a single field
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function render_field( $field, $post_id ) {
			$field_type = $field['type'] ?? 'textfield';

			$method_name = 'field_' . $field_type;
			if ( method_exists( $this, $method_name ) ) {
				call_user_func( array( $this, $method_name ), $field, $post_id );
			}
		}

		/**
		 * Render color picker field
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function field_color( $field, $post_id ) {

			wp_enqueue_style( 'wp-color-picker' );
			$color_value = get_post_meta( $post_id, $field['id'], true );
			$extra_class = $field['class'] ?? '';
			$extra_class = $extra_class ? 'thim-field ' . $extra_class : 'thim-field';
			?>
			<div class="<?php echo esc_attr( $extra_class ); ?>">
				<div class="thim-label">
					<label
						for="<?php echo esc_attr( $field['id'] ); ?>"><?php echo esc_html( $field['name'] ); ?></label>
				</div>
				<div class="thim-input">
					<input type="text" class="thim-color-field" id="<?php echo esc_attr( $field['id'] ); ?>"
						   name="<?php echo esc_attr( $field['id'] ); ?>"
						   value="<?php echo esc_attr( $color_value ); ?>"/>
				</div>
			</div>
			<?php
		}

		/**
		 * Render repeater field
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function field_repeater( $field, $post_id ) {
			$repeater_data = get_post_meta( $post_id, $field['id'], true );
			$extra_class   = $field['class'] ?? '';
			$extra_class   = $extra_class ? 'thim-field ' . $extra_class : 'thim-field';
			?>
			<div id="portfolio-repeater-wrapper" class="<?php echo esc_attr( $extra_class ); ?>">
				<?php
				if ( ! empty( $repeater_data ) && is_array( $repeater_data ) ) {
					foreach ( $repeater_data as $index => $row ) {
						$this->field_repeater_item( $field, $index, $row['title'] ?? '', $row['description'] ?? '' );
					}
				} else {
					$this->field_repeater_item( $field, 0, '', '' );
				}
				?>
			</div>
			<button type="button" id="add-<?php echo esc_attr( $field['id'] ); ?>" class="button">Add Item</button>
			<?php
		}

		/**
		 * Render repeater item
		 *
		 * @param array $field Field configuration.
		 * @param int $index Item index.
		 * @param string $title Item title.
		 * @param string $description Item description.
		 *
		 * @return void
		 */
		private function field_repeater_item( $field, $index, $title, $description ) {
			?>
			<div class="portfolio-repeater-item">
				<label><?php echo esc_html( $field['name'] ); ?> <?php echo absint( $index ) + 1; ?>:</label>
				<input type="text"
					   name="<?php echo esc_attr( $field['id'] ); ?>[<?php echo absint( $index ); ?>][title]"
					   value="<?php echo esc_attr( $title ); ?>" placeholder="Title">
				<input type="text"
					   name="<?php echo esc_attr( $field['id'] ); ?>[<?php echo absint( $index ); ?>][description]"
					   value="<?php echo esc_attr( $description ); ?>" placeholder="Description">
				<button type="button" class="remove-repeater-item button">Delete</button>
			</div>
			<?php
		}

		/**
		 * Render file upload field
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function field_upload( $field, $post_id ) {
			$file_data   = get_post_meta( $post_id, $field['id'], true );
			$file_url    = $file_data['url'] ?? '';
			$file_name   = $file_data['name'] ?? '';
			$extra_class = $field['class'] ?? '';
			$extra_class = $extra_class ? 'thim-field ' . $extra_class : 'thim-field';
			?>
			<div class="file-preview <?php echo esc_attr( $extra_class ); ?>">
				<div class="thim-label">
					<label
						for="<?php echo esc_attr( $field['id'] ); ?>"><?php echo esc_html( $field['name'] ); ?></label>
				</div>
				<div class="thim-input">
					<input type="text" id="<?php echo esc_attr( $field['id'] ); ?>_name"
						   name="<?php echo esc_attr( $field['id'] ); ?>_name"
						   value="<?php echo esc_attr( $file_name ); ?>" readonly/>
					<input type="text" id="<?php echo esc_attr( $field['id'] ); ?>_url"
						   name="<?php echo esc_attr( $field['id'] ); ?>_url"
						   value="<?php echo esc_url( $file_url ); ?>" readonly/>
					<button type="button" id="<?php echo esc_attr( $field['id'] ); ?>_button" class="button">Select
						File
					</button>
					<?php if ( $file_url ) : ?>
						<button type="button" class="portfolio_file_delete_button button">Delete File</button>
						<p><strong>Current File:</strong> <a href="<?php echo esc_url( $file_url ); ?>" target="_blank">View
								File</a></p>
					<?php endif; ?>
				</div>
			</div>
			<?php
		}


		/**
		 * Render image-video field
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function field_image_video( $field, $post_id ) {
			$images       = get_post_meta( $post_id, $field['id'], false );
			$attach_nonce = wp_create_nonce( "thim-attach-media_{$field['id']}" );
			$extra_class  = $field['class'] ?? '';
			$extra_class  = $extra_class ? ' ' . $extra_class : '';
			?>
			<div class="thim-field<?php echo esc_attr( $extra_class ); ?>">
				<div class="thim-label"><label><?php echo esc_html( $field['name'] ); ?></label></div>
				<div class="thim-input">
					<ul class="thim-images thim-images-video thim-image-video-uploaded ui-sortable"
						data-field_id="<?php echo esc_attr( $field['id'] ); ?>"
						data-reorder_nonce="<?php echo esc_attr( wp_create_nonce( "thim-reorder-images_{$field['id']}" ) ); ?>"
						data-delete_nonce="<?php echo esc_attr( wp_create_nonce( "thim-delete-file_{$field['id']}" ) ); ?>">
						<?php $this->render_image_video_list( $images ); ?>
					</ul>
					<a href="#" class="button thim-image-video-advanced-upload hide-if-no-js new-files"
					   data-attach_media_nonce="<?php echo esc_attr( $attach_nonce ); ?>">Select or Upload Images</a>
					<a href="javascript:void(null);" class="button-primary thim-video-advanced-upload-k"
					   data-attach_media_nonce="<?php echo esc_attr( $attach_nonce ); ?>">Add Video</a>
					<?php $this->render_video_dialog(); ?>
					<div class="desc"><?php echo isset( $field['desc'] ) ? wp_kses_post( $field['desc'] ) : ''; ?></div>
				</div>
			</div>
			<?php
		}

		/**
		 * Render image-video list
		 *
		 * @param array $images Images array.
		 *
		 * @return void
		 */
		private function render_image_video_list( $images ) {
			foreach ( $images as $image ) {
				$img_url = wp_get_attachment_image_src( $image, 'thumbnail' );
				$link    = get_edit_post_link( $image );
				$url_img = $img_url ? $img_url[0] : '';

				if ( 'v.' === substr( $image, 0, 2 ) ) {
					// Vimeo video
					echo '<li id="item_' . esc_attr( $image ) . '"><iframe src="https://player.vimeo.com/video/' . esc_attr( substr( $image, 2 ) ) . '?title=0&byline=0&portrait=0&color=ffffff" width="150" height="150" frameborder="0"></iframe><div class="thim-image-bar"><a title="Edit" class="thim-edit-file-k" href="#" target="_blank">Edit</a> | <a title="Delete" class="thim-delete-file-k" href="#" data-attachment_id="' . esc_attr( $image ) . '">×</a></div></li>';
				} elseif ( 'y.' === substr( $image, 0, 2 ) ) {
					// YouTube video
					echo '<li id="item_' . esc_attr( $image ) . '"><iframe title="YouTube video player" class="youtube-player" type="text/html" width="150" height="150" src="https://www.youtube.com/embed/' . esc_attr( substr( $image, 2 ) ) . '" frameborder="0"></iframe><div class="thim-image-bar"><a title="Edit" class="thim-edit-file-k" href="#" target="_blank">Edit</a> | <a title="Delete" class="thim-delete-file-k" href="#" data-attachment_id="' . esc_attr( $image ) . '">×</a></div></li>';
				} else {
					// Regular image
					echo '<li id="item_' . esc_attr( $image ) . '"><img src="' . esc_url( $url_img ) . '" /><div class="thim-image-bar"><a title="Edit" class="thim-edit-file" href="' . esc_url( $link ) . '" target="_blank">Edit</a> | <a title="Delete" class="thim-delete-file" href="#" data-attachment_id="' . esc_attr( $image ) . '">×</a></div></li>';
				}
			}
		}

		/**
		 * Render video dialog
		 *
		 * @return void
		 */
		private function render_video_dialog() {
			?>
			<div id="dialog-k" title="Insert Video Code">
				<div class="thim-label">
					<label for="project_video_type">Video</label>
				</div>
				<div class="thim-input">
					<select class="thim-select" name="thim-video-type-k" id="thim-video-type-k" size="0">
						<option value="youtube" selected="selected">Youtube</option>
						<option value="vimeo">Vimeo</option>
					</select>
				</div>
				<div class="thim-label">
					<label for="project_video_embed">Video URL or own Embed Code</label>
				</div>
				<div class="thim-input">
					<textarea class="thim-textarea large-text" name="thim-video-data-k" id="thim-video-data-k" cols="40"
							  rows="8" placeholder=""></textarea>
					<p class="description">Paste the video ID or embed code. Video will show instead of image
						slider.</p>
				</div>
			</div>
			<?php
		}

		/**
		 * Render textarea field
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function field_textarea( $field, $post_id ) {
			$post_meta   = get_post_meta( $post_id, $field['id'], true );
			$extra_class = $field['class'] ?? '';
			$extra_class = $extra_class ? ' ' . $extra_class : '';
			?>
			<div class="thim-field<?php echo esc_attr( $extra_class ); ?>">
				<div class="thim-label"><label><?php echo esc_html( $field['name'] ); ?>:</label></div>
				<div class="thim-input">
					<textarea name="<?php echo esc_attr( $field['id'] ); ?>"
							  id="<?php echo esc_attr( $field['id'] ); ?>"><?php echo esc_textarea( $post_meta ); ?></textarea>
					<div class="desc"><?php echo isset( $field['desc'] ) ? wp_kses_post( $field['desc'] ) : ''; ?></div>
				</div>
			</div>
			<?php
		}

		/**
		 * Render textfield
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function field_textfield( $field, $post_id ) {
			$post_meta   = get_post_meta( $post_id, $field['id'], true );
			$extra_class = $field['class'] ?? '';
			$extra_class = $extra_class ? ' ' . $extra_class : '';
			$desc        = isset( $field['desc'] ) ? $field['desc'] : '';
			?>
			<div class="thim-field<?php echo esc_attr( $extra_class ); ?>">
				<div class="thim-label"><label><?php echo esc_html( $field['name'] ); ?>:</label></div>
				<div class="thim-input">
					<input type="text" name="<?php echo esc_attr( $field['id'] ); ?>"
						   value="<?php echo esc_attr( $post_meta ); ?>"/>
					<div class="desc"><?php echo wp_kses_post( $desc ); ?></div>
				</div>
			</div>
			<?php
		}

		/**
		 * Render checkbox field
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function field_checkbox( $field, $post_id ) {
			$post_meta   = get_post_meta( $post_id, $field['id'], true );
			$extra_class = $field['class'] ?? '';
			$extra_class = $extra_class ? ' ' . $extra_class : '';
			?>
			<div class="thim-field<?php echo esc_attr( $extra_class ); ?>">
				<div class="thim-label"><label><?php echo esc_html( $field['name'] ); ?></label></div>
				<div class="thim-input">
					<input type="checkbox"
						   name="<?php echo esc_attr( $field['id'] ); ?>" <?php checked( $post_meta, 'on' ); ?> />
					<div class="desc"><?php echo wp_kses_post( $field['desc'] ); ?></div>
				</div>
			</div>
			<?php
		}

		/**
		 * Render select field
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function field_select( $field, $post_id ) {
			$post_meta   = get_post_meta( $post_id, $field['id'], true );
			$extra_class = $field['class'] ?? '';
			$extra_class = $extra_class ? ' ' . $extra_class : '';
			$desc        = isset( $field['desc'] ) ? $field['desc'] : '';
			?>
			<div class="thim-field<?php echo esc_attr( $extra_class ); ?>">
				<div class="thim-label"><label><?php echo esc_html( $field['name'] ); ?></label></div>
				<div class="thim-input">
					<select name="<?php echo esc_attr( $field['id'] ); ?>" id="<?php echo esc_attr( $field['id'] ); ?>">
						<?php foreach ( $field['options'] as $key => $value ) : ?>
							<option
								value="<?php echo esc_attr( $key ); ?>" <?php selected( $post_meta, $key ); ?>><?php echo esc_html( $value ); ?></option>
						<?php endforeach; ?>
					</select>
					<div class="desc"><?php echo wp_kses_post( $desc ); ?></div>
				</div>
			</div>
			<?php
		}

		/**
		 * Render image field
		 *
		 * @param array $field Field configuration.
		 * @param int $post_id Post ID.
		 *
		 * @return void
		 */
		private function field_image( $field, $post_id ) {
			$images       = get_post_meta( $post_id, $field['id'], false );
			$attach_nonce = wp_create_nonce( "thim-attach-media_{$field['id']}" );
			$extra_class  = $field['class'] ?? '';
			$extra_class  = $extra_class ? ' ' . $extra_class : '';
			?>
			<div class="thim-field<?php echo esc_attr( $extra_class ); ?>">
				<div class="thim-label"><label><?php echo esc_html( $field['name'] ); ?></label></div>
				<div class="thim-input">
					<ul class="thim-images thim-uploaded ui-sortable"
						data-field_id="<?php echo esc_attr( $field['id'] ); ?>"
						data-reorder_nonce="<?php echo esc_attr( wp_create_nonce( "thim-reorder-images_{$field['id']}" ) ); ?>"
						data-delete_nonce="<?php echo esc_attr( wp_create_nonce( "thim-delete-file_{$field['id']}" ) ); ?>">
						<?php $this->render_image_list( $images ); ?>
					</ul>
					<a href="#" class="button thim-image-advanced-upload hide-if-no-js new-files"
					   data-attach_media_nonce="<?php echo esc_attr( $attach_nonce ); ?>">Select or Upload Images</a>
					<div class="desc"><?php echo isset( $field['desc'] ) ? wp_kses_post( $field['desc'] ) : ''; ?></div>
				</div>
			</div>
			<?php
		}

		/**
		 * Render image list
		 *
		 * @param array $images Images array.
		 *
		 * @return void
		 */
		private function render_image_list( $images ) {
			foreach ( $images as $image ) {
				$img_url = wp_get_attachment_image_src( $image, 'thumbnail' );
				$link    = get_edit_post_link( $image );
				$url_img = $img_url ? $img_url[0] : '';
				?>
				<li id="item_<?php echo esc_attr( $image ); ?>">
					<img src="<?php echo esc_url( $url_img ); ?>"/>
					<div class="thim-image-bar">
						<a title="Edit" class="thim-edit-file" href="<?php echo esc_url( $link ); ?>" target="_blank">Edit</a>
						|
						<a title="Delete" class="thim-delete-file" href="#"
						   data-attachment_id="<?php echo esc_attr( $image ); ?>">×</a>
					</div>
				</li>
				<?php
			}
		}

		/**
		 * Ajax callback for attaching media to field
		 *
		 * @return void
		 */
		public static function wp_ajax_attach_media() {
			$post_id        = is_numeric( $_REQUEST['post_id'] ) ? $_REQUEST['post_id'] : 0;
			$field_id       = isset( $_POST['field_id'] ) ? $_POST['field_id'] : 0;
			$attachment_ids = isset( $_POST['attachment_ids'] ) ? $_POST['attachment_ids'] : array();

			check_ajax_referer( "thim-attach-media_{$field_id}" );
			$html = '';
			foreach ( $attachment_ids as $attachment_id ) {
				$img_url = wp_get_attachment_image_src( $attachment_id, 'thumbnail' );
				$link    = get_edit_post_link( $attachment_id );
				$html    .= '<li id="item_' . $attachment_id . '">
                            <img src="' . $img_url[0] . '">
                                <div class="thim-image-bar">
                                    <a title="Edit" class="thim-edit-file" href="' . $link . '" target="_blank">Edit</a> |
                                    <a title="Delete" class="thim-delete-file" href="#" data-attachment_id="' . $attachment_id . '">×</a>
                            </div>
                        </li>';

				add_post_meta( $post_id, $field_id, $attachment_id, false );
			}
			wp_send_json_success( $html );
		}

		/**
		 * Ajax callback for attaching media to field
		 *
		 * @return void
		 */
		public static function wp_ajax_edit_media() {
			$post_id        = is_numeric( $_REQUEST['post_id'] ) ? $_REQUEST['post_id'] : 0;
			$field_id       = isset( $_POST['field_id'] ) ? $_POST['field_id'] : 0;
			$attachment_ids = isset( $_POST['attachment_ids'] ) ? $_POST['attachment_ids'] : array();
			$attachment_old = isset( $_POST['attachment_old'] ) ? $_POST['attachment_old'] : 0;

			check_ajax_referer( "thim-attach-media_{$field_id}" );
			foreach ( $attachment_ids as $attachment_id ) {
				update_post_meta( $post_id, $field_id, $attachment_id, $attachment_old );
			}
			wp_send_json_success();
		}

		/**
		 * Ajax callback for reordering images
		 *
		 * @return void
		 */
		public static function wp_ajax_reorder_images() {
			$field_id = isset( $_POST['field_id'] ) ? $_POST['field_id'] : 0;
			$order    = isset( $_POST['order'] ) ? $_POST['order'] : 0;
			$post_id  = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;

			check_ajax_referer( "thim-reorder-images_{$field_id}" );

			parse_str( $order, $items );

			delete_post_meta( $post_id, $field_id );
			foreach ( $items['item'] as $item ) {
				add_post_meta( $post_id, $field_id, $item, false );
			}
			wp_send_json_success();
		}

		/**
		 * Ajax callback for deleting files.
		 * Modified from a function
		 *
		 * @return void
		 */
		public static function wp_ajax_delete_file() {
			$post_id       = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
			$field_id      = isset( $_POST['field_id'] ) ? $_POST['field_id'] : 0;
			$attachment_id = isset( $_POST['attachment_id'] ) ? $_POST['attachment_id'] : 0; //change
			$force_delete  = isset( $_POST['force_delete'] ) ? intval( $_POST['force_delete'] ) : 0;

			check_ajax_referer( "thim-delete-file_{$field_id}" );

			delete_post_meta( $post_id, $field_id, $attachment_id );
			$ok = $force_delete ? wp_delete_attachment( $attachment_id ) : true;

			if ( $ok ) {
				wp_send_json_success();
			} else {
				wp_send_json_error( __( 'Error: Cannot delete file', 'tp-portfolio' ) );
			}
		}

		/******************************/
		/**
		 * Ajax callback for attaching media to field
		 *
		 * @return void
		 */
		public static function wp_ajax_attach_image_video() {
			$post_id        = is_numeric( $_REQUEST['post_id'] ) ? $_REQUEST['post_id'] : 0;
			$field_id       = isset( $_POST['field_id'] ) ? $_POST['field_id'] : 0;
			$attachment_ids = isset( $_POST['attachment_ids'] ) ? $_POST['attachment_ids'] : array();

			check_ajax_referer( "thim-attach-media_{$field_id}" );
			$html = '';
			foreach ( $attachment_ids as $attachment_id ) {
				$img_url = wp_get_attachment_image_src( $attachment_id, 'thumbnail' );
				$link    = get_edit_post_link( $attachment_id );
				$html    .= '<li id="item_' . $attachment_id . '">
                            <img src="' . $img_url[0] . '">
                                <div class="thim-image-bar">
                                    <a title="Edit" class="thim-edit-file" href="' . $link . '" target="_blank">Edit</a> |
                                    <a title="Delete" class="thim-delete-file" href="#" data-attachment_id="' . $attachment_id . '">×</a>
                            </div>
                        </li>';

				add_post_meta( $post_id, $field_id, $attachment_id, false );
			}
			wp_send_json_success( $html );
		}

		/**
		 * Ajax callback for attaching media to field
		 *
		 * @return void
		 */
		public static function wp_ajax_edit_image_video() {
			$post_id        = is_numeric( $_REQUEST['post_id'] ) ? $_REQUEST['post_id'] : 0;
			$field_id       = isset( $_POST['field_id'] ) ? $_POST['field_id'] : 0;
			$attachment_ids = isset( $_POST['attachment_ids'] ) ? $_POST['attachment_ids'] : array();
			$attachment_old = isset( $_POST['attachment_old'] ) ? $_POST['attachment_old'] : 0;

			check_ajax_referer( "thim-attach-media_{$field_id}" );
			foreach ( $attachment_ids as $attachment_id ) {
				update_post_meta( $post_id, $field_id, $attachment_id, $attachment_old );
				//add_post_meta( $post_id, $field_id, $attachment_id, false );
			}
			wp_send_json_success();
		}

		/**
		 * Ajax callback for reordering images
		 *
		 * @return void
		 */
		public static function wp_ajax_reorder_image_video() {
			$field_id = isset( $_POST['field_id'] ) ? $_POST['field_id'] : 0;
			$order    = isset( $_POST['order'] ) ? $_POST['order'] : 0;
			$post_id  = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;

			check_ajax_referer( "thim-reorder-images_{$field_id}" );

			parse_str( $order, $items );

			delete_post_meta( $post_id, $field_id );
			foreach ( $items['item'] as $item ) {
				add_post_meta( $post_id, $field_id, $item, false );
			}
			wp_send_json_success();
		}

		/**
		 * Ajax callback for deleting files.
		 * Modified from a function
		 *
		 * @return void
		 */
		public static function wp_ajax_delete_image_video() {
			$post_id       = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
			$field_id      = isset( $_POST['field_id'] ) ? $_POST['field_id'] : 0;
			$attachment_id = isset( $_POST['attachment_id'] ) ? $_POST['attachment_id'] : 0; //change
			$force_delete  = isset( $_POST['force_delete'] ) ? intval( $_POST['force_delete'] ) : 0;

			check_ajax_referer( "thim-delete-file_{$field_id}" );

			delete_post_meta( $post_id, $field_id, $attachment_id );
			$ok = $force_delete ? wp_delete_attachment( $attachment_id ) : true;

			if ( $ok ) {
				wp_send_json_success();
			} else {
				wp_send_json_error( __( 'Error: Cannot delete file', 'tp-portfolio' ) );
			}
		}

		/**
		 * Save meta box data
		 *
		 * @param int $post_id Post ID.
		 *
		 * @return int Post ID.
		 */
		public function save_data( $post_id ) {
			// Verify POST request and nonce
			if ( ! isset( $_POST['thim_meta_box_nonce'] ) ) {
				return $post_id;
			}

			// Verify nonce
			if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['thim_meta_box_nonce'] ) ), 'thim_meta_box_save' ) ) {
				return $post_id;
			}

			// Check for autosave
			if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
				return $post_id;
			}

			// Check user permissions
			if ( ! current_user_can( 'edit_post', $post_id ) ) {
				return $post_id;
			}

			// Save fields
			foreach ( $this->meta_box['fields'] as $field ) {
				$this->save_field( $post_id, $field );
			}

			return $post_id;
		}

		/**
		 * Save individual field
		 *
		 * @param int $post_id Post ID.
		 * @param array $field Field configuration.
		 *
		 * @return void
		 */
		private function save_field( $post_id, $field ) {
			$field_type = $field['type'] ?? 'textfield';
			$field_id   = $field['id'];

			// Skip if field not in POST (except for upload which handles deletion)
			if ( ! isset( $_POST[ $field_id ] ) && 'upload' !== $field_type && 'repeater' !== $field_type ) {
				return;
			}

			switch ( $field_type ) {
				case 'textfield':
				case 'textarea':
				case 'select':
				case 'checkbox':
				case 'image':
				case 'color':
				case 'image_video':
					$this->save_simple_field( $post_id, $field_id );
					break;

				case 'upload':
					$this->save_upload_field( $post_id, $field );
					break;

				case 'repeater':
					$this->save_repeater_field( $post_id, $field );
					break;
			}
		}

		/**
		 * Save simple field
		 *
		 * @param int $post_id Post ID.
		 * @param string $field_id Field ID.
		 *
		 * @return void
		 */
		private function save_simple_field( $post_id, $field_id ) {
			$old = get_post_meta( $post_id, $field_id, true );
			$new = isset( $_POST[ $field_id ] ) ? sanitize_text_field( wp_unslash( $_POST[ $field_id ] ) ) : '';

			if ( $new && $new !== $old ) {
				update_post_meta( $post_id, $field_id, $new );
			} elseif ( ! $new && $old ) {
				delete_post_meta( $post_id, $field_id );
			}
		}

		/**
		 * Save upload field
		 *
		 * @param int $post_id Post ID.
		 * @param array $field Field configuration.
		 *
		 * @return void
		 */
		private function save_upload_field( $post_id, $field ) {
			$field_id = $field['id'];
			$name_key = $field_id . '_name';
			$url_key  = $field_id . '_url';

			$new_name = isset( $_POST[ $name_key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $name_key ] ) ) : '';
			$new_url  = isset( $_POST[ $url_key ] ) ? esc_url_raw( wp_unslash( $_POST[ $url_key ] ) ) : '';

			$old_meta = get_post_meta( $post_id, $field_id, true );

			// Normalize old meta
			if ( is_array( $old_meta ) ) {
				$old_name = $old_meta['name'] ?? '';
				$old_url  = $old_meta['url'] ?? '';
			} else {
				$old_name = '';
				$old_url  = $old_meta ?: '';
			}

			if ( empty( $new_name ) && empty( $new_url ) ) {
				delete_post_meta( $post_id, $field_id );
			} else {
				$new_meta = array(
					'name' => $new_name,
					'url'  => $new_url,
				);

				if ( $new_meta !== array( 'name' => $old_name, 'url' => $old_url ) ) {
					update_post_meta( $post_id, $field_id, $new_meta );
				}
			}
		}

		/**
		 * Save repeater field
		 *
		 * @param int $post_id Post ID.
		 * @param array $field Field configuration.
		 *
		 * @return void
		 */
		private function save_repeater_field( $post_id, $field ) {
			$field_id = $field['id'];

			if ( isset( $_POST[ $field_id ] ) && is_array( $_POST[ $field_id ] ) ) {
				// Sanitize repeater data
				$repeater_data = wp_unslash( $_POST[ $field_id ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				$data          = array();

				foreach ( $repeater_data as $item ) {
					if ( is_array( $item ) ) {
						$data[] = array(
							'title'       => sanitize_text_field( $item['title'] ?? '' ),
							'description' => sanitize_text_field( $item['description'] ?? '' ),
						);
					}
				}

				// Only update if data exists
				if ( ! empty( $data ) ) {
					update_post_meta( $post_id, $field_id, $data );
				} else {
					delete_post_meta( $post_id, $field_id );
				}
			} else {
				delete_post_meta( $post_id, $field_id );
			}
		}
	}
}