<?php
/**
 * The7 Icons Manager.
 *
 * @since 6.8.0
 *
 * @package The7
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class The7_Icon_Manager
 */
class The7_Icon_Manager {

	/**
	 * @var array Icons list.
	 */
	protected static $iconlist = array();

	const ICONS_DIR_NAME   = 'smile_fonts';
	const TEMP_DIR_NAME    = 'smile_temp';
	const CONFIG_FILE_NAME = 'charmap.php';

	/**
	 * Capability required to install or remove icon fonts.
	 *
	 * Paired with the network check in current_user_can_manage_icon_fonts(); on
	 * its own this capability is NOT sufficient, because multisite site
	 * administrators hold it.
	 */
	const MANAGE_FONTS_CAPABILITY = 'switch_themes';

	/**
	 * Maximum length of a font name, in bytes.
	 */
	const MAX_FONT_NAME_LENGTH = 64;

	/**
	 * Maximum length of a single charmap value, in characters.
	 */
	const MAX_CHARMAP_VALUE_LENGTH = 255;

	/**
	 * Add base hooks.
	 */
	public static function add_hooks() {
		add_action( 'wp_enqueue_scripts', [ __CLASS__, 'enqueue_icon_fonts' ] );
		add_filter( 'body_class', [ __CLASS__, 'add_body_class' ] );

		if ( is_admin() ) {
			add_action( 'admin_menu', [ __CLASS__, 'add_admin_menu' ] );
			add_action( 'wp_ajax_the7_icons_manager_add_zipped_font', [ __CLASS__, 'ajax_add_zipped_font' ] );
			add_action( 'wp_ajax_the7_icons_manager_remove_zipped_font', [ __CLASS__, 'ajax_remove_zipped_font' ] );
			add_action( 'wp_ajax_the7_icons_manager_add_font_awesome', [ __CLASS__, 'ajax_add_font_awesome' ] );
			add_action( 'wp_ajax_the7_icons_manager_add_ua_default_icons', [ __CLASS__, 'ajax_add_ua_default_icons' ] );
		}
	}

	/**
	 * Add admin menu page.
	 */
	public static function add_admin_menu() {
		$hook_suffix = add_submenu_page(
			'the7-dashboard',
			_x( 'Icons Manager', 'admin', 'the7mk2' ),
			_x( 'Icons Manager', 'admin', 'the7mk2' ),
			'edit_theme_options',
			'the7-icons',
			array(
				__CLASS__,
				'icon_manager_dashboard',
			)
		);

		if ( class_exists( 'The7_Admin_Dashboard' ) ) {
			$the7_dashboard = new The7_Admin_Dashboard();
			add_action( 'admin_print_styles-' . $hook_suffix, array( $the7_dashboard, 'enqueue_styles' ) );
			add_action( 'admin_print_scripts-' . $hook_suffix, array( $the7_dashboard, 'enqueue_scripts' ) );
		}

		add_action( 'admin_print_scripts-' . $hook_suffix, array( __CLASS__, 'admin_scripts' ) );
	}

	/**
	 * Enqueue admin scripts.
	 */
	public static function admin_scripts() {
		$admin_uri = PRESSCORE_ADMIN_URI;

		the7_register_style( 'the7-icon-manager', "{$admin_uri}/assets/css/the7-icon-manager" );
		the7_register_script( 'the7-icon-manager', "{$admin_uri}/assets/js/the7-icon-manager", array( 'jquery' ) );

		wp_enqueue_script( 'the7-icon-manager' );
		wp_enqueue_script( 'media-upload' );
		wp_enqueue_media();
		wp_enqueue_style( 'the7-icon-manager' );

		self::enqueue_icon_fonts();

		wp_localize_script(
			'the7-icon-manager',
			'the7IconManagerLocal',
			array(
				'nonces' => array(
					'add_zipped_font'          => wp_create_nonce( 'the7-add-zipped-fonts-nonce' ),
					'remove_zipped_font'       => wp_create_nonce( 'the7-remove-zipped-fonts-nonce' ),
					'add_add_font_awesome'     => wp_create_nonce( 'the7-add-font-awesome-nonce' ),
					'add_add_ua_default_icons' => wp_create_nonce( 'the7-add-ua-default-icons-nonce' ),
				),
				'text'   => array(
					'error'   => array(
						'invalid_file_format'       => wp_kses_post( _x( 'Please upload a valid ZIP file.<br/>You can create the file on icomoon.io', 'admin', 'the7mk2' ) ),
						'server_error'              => esc_html_x( 'Could not add the font because the server did not respond. Please reload the page, then try again.', 'admin', 'the7mk2' ),
						'could_not_add_font'        => wp_kses_post( _x( 'Could not add the font.<br/>The script returned the following error:', 'admin', 'the7mk2' ) ),
						'server_error_while_delete' => wp_kses_post( _x( 'Could not remove the font because the server did not respond.<br/>Please reload the page, then try again', 'admin', 'the7mk2' ) ),
						'could_not_remove_font'     => esc_html_x( 'Could not remove the font', 'admin', 'the7mk2' ),
					),
					'success' => array(
						'font_added' => esc_html_x( 'Font icon added successfully! Reloading ...', 'admin', 'the7mk2' ),
					),
				),
			)
		);
	}

	/**
	 * Terminate an icons manager AJAX request with a plain text response.
	 *
	 * Routed through wp_die() rather than die() so it goes via WordPress' AJAX
	 * die handler: the response body is identical in production, but the request
	 * becomes observable in tests.
	 *
	 * @param string $message Response body.
	 *
	 * @return void
	 */
	protected static function ajax_die( $message ) {
		wp_die( $message, '', array( 'response' => null ) );
	}

	/**
	 * Whether the current user may install or remove icon fonts.
	 *
	 * Installing a font writes a PHP file into wp-content/uploads that the theme
	 * includes later on, so this has to be a network-level decision. On
	 * multisite an ordinary site administrator holds 'switch_themes' but is not
	 * trusted to put executable code on the shared host, so Super Admin is
	 * required outright - exactly the boundary the UI has always advertised.
	 * The capability filter deliberately cannot relax that half.
	 *
	 * @return bool
	 */
	public static function current_user_can_manage_icon_fonts() {
		if ( is_multisite() && ! is_super_admin() ) {
			return false;
		}

		return current_user_can( apply_filters( 'the7_file_upload_capability', self::MANAGE_FONTS_CAPABILITY ) );
	}

	/**
	 * Validate a font name coming from an uploaded archive.
	 *
	 * The name ends up as a directory name under wp-content/uploads/smile_fonts,
	 * as an array key in the 'smile_fonts' option and as a CSS class prefix, so
	 * it is restricted to an allow-list instead of being merely escaped.
	 *
	 * @param mixed $font_name Raw font name.
	 *
	 * @return string Validated font name, or an empty string when it is not usable.
	 */
	protected static function sanitize_font_name( $font_name ) {
		if ( ! is_scalar( $font_name ) ) {
			return '';
		}

		$font_name = trim( (string) $font_name );

		if ( '' === $font_name || strlen( $font_name ) > self::MAX_FONT_NAME_LENGTH ) {
			return '';
		}

		// A name, never a path: anything carrying a path component is refused
		// outright rather than normalised, so '../mu-plugins' cannot quietly
		// become the perfectly valid font name 'mu-plugins'.
		if ( preg_match( '#[./\\\\\x00]#', $font_name ) ) {
			return '';
		}

		// Fold the cosmetic differences WordPress would apply anyway - runs of
		// dashes and spaces, leading/trailing '.-_' - so that 'my font' installs
		// as 'my-font' instead of being rejected.
		$font_name = sanitize_file_name( $font_name );

		// Latin letters, digits, dashes and underscores only, starting with an
		// alphanumeric.
		if ( ! preg_match( '/^[A-Za-z0-9][A-Za-z0-9_-]*$/', $font_name ) ) {
			return '';
		}

		// The directory name must now be a fixed point of sanitize_file_name(),
		// otherwise validate_icon_font_path() would reject it on the next read.
		if ( $font_name !== sanitize_file_name( $font_name ) ) {
			return '';
		}

		return $font_name;
	}

	/**
	 * Normalise a value that is about to be stored in a charmap.
	 *
	 * @param mixed $value Raw value.
	 *
	 * @return string
	 */
	protected static function sanitize_charmap_value( $value ) {
		if ( ! is_scalar( $value ) ) {
			return '';
		}

		// Drop control characters, including the null byte.
		$value = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', (string) $value );
		if ( null === $value ) {
			return '';
		}

		if ( function_exists( 'mb_substr' ) ) {
			$value = mb_substr( $value, 0, self::MAX_CHARMAP_VALUE_LENGTH );
		} else {
			$value = substr( $value, 0, self::MAX_CHARMAP_VALUE_LENGTH );
		}

		return trim( $value );
	}

	/**
	 * Normalise an icon CSS class list.
	 *
	 * Deliberately not an allow-list: the matching selectors are emitted into
	 * style.css by IcoMoon and only prefix-rewritten by re_write_css(), so
	 * dropping characters here would leave the charmap disagreeing with the
	 * stylesheet and render the icon blank. var_export() is what makes the
	 * charmap inert; this only removes characters that would be hostile if the
	 * value ever reached markup unescaped.
	 *
	 * @param mixed $class Raw class list.
	 *
	 * @return string
	 */
	protected static function sanitize_icon_class( $class ) {
		$class = str_replace( array( '<', '>', '"', "'", '`', '\\' ), '', self::sanitize_charmap_value( $class ) );

		return trim( preg_replace( '/\s+/', ' ', $class ) );
	}

	/**
	 * Whether a path resolves to a location strictly inside a base directory.
	 *
	 * Works for paths that do not exist yet: in that case the parent directory is
	 * resolved instead, which is enough to catch '..' segments and symlinks.
	 *
	 * @param string $path     Path to check.
	 * @param string $base_dir Directory the path must stay inside of.
	 *
	 * @return bool
	 */
	protected static function is_path_inside( $path, $base_dir ) {
		if ( ! is_scalar( $path ) ) {
			return false;
		}

		$path = (string) $path;
		if ( '' === $path || strpos( $path, "\0" ) !== false ) {
			return false;
		}

		$real_base = realpath( $base_dir );
		if ( ! $real_base ) {
			return false;
		}
		$real_base = trailingslashit( wp_normalize_path( $real_base ) );

		$real_path = realpath( $path );
		if ( false === $real_path ) {
			$real_parent = realpath( dirname( $path ) );
			if ( false === $real_parent ) {
				return false;
			}

			$real_path = trailingslashit( wp_normalize_path( $real_parent ) ) . basename( $path );
		}
		$real_path = trailingslashit( wp_normalize_path( $real_path ) );

		return $real_path !== $real_base && strpos( $real_path, $real_base ) === 0;
	}

	/**
	 * Validate icon font file path to prevent Local File Inclusion vulnerabilities.
	 *
	 * @param string $include_path    The include path from font config.
	 * @param string $config_file     The config filename.
	 * @param string $upload_base_dir The upload base directory.
	 *
	 * @return string|false Validated absolute path or false on failure.
	 */
	protected static function validate_icon_font_path( $include_path, $config_file, $upload_base_dir ) {
		// 1. Validate config file name - must be exactly 'charmap.php'.
		if ( $config_file !== self::CONFIG_FILE_NAME ) {
			return false;
		}

		// 2. Sanitize and validate include path.
		$include_path = sanitize_file_name( basename( $include_path ) );
		if ( empty( $include_path ) ) {
			return false;
		}

		// 3. Check for directory traversal attempts.
		if ( validate_file( $include_path ) !== 0 ) {
			return false;
		}

		// 4. Construct expected path within smile_fonts directory.
		$base_path = trailingslashit( $upload_base_dir ) . self::ICONS_DIR_NAME;
		$full_path = trailingslashit( $base_path ) . $include_path . '/' . $config_file;

		// 5. Resolve real path to prevent symlink attacks.
		$real_file = realpath( $full_path );
		$real_base = realpath( $base_path );

		// 6. Verify file exists and is within allowed directory.
		if ( ! $real_file || ! $real_base ) {
			return false;
		}

		// Ensure the resolved path is within the base directory. The trailing
		// separator matters: without it a sibling such as 'smile_fontsX' would
		// pass the prefix test.
		if ( strpos( $real_file, trailingslashit( $real_base ) ) !== 0 ) {
			return false;
		}

		// 7. Verify .php extension.
		if ( '.php' !== substr( $real_file, -4 ) ) {
			return false;
		}

		return $real_file;
	}

	/**
	 * Enqueue icon fonts.
	 */
	public static function enqueue_icon_fonts() {
		the7_register_style( 'the7-font', PRESSCORE_THEME_URI . '/fonts/icomoon-the7-font/icomoon-the7-font.css' );
		wp_enqueue_style( 'the7-font' );

		$fa_status = self::is_fontawesome_enabled();
		if ( $fa_status !== false ) {
			the7_register_fontawesome_style( 'the7-awesome-fonts' );
			wp_enqueue_style( 'the7-awesome-fonts' );
			if ( $fa_status === 'fa4' ) {
				the7_register_style( 'the7-awesome-fonts-back', PRESSCORE_THEME_URI . '/fonts/FontAwesome/back-compat.css' );
				wp_enqueue_style( 'the7-awesome-fonts-back' );
			}
		}

		$fonts = self::get_custom_icons();

		if ( ! $fonts ) {
			return;
		}

		$font_base_url = self::get_font_url();
		foreach ( $fonts as $font => $info ) {
			if ( strpos( $info['style'], 'http://' ) !== false ) {
				wp_enqueue_style( 'the7-' . $font, $info['style'] );
			} else {
				wp_enqueue_style( 'the7-' . $font, trailingslashit( $font_base_url ) . $info['style'] );
			}
		}
	}

	/**
	 * @param array $classes Classes.
	 *
	 * @return array
	 */
	public static function add_body_class( $classes ) {
		if ( self::is_fontawesome_enabled() ) {
			$classes[] = 'dt-fa-compatibility';
		}

		return $classes;
	}

	/**
	 * Display icons manager page.
	 */
	public static function icon_manager_dashboard() {
		$can_manage_fonts = self::current_user_can_manage_icon_fonts();
		?>
			<div id="the7-dashboard" class="wrap">
				<h1>
				<?php echo esc_html_x( 'Icon Fonts Manager', 'admin', 'the7mk2' ); ?>
				<?php if ( $can_manage_fonts ) { ?>
					<a href="#the7_upload_icon" class="add-new-h2 the7_upload_icon"
					   data-target="iconfont_upload"
					   data-title="<?php echo esc_attr_x( 'Upload/Select Fontello Font Zip', 'admin', 'the7mk2' ); ?>"
					   data-type="application/zip"
					   data-button="<?php echo esc_attr_x( 'Insert Fonts Zip File', 'admin', 'the7mk2' ); ?>"
					   data-trigger="the7_icon_manager_insert"
					   data-class="media-frame"
					>
					<?php echo esc_html_x( 'Upload New Icons', 'admin', 'the7mk2' ); ?>
					</a>&nbsp;<span class="spinner"></span>

					<?php if ( ! self::is_fontawesome_enabled() ) { ?>
						<a href="#the7_install_fa" class="add-new-h2 the7_add_fontawesome" data-fa-type="fa4">
							<?php echo esc_html_x( 'Install Font Awesome 4', 'admin', 'the7mk2' ); ?>
						</a>
						<a href="#the7_install_fa" class="add-new-h2 the7_add_fontawesome" data-fa-type="fa5">
							<?php echo esc_html_x( 'Install Font Awesome 6', 'admin', 'the7mk2' ); ?>
						</a>
					<?php } ?>

					<?php
					if ( class_exists( 'Ultimate_VC_Addons', false ) && ! self::is_ua_default_icons_installed() ) {
						printf(
							'<a href="#the7_install_bsf_default_icons" class="add-new-h2 the7-add-ua-default-font">%s</a>',
							esc_html_x( 'Install UA default icons', 'admin', 'the7mk2' )
						);
					}
					?>
				<?php } ?>
				</h1>
				<div id="msg"></div>
			<?php
			self::print_icon_set( self::get_the7_icons(), 'The7 Icons', '', '', false );
			self::get_font_set();
			$fa_status = self::is_fontawesome_enabled();
			if ( $fa_status ) {
				$fa_title   = $fa_status === 'fa5' ? 'Font Awesome 6' : 'Font Awesome 4';
				$fa_version = self::get_fontawesome_version();
				self::print_icon_set( self::get_fontawesome_icons(), $fa_title, $fa_title . " ($fa_version)", '', $can_manage_fonts );
			}
			?>
			</div>
			<?php
	}

	/**
	 * Display icons set.
	 */
	public static function get_font_set() {
		$upload_dir = wp_get_upload_dir();
		$fonts      = self::get_custom_icons();

		foreach ( $fonts as $font => $info ) {
			$icon_set = array();
			$icons    = array();

			$validated_file = self::validate_icon_font_path(
				$info['include'],
				$info['config'],
				$upload_dir['basedir']
			);

			if ( false === $validated_file ) {
				self::remove_font( $font );
				continue;
			}

			include $validated_file;

			if ( ! empty( $icons ) ) {
				$icon_set = array_merge( $icon_set, $icons );
			}

			self::print_icon_set( $icon_set, $font, '', "$font-", self::current_user_can_manage_icon_fonts() );
		}
	}

	/**
	 * @param array  $icon_set Icons set.
	 * @param string $font_id Font id.
	 * @param string $font_title Font title.
	 * @param string $icons_prefix Icons prefix.
	 * @param bool   $can_be_deleted Can be ddeleted.
	 *
	 * @return string|void
	 */
	public static function print_icon_set( $icon_set, $font_id, $font_title = '', $icons_prefix = '', $can_be_deleted = true ) {
		if ( empty( $icon_set ) ) {
			return '';
		}

		$font_title = $font_title ? $font_title : $font_id;
		$font_id    = str_replace( ' ', '-', $font_id );
		$output     = '<div class="icon_set-' . esc_attr( $font_id ) . ' metabox-holder">';
		$output    .= '<div class="postbox">';
		reset( $icon_set );
		$count = count( current( $icon_set ) );
		if ( $font_id === 'smt' || $font_id === 'Defaults' ) {
			$output .= '<h3 class="icon_font_name"><strong>' . _x( 'Default Icons', 'admin', 'the7mk2' ) . '</strong>';
		} else {
			$output .= '<h3 class="icon_font_name"><strong>' . esc_html( ucfirst( $font_title ) ) . '</strong>';
		}
		$output .= '<span class="fonts-count count-' . esc_attr( $font_id ) . '">' . $count . '</span>';
		if ( $can_be_deleted ) {
			$output .= '<button class="button button-secondary button-small the7_del_icon" data-delete=' . esc_attr( $font_id ) . ' data-title="' . esc_attr( _x( 'Delete Icon Set', 'admin', 'the7mk2' ) ) . '">' . esc_html( _x( 'Delete Icon Set', 'admin', 'the7mk2' ) ) . '</button>';
		}
		$output .= '</h3>';
		$output .= '<div class="inside"><div class="icon_actions">';
		$output .= '</div>';
		$output .= '<div class="icon_search"><ul class="icons-list fi_icon">';
		foreach ( $icon_set as $icons ) {
			foreach ( $icons as $icon ) {
				$output .= '<li title="' . esc_attr( $icon['class'] ) . '" data-icons="' . esc_attr( $icon['class'] ) . '" data-icons-tag="' . esc_attr( $icon['tags'] ) . '">';
				$output .= '<i class="' . esc_attr( $icons_prefix . $icon['class'] ) . '"></i><label class="icon">' . esc_html( $icon['class'] ) . '</label></li>';
			}
		}
		$output .= '</ul>';
		$output .= '</div><!-- .icon_search-->';
		$output .= '</div><!-- .inside-->';
		$output .= '</div><!-- .postbox-->';
		$output .= '</div>';

		echo $output;
	}

	/**
	 * Ajax action to add icons.
	 *
	 * @throws Exception
	 */
	public static function ajax_add_zipped_font() {
		global $wp_filesystem;

		try {
			check_ajax_referer( 'the7-add-zipped-fonts-nonce', 'security' );

			if ( ! self::current_user_can_manage_icon_fonts() ) {
				throw new Exception(
					_x(
						"Using this feature is reserved for Super Admins. You unfortunately don't have the necessary permissions.",
						'admin',
						'the7mk2'
					)
				);
			}

			self::load_wp_filesystem();

			if ( ! isset( $_POST['values']['id'] ) ) {
				throw new Exception( _x( 'Unable to get attachment id.', 'admin', 'the7mk2' ) );
			}

			$tmp_dir       = self::get_tmp_dir();
			$font_dir      = self::get_font_dir();
			$attachment_id = absint( $_POST['values']['id'] );
			$filter        = array(
				'\.eot',
				'\.svg',
				'\.ttf',
				'\.woff',
				'\.json',
				'\.css',
			);
			$unzipped = self::zip_flatten( get_attached_file( $attachment_id ), $tmp_dir, $filter );
			if ( ! $unzipped ) {
				throw new Exception( _x( 'Unable to unzip icons archive.', 'admin', 'the7mk2' ) );
			}

			$installed_fonts = (array) $wp_filesystem->dirlist( $font_dir );
			$font_name       = self::create_config( $tmp_dir, $font_dir, array_keys( $installed_fonts ) );

			$response = 'the7_icon_font_added: ' . esc_html( $font_name );
		} catch ( Exception $e ) {
			$response = esc_html( $e->getMessage() );
		}

		self::ajax_die( $response );
	}

	/**
	 * Ajax action to remove icons.
	 *
	 * @throws Exception
	 */
	public static function ajax_remove_zipped_font() {
		global $wp_filesystem;

		try {
			check_ajax_referer( 'the7-remove-zipped-fonts-nonce', 'security' );

			if ( ! self::current_user_can_manage_icon_fonts() ) {
				throw new Exception(
					_x(
						"Using this feature is reserved for Super Admins. You unfortunately don't have the necessary permissions.",
						'admin',
						'the7mk2'
					)
				);
			}

			if ( ! isset( $_POST['del_font'] ) ) {
				throw new Exception( _x( 'Was not able to remove Font.', 'admin', 'the7mk2' ) );
			}

			$font = sanitize_text_field( wp_unslash( $_POST['del_font'] ) );

			$is_font_awesome     = in_array( $font, array( 'Font-Awesome-4', 'Font-Awesome-6' ), true );
			$is_ua_default_icons = $font === 'Defaults';

			$return_value = 'the7_icon_font_removed';
			if ( $is_font_awesome || $is_ua_default_icons ) {
				$return_value .= '_with_reload';
			}

			if ( $is_font_awesome ) {
				self::disable_fontawesome();
			} else {
				self::load_wp_filesystem();

				$list = self::load_iconfont_list();
				if ( ! isset( $list[ $font ] ) ) {
					throw new Exception( _x( 'Was not able to remove Font.', 'admin', 'the7mk2' ) );
				}

				$font_to_delete = $list[ $font ];

				/*
				 * Never recurse outside the uploads tree. Fonts this class installed
				 * always sit under smile_fonts; entries carrying an absolute
				 * 'full_path' come from third-party icon plugins, which still place
				 * their fonts in uploads, so that is the widest boundary allowed. A
				 * font pointing anywhere else is dropped from the option without
				 * touching the filesystem.
				 */
				$has_absolute_path = ! empty( $font_to_delete['full_path'] );
				$boundary          = $has_absolute_path ? wp_get_upload_dir()['basedir'] : self::get_font_dir();

				if ( self::is_path_inside( $font_to_delete['include'], $boundary ) ) {
					$wp_filesystem->rmdir( $font_to_delete['include'], true );
				}

				self::remove_font( $font );
			}

			$response = esc_html( $return_value );
		} catch ( Exception $e ) {
			$response = esc_html( $e->getMessage() );
		}

		self::ajax_die( $response );
	}

	/**
	 * Add Font Awesome.
	 *
	 * @throws Exception
	 */
	public static function ajax_add_font_awesome() {
		try {
			check_ajax_referer( 'the7-add-font-awesome-nonce', 'security' );

			if ( ! self::current_user_can_manage_icon_fonts() ) {
				throw new Exception(
					_x(
						"Using this feature is reserved for Super Admins. You unfortunately don't have the necessary permissions.",
						'admin',
						'the7mk2'
					)
				);
			}

			$type = isset( $_POST['type'] ) ? $_POST['type'] : 'fa5';
			$type === 'fa5' ? self::enable_fontawesome5() : self::enable_fontawesome4();

			$response = 'the7_icon_font_added: FontAwesome';
		} catch ( Exception $e ) {
			$response = esc_html( $e->getMessage() );
		}

		self::ajax_die( $response );
	}

	/**
	 * @return bool
	 */
	public static function add_ua_default_icons() {
		if ( class_exists( 'AIO_Icon_Manager' ) ) {
			$ua_icons_manager = new AIO_Icon_Manager();
		} elseif ( class_exists( 'Ultimate_VC_Addons_Icon_Manager' ) ) {
			$ua_icons_manager = new Ultimate_VC_Addons_Icon_Manager();
		} else {
			return false;
		}

		if ( ! method_exists( $ua_icons_manager, 'AIO_move_fonts' ) ) {
			return false;
		}

		$ua_icons_manager->AIO_move_fonts();

		return true;
	}

	/**
	 * Add UA default icons via AJAX.
	 *
	 * @throws Exception
	 */
	public static function ajax_add_ua_default_icons() {
		try {
			check_ajax_referer( 'the7-add-ua-default-icons-nonce', 'security' );

			if ( ! self::current_user_can_manage_icon_fonts() ) {
				throw new Exception(
					_x(
						"Using this feature is reserved for Super Admins. You unfortunately don't have the necessary permissions.",
						'admin',
						'the7mk2'
					)
				);
			}

			if ( static::add_ua_default_icons() ) {
				$response = 'the7_icon_font_added: Defaults';
			} else {
				$response = esc_html_x( 'Seems that Ultimate VC Addons plugin is inactive. Please, activate it and try again.', 'admin', 'the7mk2' );
			}
		} catch ( Exception $e ) {
			$response = esc_html( $e->getMessage() );
		}

		self::ajax_die( $response );
	}

	/**
	 * @return string
	 */
	protected static function get_tmp_dir() {
		$wp_upload_dir = wp_get_upload_dir();

		return trailingslashit( $wp_upload_dir['basedir'] ) . self::ICONS_DIR_NAME . '/' . self::TEMP_DIR_NAME;
	}

	/**
	 * @return string
	 */
	protected static function get_font_dir() {
		$wp_upload_dir = wp_get_upload_dir();

		return trailingslashit( $wp_upload_dir['basedir'] ) . self::ICONS_DIR_NAME;
	}

	/**
	 * @return string
	 */
	protected static function get_font_url() {
		$wp_upload_dir = wp_get_upload_dir();

		return set_url_scheme( trailingslashit( $wp_upload_dir['baseurl'] ) ) . self::ICONS_DIR_NAME;
	}

	/**
	 * @param string $file File.
	 * @param string $to Dir name.
	 * @param array  $filter Filter.
	 *
	 * @return bool
	 * @throws Exception
	 */
	protected static function zip_flatten( $file, $to, $filter = array() ) {
		global $wp_filesystem;

		wp_raise_memory_limit( 'admin' );

		$to = trailingslashit( $to );

		if ( $wp_filesystem->exists( $to ) ) {
			$wp_filesystem->rmdir( $to, true );
		}

		if ( ! wp_mkdir_p( $to ) ) {
			throw new Exception( __( "Wasn't able to create temp folder", 'the7mk2' ) );
		}

		if ( ! class_exists( 'ZipArchive' ) ) {
			throw new Exception( __( "Wasn't able to work with Zip Archive", 'the7mk2' ) );
		}

		$z     = new ZipArchive();
		$zopen = $z->open( $file, ZIPARCHIVE::CHECKCONS );
		if ( $zopen !== true ) {
			throw new Exception( __( 'Incompatible Archive.', 'the7mk2' ) );
		}

		for ( $i = 0; $i < $z->numFiles; $i++ ) {
			if ( ! $info = $z->statIndex( $i ) ) {
				throw new Exception( __( 'Could not retrieve file from archive.', 'the7mk2' ) );
			}

			// Directory.
			if ( '/' === substr( $info['name'], -1 ) ) {
				continue;
			}

			// Don't extract the OS X-created __MACOSX directory files.
			if ( 0 === strpos( $info['name'], '__MACOSX/' ) ) {
				continue;
			}

			// Don't extract invalid files:
			if ( 0 !== validate_file( $info['name'] ) ) {
				continue;
			}

			if ( $filter ) {
				$regex = implode( '|', $filter );
				if ( ! preg_match( "!($regex)$!", $info['name'] ) ) {
					continue;
				}
			}

			$contents = $z->getFromIndex( $i );
			if ( false === $contents ) {
				throw new Exception( __( 'Could not extract file from archive.', 'the7mk2' ) );
			}

			if ( ! $wp_filesystem->put_contents( $to . basename( $info['name'] ), $contents, FS_CHMOD_FILE ) ) {
				throw new Exception( __( 'Could not copy file.', 'the7mk2' ) );
			}
		}
		$z->close();

		return true;
	}

	/**
	 * @throws Exception
	 *
	 * @param string $font_dir Font dir.
	 * @param array  $installed_fonts Fonts list.
	 * @param string $work_dir Work dir.
	 *
	 * @return string
	 */
	protected static function create_config( $work_dir, $font_dir, $installed_fonts = array() ) {
		global $wp_filesystem;

		$json_file = self::find_json( $work_dir );
		$svg_file  = self::find_svg( $work_dir );
		if ( empty( $json_file ) || empty( $svg_file ) ) {
			$wp_filesystem->rmdir( $work_dir, true );
			throw new Exception( __( 'selection.json or SVG file not found. Was not able to create the necessary config files', 'the7mk2' ) );
		}

		$response = $wp_filesystem->get_contents( trailingslashit( $work_dir ) . $svg_file );
		$json     = $wp_filesystem->get_contents( trailingslashit( $work_dir ) . $json_file );

		if ( '' === $json || '' === $response ) {
			return '';
		}

		$xml = simplexml_load_string( $response, 'SimpleXMLElement', LIBXML_NONET );

		if ( ! $xml || ! isset( $xml->defs->font ) ) {
			$wp_filesystem->rmdir( $work_dir, true );
			throw new Exception( __( 'Was not able to read the SVG font file. Please upload fonts created with the IcoMoon App Only.', 'the7mk2' ) );
		}

		$font_attr = $xml->defs->font->attributes();
		$font_name = self::sanitize_font_name( (string) $font_attr['id'] );

		if ( '' === $font_name ) {
			$wp_filesystem->rmdir( $work_dir, true );
			throw new Exception( __( 'The font name is missing or contains unsupported characters. Please rename the font in the IcoMoon App using latin letters, digits, dashes and underscores only, then upload it again.', 'the7mk2' ) );
		}

		if ( $font_name === 'unknown' || in_array( $font_name, $installed_fonts, true ) ) {
			$wp_filesystem->rmdir( $work_dir, true );
			throw new Exception( __( 'It seems that the font with the same name is already exists! Please upload the font with different name.', 'the7mk2' ) );
		}

		$glyphs = $xml->defs->font->children();

		$unicodes = array();
		if ( isset( $glyphs->glyph ) ) {
			foreach ( $glyphs->glyph as $glyph ) {
				$attributes = $glyph->attributes();
				$unicodes[] = (string) $attributes['unicode'];
			}
			unset( $unicodes[0] );
		}

		$file_contents = json_decode( $json );
		if ( ! isset( $file_contents->IcoMoonType ) ) {
			$wp_filesystem->rmdir( $work_dir, true );
			throw new Exception( __( 'Uploaded font is not from IcoMoon. Please upload fonts created with the IcoMoon App Only.', 'the7mk2' ) );
		}

		$icons = $file_contents->icons;
		if ( empty( $icons ) ) {
			$wp_filesystem->rmdir( $work_dir, true );
			throw new Exception( __( 'There are no icons.', 'the7mk2' ) );
		}

		$n           = 1;
		$json_config = array();
		foreach ( $icons as $icon ) {
			// $n advances once per icon, whether or not the icon is usable, so the
			// glyph order from the SVG keeps lining up with the icon order from the JSON.
			$unicode = isset( $unicodes[ $n ] ) ? $unicodes[ $n ] : '';
			++$n;

			$icon_name = isset( $icon->properties->name ) ? self::sanitize_charmap_value( $icon->properties->name ) : '';
			if ( '' === $icon_name ) {
				continue;
			}

			$tags = array();
			if ( isset( $icon->icon->tags ) && is_array( $icon->icon->tags ) ) {
				foreach ( $icon->icon->tags as $tag ) {
					if ( is_scalar( $tag ) ) {
						$tags[] = (string) $tag;
					}
				}
			}

			$json_config[ $font_name ][ $icon_name ] = array(
				'class'   => self::sanitize_icon_class( str_replace( array( ' ', ',' ), array( '', ' ' ), $icon_name ) ),
				'tags'    => self::sanitize_charmap_value( implode( ',', $tags ) ),
				'unicode' => self::sanitize_charmap_value( $unicode ),
			);
		}

		if ( empty( $json_config[ $font_name ] ) ) {
			$wp_filesystem->rmdir( $work_dir, true );
			throw new Exception( __( 'There are no icons.', 'the7mk2' ) );
		}

		self::write_config( $font_name, $json_config, $work_dir );
		self::re_write_css( $font_name, $work_dir );
		self::rename_files( $font_name, $work_dir );
		self::rename_folder( $work_dir, "{$font_dir}/{$font_name}" );
		self::add_font( $font_name, self::ICONS_DIR_NAME . "/{$font_name}" );

		return $font_name;
	}

	/**
	 * @param string $font_name Font name.
	 * @param array  $json_config Config.
	 * @param string $work_dir Work dir.
	 *
	 * @throws Exception
	 */
	protected static function write_config( $font_name, $json_config, $work_dir ) {
		global $wp_filesystem;

		$font_name = self::sanitize_font_name( $font_name );
		if ( '' === $font_name || empty( $json_config[ $font_name ] ) ) {
			$wp_filesystem->rmdir( $work_dir, true );
			throw new Exception( __( 'Was not able to write a config file', 'the7mk2' ) );
		}

		$charmap_icons = array();
		foreach ( $json_config[ $font_name ] as $icon => $info ) {
			if ( empty( $info ) ) {
				$wp_filesystem->rmdir( $work_dir, true );
				throw new Exception( __( 'Was not able to write a config file', 'the7mk2' ) );
			}

			$charmap_icons[ self::sanitize_charmap_value( $icon ) ] = array(
				'class'   => self::sanitize_icon_class( isset( $info['class'] ) ? $info['class'] : '' ),
				'tags'    => self::sanitize_charmap_value( isset( $info['tags'] ) ? $info['tags'] : '' ),
				'unicode' => self::sanitize_charmap_value( isset( $info['unicode'] ) ? $info['unicode'] : '' ),
			);
		}

		/*
		 * The charmap is data, never code: the whole array is emitted through
		 * var_export(), so no string taken out of the uploaded archive can break
		 * out of its literal and become executable PHP.
		 */
		$config  = "<?php\n";
		$config .= "// Generated by The7 Icons Manager. Data only - do not edit.\n";
		$config .= "defined( 'ABSPATH' ) || exit;\n\n";
		$config .= '$icons = ' . var_export( array( $font_name => $charmap_icons ), true ) . ";\n";

		$charmap = $work_dir . '/' . self::CONFIG_FILE_NAME;
		if ( ! $wp_filesystem->put_contents( $charmap, $config ) ) {
			$wp_filesystem->rmdir( $work_dir, true );
			throw new Exception( __( 'Was not able to write a config file', 'the7mk2' ) );
		}
	}

	/**
	 * @param string $font_name Font name.
	 * @param string $work_dir Work dir.
	 *
	 * @throws Exception
	 */
	protected static function re_write_css( $font_name, $work_dir ) {
		global $wp_filesystem;

		$style = $work_dir . '/style.css';
		$file  = $wp_filesystem->get_contents( $style );
		if ( ! $file ) {
			throw new Exception( __( 'Unable to write css. Upload icons downloaded only from icomoon', 'the7mk2' ) );
		}

		$find_and_replace = array(
			'fonts/'  => '',
			'icon-'   => "{$font_name}-",
			'.icon {' => "[class^='{$font_name}-'], [class*='{$font_name}-'] {",
			'i {'     => "[class^={$font_name}-'], [class*='{$font_name}-'] {",
		);
		$str              = str_replace( array_keys( $find_and_replace ), array_values( $find_and_replace ), $file );

		/* Remove comments */
		$str = preg_replace( '!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $str );

		/* Remove tabs, spaces, newlines, etc. */
		$str = str_replace( array( "\r\n", "\r", "\n", "\t", '  ', '    ', '    ' ), '', $str );

		$wp_filesystem->put_contents( $style, $str );
	}

	/**
	 * @param string $font_name Font name.
	 * @param string $work_dir Work dir.
	 *
	 * @throws Exception
	 */
	protected static function rename_files( $font_name, $work_dir ) {
		$font_name = self::sanitize_font_name( $font_name );
		if ( '' === $font_name ) {
			throw new Exception( __( 'Unable to add this font. Please try again.', 'the7mk2' ) );
		}

		$font_dir   = self::get_font_dir();
		$extensions = array( 'eot', 'svg', 'ttf', 'woff', 'css' );
		foreach ( glob( trailingslashit( $work_dir ) . '*' ) as $file ) {
			$path_parts = pathinfo( $file );

			if ( ! isset( $path_parts['extension'] ) || ! in_array( $path_parts['extension'], $extensions, true ) ) {
				continue;
			}

			if ( strpos( $path_parts['filename'], '.dev' ) !== false || $path_parts['filename'] === $font_name ) {
				continue;
			}

			$renamed_file = trailingslashit( $path_parts['dirname'] ) . $font_name . '.' . $path_parts['extension'];
			if ( ! self::is_path_inside( $renamed_file, $font_dir ) ) {
				continue;
			}

			rename( $file, $renamed_file );
		}
	}

	/**
	 * @param string $source_dir Source dir.
	 * @param string $destination_dir Destination dir.
	 *
	 * @return bool
	 * @throws Exception
	 */
	protected static function rename_folder( $source_dir, $destination_dir ) {
		global $wp_filesystem;

		// Both ends of the move have to stay inside the smile_fonts directory, so a
		// font name carrying '..' cannot drop the generated charmap.php somewhere
		// WordPress would auto-load it from (mu-plugins, for instance).
		$font_dir = self::get_font_dir();
		if ( ! self::is_path_inside( $source_dir, $font_dir ) || ! self::is_path_inside( $destination_dir, $font_dir ) ) {
			$wp_filesystem->rmdir( $source_dir, true );
			throw new Exception( __( 'Unable to add this font. Please try again.', 'the7mk2' ) );
		}

		$wp_filesystem->rmdir( $destination_dir );
		if ( ! rename( $source_dir, $destination_dir ) ) {
			$wp_filesystem->rmdir( $source_dir );
			throw new Exception( __( 'Unable to add this font. Please try again.', 'the7mk2' ) );
		}

		return true;
	}

	/**
	 * @param string $font_name Font name.
	 * @param string $font_dir Font dir.
	 *
	 * @return void
	 */
	protected static function add_font( $font_name, $font_dir ) {
		$fonts               = self::get_custom_icons();
		$fonts[ $font_name ] = array(
			'include' => $font_dir,
			'folder'  => $font_dir,
			'style'   => "{$font_name}/{$font_name}.css",
			'config'  => self::CONFIG_FILE_NAME,
		);
		update_option( 'smile_fonts', $fonts );
	}

	/**
	 * @param string $font Font.
	 *
	 * @return void
	 */
	protected static function remove_font( $font ) {
		$fonts = self::get_custom_icons();
		if ( isset( $fonts[ $font ] ) ) {
			unset( $fonts[ $font ] );
			update_option( 'smile_fonts', $fonts );
		}
	}

	/**
	 * @param string $tmp_dir Tmp dir.
	 *
	 * @return string
	 */
	protected static function find_json( $tmp_dir ) {
		$files = scandir( $tmp_dir );
		foreach ( $files as $file ) {
			if ( strpos( strtolower( $file ), '.json' ) !== false && $file[0] !== '.' ) {
				return $file;
			}
		}

		return '';
	}

	/**
	 * @param string $tmp_dir Tmp dir.
	 *
	 * @return string
	 */
	protected static function find_svg( $tmp_dir ) {
		$files = scandir( $tmp_dir );
		foreach ( $files as $file ) {
			if ( strpos( strtolower( $file ), '.svg' ) !== false && $file[0] !== '.' ) {
				return $file;
			}
		}

		return '';
	}

	/**
	 * Return icons files list to enqueue.
	 *
	 * @return array
	 */
	public static function load_iconfont_list() {
		if ( ! empty( self::$iconlist ) ) {
			return self::$iconlist;
		}

		$font_configs = self::get_custom_icons();
		// If we got any include the charmaps and add the chars to an array.
		$upload_dir = wp_get_upload_dir();
		$path       = trailingslashit( $upload_dir['basedir'] );
		$url        = trailingslashit( $upload_dir['baseurl'] );
		foreach ( $font_configs as $key => $config ) {
			if ( empty( $config['full_path'] ) ) {
				$font_configs[ $key ]['include'] = $path . $font_configs[ $key ]['include'];
				$font_configs[ $key ]['folder']  = $url . $font_configs[ $key ]['folder'];
			}
		}
		// Cache the result.
		self::$iconlist = $font_configs;

		return $font_configs;
	}

	/**
	 * Return icons classes to be used in shortcodes UI.
	 *
	 * @return array
	 */
	public static function get_icons_classes() {
		$icons_classes = array();
		foreach ( self::get_icon_fonts_list() as $font => $icons ) {
			$icons_classes[ $font ] = array();

			foreach ( $icons as $icon_name => $icon_definition ) {
                $icon = $icon_definition['class'];
                if ( is_string( $icon_name ) ) {
                    $icon = "$font-$icon";
				}
				$icons_classes[ $font ][] = current( explode( ',', $icon ) );
			}
		}

		return $icons_classes;
	}

	/**
	 * @return array
	 */
	public static function get_icon_fonts_list() {
		$upload_dir = wp_get_upload_dir();
		$icon_fonts = array();
		$icon_fonts = array_merge( $icon_fonts, self::get_the7_icons() );

		foreach ( self::get_custom_icons() as $font => $info ) {
			$icons = array();

			$validated_file = self::validate_icon_font_path(
				$info['include'],
				$info['config'],
				$upload_dir['basedir']
			);

			if ( false === $validated_file ) {
				continue;
			}

			include $validated_file;

			if ( ! isset( $icons[ $font ] ) ) {
				continue;
			}

			$icon_fonts[ $font ] = $icons[ $font ];
		}

		if ( self::is_fontawesome_enabled() ) {
			$icon_fonts = array_merge( $icon_fonts, self::get_fontawesome_icons() );
		}

		return $icon_fonts;
	}

	/**
	 * @throws Exception
	 */
	protected static function load_wp_filesystem() {
		global $wp_filesystem;

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

		$wp_upload = wp_get_upload_dir();
		if ( ! $wp_filesystem && ! WP_Filesystem( false, $wp_upload['basedir'] ) ) {
			throw new Exception( __( 'Cannot access file system.', 'the7mk2' ) );
		}
	}

	/**
	 * Return 'smile_fonts' option value.
	 *
	 * @return array
	 */
	protected static function get_custom_icons() {
		$icons = get_option( 'smile_fonts' );

		return $icons ? (array) $icons : array();
	}

	/**
	 * Return true if UA Default icons is installed.
	 *
	 * @return bool
	 */
	public static function is_ua_default_icons_installed() {
		$installed_icons = self::get_custom_icons();

		return array_key_exists( 'Defaults', $installed_icons );
	}

	/**
	 * @return false|mixed|void
	 */
	public static function is_fontawesome_enabled() {
		return get_option( 'the7_fontawesome_enabled' );
	}

	/**
	 * @return void
	 */
	public static function enable_fontawesome5() {
		update_option( 'the7_fontawesome_enabled', 'fa5' );
	}

	/**
	 * @return void
	 */
	public static function enable_fontawesome4() {
		update_option( 'the7_fontawesome_enabled', 'fa4' );
	}

	/**
	 * @return void
	 */
	public static function disable_fontawesome() {
		delete_option( 'the7_fontawesome_enabled' );
	}

	/**
	 * @return string
	 */
	protected static function get_fontawesome_version() {
		return include PRESSCORE_EXTENSIONS_DIR . '/font-awesome-version.php';
	}

	/**
	 * @return array
	 */
	protected static function get_fontawesome_icons() {
		$font_awesome_icons                 = array();
		$font_awesome_icons['Font Awesome'] = include PRESSCORE_EXTENSIONS_DIR . '/font-awesome-icons.php';
		foreach ( $font_awesome_icons['Font Awesome'] as &$fa_icon ) {
			$fa_icon = array(
				'class'   => $fa_icon,
				'tags'    => '',
				'unicode' => '',
			);
		}

		return $font_awesome_icons;
	}

	/**
	 * @return array
	 */
	protected static function get_the7_icons() {
		$the7_icons               = array();
		$the7_icons['The7 Icons'] = include PRESSCORE_EXTENSIONS_DIR . '/the7-icons-list.php';
		foreach ( $the7_icons['The7 Icons'] as &$fa_icon ) {
			$fa_icon = array(
				'class'   => $fa_icon,
				'tags'    => '',
				'unicode' => '',
			);
		}

		return $the7_icons;
	}
}
