<?php
/**
 * Import class.
 *
 * @since 1.0.0
 *
 * @package Envira_Gallery
 * @author  Envira Team
 */

namespace Envira\Utils;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Import class.
 *
 * @since 1.0.0
 *
 * @package Envira_Gallery
 * @author  Envira Team
 */
class Import {

	/**
	 * Path to the file.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	public $file = __FILE__;

	/**
	 * Holds the base class object.
	 *
	 * @since 1.0.0
	 *
	 * @var object
	 */
	public $base;

	/**
	 * Holds any plugin error messages.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	public $errors = [];

	/**
	 * Primary class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {

		// Import a gallery.
		$this->import_gallery();

		// Import a gallery via Envira Import.
		add_action( 'init', [ $this, 'import_gallery' ] );
		add_action( 'admin_notices', [ $this, 'notices' ] );

		// WordPress XML Import.
		add_action( 'import_post_meta', [ $this, 'change_xml_import' ], 10, 3 );
	}

	/**
	 * Hooks into XML Import
	 *
	 * @since 1.0.0
	 * @param int    $post_id Post ID.
	 * @param string $key Gallery Key.
	 * @param string $value Gallery Value.
	 * @return null
	 */
	public function change_xml_import( $post_id, $key, $value ) {

		if ( '_eg_gallery_data' !== $key || empty( $value ) || empty( $value['gallery'] ) ) {
			return;
		}

		// we should be changing the url in the meta_data.
		foreach ( $value['gallery'] as $id => $item_data ) {
			$value['gallery'][ $id ]['src']  = $this->parse_import_url( $value['gallery'][ $id ]['src'] );
			$value['gallery'][ $id ]['link'] = $this->parse_import_url( $value['gallery'][ $id ]['link'] );
		}

		// update the meta data.
		update_post_meta( $post_id, $key, $value );
	}

	/**
	 * Change an old Envira url to the new one
	 *
	 * @since 1.0.0
	 *
	 * @param string $url URL.
	 * @return null $new_url
	 */
	public function parse_import_url( $url ) {

		$parsed_src_array = explode( '/', $url );
		$old_wp_url       = [];
		foreach ( $parsed_src_array as $segment ) {
			if ( 'wp-content' === $segment ) {
				break;
			} else {
				$old_wp_url[] = $segment;
			}
		}
		$old_wp_url_string            = trailingslashit( implode( '/', $old_wp_url ) );
		$new_wp_url_string_no_content = trailingslashit( str_replace( 'wp-content', '', content_url() ) );
		return ( str_replace( $old_wp_url_string, $new_wp_url_string_no_content, $url ) );
	}

	/**
	 * Imports an Envira gallery.
	 *
	 * @since 1.0.0
	 *
	 * @return null Return early (possibly setting errors) if failing proper checks to import the gallery.
	 */
	public function import_gallery() {

		if ( ! $this->has_imported_gallery() ) {
			return;
		}

		if ( ! $this->verify_imported_gallery() ) {
			return;
		}

		if ( ! $this->can_import_gallery() ) {
			$this->errors[] = __( 'Sorry, but you lack the permissions to import a gallery to this post.', 'envira-gallery' );
			return;
		}

		if ( ! $this->post_can_handle_gallery() ) {
			$this->errors[] = __( 'Sorry, but the post ID you are attempting to import the gallery to cannot handle a gallery.', 'envira-gallery' );
			return;
		}

		if ( ! $this->has_imported_gallery_files() ) {
			$this->errors[] = __( 'Sorry, but there are no files available to import a gallery.', 'envira-gallery' );
			$this->errors   = array_unique( $this->errors );
			return;
		}

		if ( ! $this->has_correct_filename() ) {
			$this->errors[] = __( 'Sorry, but you have attempted to upload a gallery import file with an incompatible filename. Envira Gallery import files must begin with "envira-gallery".', 'envira-gallery' );
			return;
		}

		if ( ! $this->has_json_extension() ) {
			$this->errors[] = __( 'Sorry, but Envira Gallery import files must be in <code>.json</code> format.', 'envira-gallery' );
			return;
		}

		// Retrieve the JSON contents of the file. If that fails, return an error.
		$contents = $this->get_file_contents();
		if ( ! $contents ) {
			$this->errors[] = __( 'Sorry, but there was an error retrieving the contents of the gallery export file. Please try again.', 'envira-gallery' );
			return;
		}

		// Decode the settings and start processing.
		$data    = json_decode( $contents, true );
		$post_id = isset( $_POST['envira_post_id'] ) ? absint( $_POST['envira_post_id'] ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing

		// If the post is an auto-draft (new post), make sure to save as draft first before importing.
		$this->maybe_save_draft( $post_id );

		// Delete any previous gallery data (if any) from the post that is receiving the new gallery.
		$this->remove_existing_gallery( $post_id );

		// Update the ID in the gallery data to point to the new post.
		$data['id'] = $post_id;

		// If the wp_generate_attachment_metadata function does not exist, load it into memory because we will need it.
		$this->load_metadata_function();

		// Prepare import.
		$this->prepare_import();

		// Import the gallery.
		$gallery = $this->run_import( $data, $post_id );

		// Cleanup import.
		$this->cleanup_import();

		// Update the in_gallery checker for the post that is receiving the gallery.
		update_post_meta( $post_id, '_eg_in_gallery', $gallery['in_gallery'] );

		// Unset any unncessary data from the final gallery holder.
		unset( $gallery['in_gallery'] );

		// Update the post title and slug itself.
		$gallery_post = [
			'ID' => $post_id,
		];

		// Add action so third party plugins can do things while importing (like tags addon adding categories).
		do_action( 'envira_import_gallery_end', $data, $post_id );

		// Add filter in case developer or plugin wants to modify the imported data.
		$gallery_post = apply_filters( 'envira_import_gallery_post_data', $gallery_post );
		$gallery      = apply_filters( 'envira_import_gallery_metadata', $gallery );

		// Update the post into the database.
		wp_update_post( $gallery_post );

		// Update the meta for the post that is receiving the gallery.
		update_post_meta( $post_id, '_eg_gallery_data', $gallery );

		// redirect to avoid double imports?
		wp_safe_redirect( admin_url( 'post.php?post=' . $post_id . '&action=edit&envira-gallery-imported=1#!envira-tab-misc' ) );
		exit;
	}

	/**
	 * Loops through the data provided and imports items into the gallery.
	 *
	 * @since 1.0.0
	 *
	 * @param array $data     Array of gallery data being imported.
	 * @param int   $post_id    The post ID the gallery is being imported to.
	 * @return array $gallery Modified gallery data based on imports.
	 */
	public function run_import( $data, $post_id ) {

		// Prepare variables.
		$gallery = false;
		$i       = 0;

		// Loop through the gallery items and import each item individually.
		foreach ( (array) $data['gallery'] as $id => $item ) {
			// If just starting, use the base data imported. Otherwise, use the updated data after each import.
			if ( 0 === $i ) {
				$gallery = $this->import_gallery_item( $id, $item, $data, $post_id );
			} else {
				$gallery = $this->import_gallery_item( $id, $item, $gallery, $post_id );
			}

			// Increment the iterator.
			++$i;
		}

		// Resort gallery if we have the values.
		if ( ! empty( $data['config']['sort_order'] ) && ! empty( $data['config']['sorting_direction'] ) ) {
			$gallery = envira_sort_gallery( $gallery, $data['config']['sort_order'], $data['config']['sorting_direction'] );
		}

		// Return the newly imported gallery data.
		return $gallery;
	}

	/**
	 * Imports an individual item into a gallery.
	 *
	 * @since 1.0.0
	 *
	 * @param int   $id        The image attachment ID from the import file.
	 * @param array $item    Data for the item being imported.
	 * @param array $data Array of gallery data being imported.
	 * @param int   $post_id   The post ID the gallery is being imported to.
	 * @return array $data   Modified gallery data based on import status of image.
	 */
	public function import_gallery_item( $id, $item, $data, $post_id ) {

		// If no image data was found, the image doesn't exist on the server.
		$image = wp_get_attachment_image_src( $id );
		if ( ! $image ) {
			// We need to stream our image from a remote source.
			if ( empty( $item['src'] ) && empty( $item['link'] ) ) {
				$this->errors[] = __( 'No valid URL or link found for the image ID #', 'envira-gallery' ) . $id . '.';

				// Unset it from the gallery data for meta saving.
				$data = $this->purge_image_from_gallery( $id, $data );
				// Stream the image from a remote URL.
			} elseif ( ! empty( $item['src'] ) ) {
				$data = $this->import_remote_image( $item['src'], $data, $item, $post_id, $id );
			} elseif ( ! empty( $item['link'] ) ) {
				$data = $this->import_remote_image( $item['link'], $data, $item, $post_id, $id );
			}
			// The image already exists. If the URLs don't match, stream the image into the gallery.
		} elseif ( $image[0] !== $item['src'] ) {
			// Stream the image from a remote URL.
			$data = $this->import_remote_image( $item['src'], $data, $item, $post_id, $id );
		} else {
			// The URLs match. We can simply update data and continue.
			$this->update_gallery_checker( $id, $post_id );
		}

		// Return the modified gallery data.
		return apply_filters( 'envira_gallery_imported_image_data', $data, $id, $item, $post_id );
	}

	/**
	 * Helper method to stream and import an image from a remote URL.
	 *
	 * @since 1.0.0
	 *
	 * @param string $src       The URL of the remote image to stream and import.
	 * @param array  $data       The data to use for importing the remote image.
	 * @param array  $item       The gallery image item to import.
	 * @param int    $post_id      The post ID receiving the remote image.
	 * @param int    $id           The image attachment ID to target (if available).
	 * @param bool   $stream_only Whether or not to only stream and import or actually add to gallery.
	 * @return array $data      Data with updated import information.
	 */
	public function import_remote_image( $src, $data, $item, $post_id, $id = 0, $stream_only = false ) {

		// Prepare variables.
		$stream   = wp_remote_get( $src, [ 'timeout' => 60 ] );
		$type     = wp_remote_retrieve_header( $stream, 'content-type' );
		$filename = basename( $src );
		$fileinfo = pathinfo( $filename );

		// If the filename doesn't have an extension on it, determine the filename to use to save this image to the Media Library
		// This fixes importing URLs with no file extension e.g. http://placehold.it/300x300 (which is a PNG).
		if ( ! isset( $fileinfo['extension'] ) || empty( $fileinfo['extension'] ) ) {
			switch ( $type ) {
				case 'image/jpeg':
					$filename = $filename . '.jpeg';
					break;
				case 'image/jpg':
					$filename = $filename . '.jpg';
					break;
				case 'image/gif':
					$filename = $filename . '.gif';
					break;
				case 'image/png':
					$filename = $filename . '.png';
					break;
				case 'image/webp':
					$filename = $filename . '.webp';
					break;
			}
		}

		// If we cannot get the image or determine the type, skip over the image.
		if ( is_wp_error( $stream ) ) {
			if ( $id ) {
				$data = $this->purge_image_from_gallery( $id, $data );
			}

			// If only streaming, return the error.
			if ( $stream_only ) {
				return $stream;
			}
		} elseif ( ! $type || strpos( $type, 'text/html' ) !== false ) {
			// Unset it from the gallery data for meta saving.
			if ( $id ) {
				$data = $this->purge_image_from_gallery( $id, $data );
			}

			// If only streaming, return the error.
			if ( $stream_only && isset( $stream['response']['code'] ) && 401 === $stream['response']['code'] ) {
				return envira_wp_error( 'envira_gallery_import_remote_image_error', __( '401 Error (Unauthorized) when attempting to retrieve  ', 'envira-gallery' ) . $src . '.' . __( ' This site might be behind a firewall or password-protected area.', 'envira-gallery' ) );
			} elseif ( $stream_only ) {
				return envira_wp_error( 'envira_gallery_import_remote_image_error', __( 'Could not retrieve a valid image from the URL ', 'envira-gallery' ) . $src . '.' );
			}
		} else {
			// It is an image. Stream the image.
			$mirror = wp_upload_bits( $filename, null, wp_remote_retrieve_body( $stream ) );

			// If there is an error, bail.
			if ( ! empty( $mirror['error'] ) ) {
				// Unset it from the gallery data for meta saving.
				if ( $id ) {
					$data = $this->purge_image_from_gallery( $id, $data );
				}

				// If only streaming, return the error.
				if ( $stream_only ) {
					return new \WP_Error( 'envira_gallery_import_remote_image_error', $mirror['error'] );
				}
			} else {
				// Check if the $item has title, caption, alt specified
				// If so, store those values against the attachment so they're included in the Gallery
				// If not, fallback to the defaults.
				$attachment = [
					'post_title'     => ( ( isset( $item['title'] ) && ! empty( $item['title'] ) ) ? $item['title'] : urldecode( $filename ) ), // Title.
					'post_mime_type' => $type,
					'post_excerpt'   => ( ( isset( $item['caption'] ) && ! empty( $item['caption'] ) ) ? $item['caption'] : '' ), // Caption.
				];
				$attach_id  = wp_insert_attachment( $attachment, $mirror['file'], $post_id );
				if ( ( isset( $item['alt'] ) && ! empty( $item['alt'] ) ) ) {
					update_post_meta( $attach_id, '_wp_attachment_image_alt', $item['alt'] );
				}

				// Generate and update attachment metadata.
				if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
					require_once ABSPATH . 'wp-admin/includes/image.php';
				}

				// Generate and update attachment metadata.
				$attach_data = wp_generate_attachment_metadata( $attach_id, $mirror['file'] );
				wp_update_attachment_metadata( $attach_id, $attach_data );

				// Unset it from the gallery data for meta saving now that we have a new image in its place.
				if ( $id ) {
					$data = $this->purge_image_from_gallery( $id, $data );
				}

				// Add the attachment id to the $mirror result.
				$mirror['attachment_id'] = $attach_id;

				// If only streaming and importing the image from the remote source, return it now.
				if ( $stream_only ) {
					return apply_filters( 'envira_gallery_remote_image_import_only', $mirror, $attach_data, $attach_id );
				}

				// Add the new attachment ID to the in_gallery checker.
				if ( ! is_array( $data['in_gallery'] ) ) {
					$data['in_gallery'] = [];
				}
				$data['in_gallery'][] = $attach_id;

				// Now update the attachment reference checker.
				$this->update_gallery_checker( $attach_id, $post_id );

				// Get the imported image URL.
				$image_url = wp_get_attachment_image_src( $attach_id, 'full' );

				// Update the image's src, to reflects its new URL
				// Maybe update the image's link, if the original image's link is an image
				// This ensures that links to pages, videos etc do not get 'lost' in the import
				// process.
				if ( $item['src'] === $item['link'] ) {
					$item['src']  = $image_url[0];
					$item['link'] = $image_url[0];
				} else {
					// Just update the src.
					$item['src'] = $image_url[0];
				}

				// Add the new attachment to the gallery.
				$data = $this->update_attachment_meta( $data, $attach_id, $item );
			}
		}

		// Return the remote image import data.
		return apply_filters( 'envira_gallery_remote_image_import', $data, $src, $id );
	}

	/**
	 * Purge image data from a gallery.
	 *
	 * @since 1.0.0
	 *
	 * @param int   $id      The image attachment ID to target for purging.
	 * @param array $data  The data to purge.
	 * @return array $data Purged data.
	 */
	public function purge_image_from_gallery( $id, $data ) {

		// Remove the image ID from the gallery data.
		unset( $data['gallery'][ $id ] );
		if ( isset( $data['in_gallery'] ) ) {
			$key = array_search( $id, (array) $data['in_gallery'], true );
			if ( false !== $key ) {
				unset( $data['in_gallery'][ $key ] );
			}
		}

		// Return the purged data.
		return apply_filters( 'envira_gallery_image_purged', $data, $id );
	}

	/**
	 * Update the attachment with a reference to the gallery that
	 * it has been assigned to.
	 *
	 * @since 1.0.0
	 *
	 * @param int $attach_id The image attachment ID to target.
	 * @param int $post_id   The post ID the attachment should reference.
	 */
	public function update_gallery_checker( $attach_id, $post_id ) {

		$has_gallery = get_post_meta( $attach_id, '_eg_has_gallery', true );
		if ( empty( $has_gallery ) ) {
			$has_gallery = [];
		}

		$has_gallery[] = $post_id;
		update_post_meta( $attach_id, '_eg_has_gallery', $has_gallery );
	}

	/**
	 * Update the image metadata for Envira.
	 *
	 * @since 1.0.0
	 *
	 * @param array $data       The data to use for importing the remote image.
	 * @param int   $attach_id  The image attachment ID to target.
	 * @param array $item       The original image item with metadata.
	 * @return array    $data       Data with updated meta information.
	 */
	public function update_attachment_meta( $data, $attach_id, $item ) {

		return envira_gallery_ajax_prepare_gallery_data( $data, $attach_id, $item );
	}

	/**
	 * Determines if a gallery import is available.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if an imported gallery is available, false otherwise.
	 */
	public function has_imported_gallery() {

		return ! empty( $_POST['envira_import'] );  // phpcs:ignore WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Determines if a gallery import nonce is valid and verified.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if the nonce is valid, false otherwise.
	 */
	public function verify_imported_gallery() {

		return isset( $_POST['envira-gallery-import'] ) && wp_verify_nonce( sanitize_key( $_POST['envira-gallery-import'] ), 'envira-gallery-import' );
	}

	/**
	 * Determines if the user can actually import the gallery.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if the user can import the gallery, false otherwise.
	 */
	public function can_import_gallery() {

		$manage_options = current_user_can( 'manage_options' );
		return apply_filters( 'envira_gallery_import_cap', $manage_options );
	}

	/**
	 * Determines if the post ID can handle a gallery (revision or not).
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if the post ID is not a revision, false otherwise.
	 */
	public function post_can_handle_gallery() {

		return isset( $_POST['envira_post_id'] ) && ! wp_is_post_revision( sanitize_text_field( wp_unslash( $_POST['envira_post_id'] ) ) );  // phpcs:ignore WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Determines if gallery import files are available.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if the imported gallery files are available, false otherwise.
	 */
	public function has_imported_gallery_files() {

		return ! empty( $_FILES['envira_import_gallery']['name'] ) || ! empty( $_FILES['envira_import_gallery']['tmp_name'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Determines if a gallery import file has a proper filename.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if the imported gallery file has a proper filename, false otherwise.
	 */
	public function has_correct_filename() {

		return isset( $_FILES['envira_import_gallery']['name'] ) ? preg_match( '#^envira-gallery#i', sanitize_text_field( wp_unslash( $_FILES['envira_import_gallery']['name'] ) ) ) : false;  // phpcs:ignore WordPress.Security.NonceVerification.Missing
	}

	/**
	 * Determines if a gallery import file has a proper file extension.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if the imported gallery file has a proper file extension, false otherwise.
	 */
	public function has_json_extension() {

		$file_array = isset( $_FILES['envira_import_gallery']['name'] ) ? explode( '.', sanitize_text_field( wp_unslash( $_FILES['envira_import_gallery']['name'] ) ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Missing
		$extension  = end( $file_array );
		return 'json' === $extension;
	}

	/**
	 * Retrieve the contents of the imported gallery file.
	 *
	 * @since 1.0.0
	 *
	 * @return string|bool JSON contents string if successful, false otherwise.
	 */
	public function get_file_contents() {

		$file = isset( $_FILES['envira_import_gallery']['tmp_name'] ) ? sanitize_text_field( wp_unslash( $_FILES['envira_import_gallery']['tmp_name'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing
		return file_get_contents( $file );
	}

	/**
	 * Move a new post to draft mode before importing a gallery.
	 *
	 * @since 1.0.0
	 *
	 * @param int $post_id The current post ID handling the gallery import.
	 */
	public function maybe_save_draft( $post_id ) {

		$post = get_post( $post_id );
		if ( 'auto-draft' === $post->post_status ) {
			$draft = [
				'ID'          => $post_id,
				'post_status' => 'draft',
			];
			wp_update_post( $draft );
		}
	}

	/**
	 * Helper method to remove existing gallery data when a gallery is imported.
	 *
	 * @since 1.0.0
	 *
	 * @param int $post_id The current post ID handling the gallery import.
	 */
	public function remove_existing_gallery( $post_id ) {

		delete_post_meta( $post_id, '_eg_gallery_data' );
		delete_post_meta( $post_id, '_eg_in_gallery' );
	}

	/**
	 * Load the wp_generate_attachment_metadata function if necessary.
	 *
	 * @since 1.0.0
	 */
	public function load_metadata_function() {

		if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
			require_once ABSPATH . 'wp-admin/includes/image.php';
		}
	}

	/**
	 * Set timeout to 0 and suspend cache invalidation while importing a gallery.
	 *
	 * @since 1.0.0
	 */
	public function prepare_import() {

		set_time_limit( $this->get_max_execution_time() );
		wp_suspend_cache_invalidation( true );
	}

	/**
	 * Reset cache invalidation and flush the internal cache after importing a gallery.
	 *
	 * @since 1.0.0
	 */
	public function cleanup_import() {

		wp_suspend_cache_invalidation( false );
		wp_cache_flush();
	}

	/**
	 * Helper method to return the max execution time for scripts.
	 *
	 * @since 1.0.0
	 */
	public function get_max_execution_time() {

		$time = ini_get( 'max_execution_time' );
		return ! $time || empty( $time ) ? (int) 0 : $time;
	}

	/**
	 * Outputs any errors or notices generated by the class.
	 *
	 * @since 1.0.0
	 */
	public function notices() {

		if ( ! empty( $this->errors ) ) :
			?>
		<div id="message" class="error">
			<p><?php echo wp_kses_post( implode( '<br>', $this->errors ) ); ?></p>
		</div>
			<?php
		endif;

		// If a gallery has been imported, create a notice for the import status.
		if ( empty( $this->errors ) && isset( $_GET['envira-gallery-imported'] ) && sanitize_text_field( wp_unslash( $_GET['envira-gallery-imported'] ) ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			?>
		<div id="message" class="updated">
			<p><?php esc_html_e( 'Envira gallery imported. Please check to ensure all images and data have been imported properly.', 'envira-gallery' ); ?></p>
		</div>
			<?php
		endif;
	}

	/**
	 * Returns the singleton instance of the class.
	 *
	 * @since 1.0.0
	 *
	 * @return object The Envira_Gallery_Import object.
	 */
	public static function get_instance() {

		if ( ! isset( self::$instance ) && ! ( self::$instance instanceof Envira_Gallery_Import ) ) {
			self::$instance = new Envira_Gallery_Import();
		}

		return self::$instance;
	}
}
