<?php
/**
 * Rest Font Library Controller.
 *
 * This file contains the class for the REST API Font Library Controller.
 *
 * @package    WordPress
 * @subpackage Font Library
 * @since      6.5.0
 */

if ( class_exists( 'WP_REST_Font_Library_Controller' ) ) {
	return;
}

/**
 * Font Library Controller class.
 *
 * @since 6.5.0
 */
class WP_REST_Font_Library_Controller extends WP_REST_Controller {

	/**
	 * Constructor.
	 *
	 * @since 6.5.0
	 */
	public function __construct() {
		$this->rest_base = 'fonts';
		$this->namespace = 'wp/v2';
	}

	/**
	 * Registers the routes for the objects of the controller.
	 *
	 * @since 6.5.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'install_fonts' ),
					'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
					'args'                => array(
						'font_families' => array(
							'required'          => true,
							'type'              => 'string',
							'validate_callback' => array( $this, 'validate_install_font_families' ),
						),
					),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'uninstall_fonts' ),
					'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
					'args'                => $this->uninstall_schema(),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/collections',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_font_collections' ),
					'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/collections' . '/(?P<id>[\/\w-]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_font_collection' ),
					'permission_callback' => array( $this, 'update_font_library_permissions_check' ),
				),
			)
		);
	}

	/**
	 * Gets a font collection.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_font_collection( $request ) {
		$id         = $request->get_param( 'id' );
		$collection = WP_Font_Library::get_font_collection( $id );
		// If the collection doesn't exist returns a 404.
		if ( is_wp_error( $collection ) ) {
			$collection->add_data( array( 'status' => 404 ) );
			return $collection;
		}
		$collection_with_data = $collection->get_data();
		// If there was an error getting the collection data, return the error.
		if ( is_wp_error( $collection_with_data ) ) {
			$collection_with_data->add_data( array( 'status' => 500 ) );
			return $collection_with_data;
		}
		return new WP_REST_Response( $collection_with_data );
	}

	/**
	 * Gets the font collections available.
	 *
	 * @since 6.5.0
	 *
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_font_collections() {
		$collections = array();
		foreach ( WP_Font_Library::get_font_collections() as $collection ) {
			$collections[] = $collection->get_config();
		}

		return new WP_REST_Response( $collections, 200 );
	}

	/**
	 * Returns validation errors in font families data for installation.
	 *
	 * @since 6.5.0
	 *
	 * @param array[] $font_families Font families to install.
	 * @param array   $files         Files to install.
	 * @return array $error_messages Array of error messages.
	 */
	private function get_validation_errors( $font_families, $files ) {
		$error_messages = array();

		if ( ! is_array( $font_families ) ) {
			$error_messages[] = __( 'font_families should be an array of font families.', 'gutenberg' );
			return $error_messages;
		}

		// Checks if there is at least one font family.
		if ( count( $font_families ) < 1 ) {
			$error_messages[] = __( 'font_families should have at least one font family definition.', 'gutenberg' );
			return $error_messages;
		}

		for ( $family_index = 0; $family_index < count( $font_families ); $family_index++ ) {
			$font_family = $font_families[ $family_index ];

			if (
				! isset( $font_family['slug'] ) ||
				! isset( $font_family['name'] ) ||
				! isset( $font_family['fontFamily'] )
			) {
				$error_messages[] = sprintf(
					// translators: 1: font family index.
					__( 'Font family [%s] should have slug, name and fontFamily properties defined.', 'gutenberg' ),
					$family_index
				);
			}

			if ( isset( $font_family['fontFace'] ) ) {
				if ( ! is_array( $font_family['fontFace'] ) ) {
					$error_messages[] = sprintf(
						// translators: 1: font family index.
						__( 'Font family [%s] should have fontFace property defined as an array.', 'gutenberg' ),
						$family_index
					);
					continue;
				}

				if ( count( $font_family['fontFace'] ) < 1 ) {
					$error_messages[] = sprintf(
						// translators: 1: font family index.
						__( 'Font family [%s] should have at least one font face definition.', 'gutenberg' ),
						$family_index
					);
				}

				if ( ! empty( $font_family['fontFace'] ) ) {
					for ( $face_index = 0; $face_index < count( $font_family['fontFace'] ); $face_index++ ) {

						$font_face = $font_family['fontFace'][ $face_index ];
						if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) {
							$error_messages[] = sprintf(
								// translators: 1: font family index, 2: font face index.
								__( 'Font family [%1$s] Font face [%2$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ),
								$family_index,
								$face_index
							);
						}

						if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) {
							$error_messages[] = sprintf(
								// translators: 1: font family index, 2: font face index.
								__( 'Font family [%1$s] Font face [%2$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ),
								$family_index,
								$face_index
							);
						}

						if ( isset( $font_face['uploadedFile'] ) ) {
							if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) {
								$error_messages[] = sprintf(
									// translators: 1: font family index, 2: font face index.
									__( 'Font family [%1$s] Font face [%2$s] file is not defined in the request files.', 'gutenberg' ),
									$family_index,
									$face_index
								);
							}
						}
					}
				}
			}
		}

		return $error_messages;
	}

	/**
	 * Validate input for the install endpoint.
	 *
	 * @since 6.5.0
	 *
	 * @param string          $param The font families to install.
	 * @param WP_REST_Request $request The request object.
	 * @return true|WP_Error True if the parameter is valid, WP_Error otherwise.
	 */
	public function validate_install_font_families( $param, $request ) {
		$font_families  = json_decode( $param, true );
		$files          = $request->get_file_params();
		$error_messages = $this->get_validation_errors( $font_families, $files );

		if ( empty( $error_messages ) ) {
			return true;
		}

		return new WP_Error( 'rest_invalid_param', implode( ', ', $error_messages ), array( 'status' => 400 ) );
	}

	/**
	 * Gets the schema for the uninstall endpoint.
	 *
	 * @since 6.5.0
	 *
	 * @return array Schema array.
	 */
	public function uninstall_schema() {
		return array(
			'font_families' => array(
				'type'        => 'array',
				'description' => __( 'The font families to install.', 'gutenberg' ),
				'required'    => true,
				'minItems'    => 1,
				'items'       => array(
					'required'   => true,
					'type'       => 'object',
					'properties' => array(
						'slug' => array(
							'type'        => 'string',
							'description' => __( 'The font family slug.', 'gutenberg' ),
							'required'    => true,
						),
					),
				),
			),
		);
	}

	/**
	 * Removes font families from the Font Library and all their assets.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function uninstall_fonts( $request ) {
		$fonts_to_uninstall = $request->get_param( 'font_families' );

		$errors    = array();
		$successes = array();

		if ( empty( $fonts_to_uninstall ) ) {
			$errors[] = new WP_Error(
				'no_fonts_to_install',
				__( 'No fonts to uninstall', 'gutenberg' )
			);
			$data     = array(
				'successes' => $successes,
				'errors'    => $errors,
			);
			$response = rest_ensure_response( $data );
			$response->set_status( 400 );
			return $response;
		}

		foreach ( $fonts_to_uninstall as $font_data ) {
			$font   = new WP_Font_Family( $font_data );
			$result = $font->uninstall();
			if ( is_wp_error( $result ) ) {
				$errors[] = $result;
			} else {
				$successes[] = $result;
			}
		}
		$data = array(
			'successes' => $successes,
			'errors'    => $errors,
		);
		return rest_ensure_response( $data );
	}

	/**
	 * Checks whether the user has permissions to update the Font Library.
	 *
	 * @since 6.5.0
	 *
	 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
	 */
	public function update_font_library_permissions_check() {
		if ( ! current_user_can( 'edit_theme_options' ) ) {
			return new WP_Error(
				'rest_cannot_update_font_library',
				__( 'Sorry, you are not allowed to update the Font Library on this site.', 'gutenberg' ),
				array(
					'status' => rest_authorization_required_code(),
				)
			);
		}
		return true;
	}

	/**
	 * Checks whether the font directory exists or not.
	 *
	 * @since 6.5.0
	 *
	 * @return bool Whether the font directory exists.
	 */
	private function has_upload_directory() {
		$upload_dir = WP_Font_Library::get_fonts_dir();
		return is_dir( $upload_dir );
	}

	/**
	 * Checks whether the user has write permissions to the temp and fonts directories.
	 *
	 * @since 6.5.0
	 *
	 * @return true|WP_Error True if the user has write permissions, WP_Error object otherwise.
	 */
	private function has_write_permission() {
		// The update endpoints requires write access to the temp and the fonts directories.
		$temp_dir   = get_temp_dir();
		$upload_dir = WP_Font_Library::get_fonts_dir();
		if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) {
			return false;
		}
		return true;
	}

	/**
	 * Checks whether the request needs write permissions.
	 *
	 * @since 6.5.0
	 *
	 * @param array[] $font_families Font families to install.
	 * @return bool Whether the request needs write permissions.
	 */
	private function needs_write_permission( $font_families ) {
		foreach ( $font_families as $font ) {
			if ( isset( $font['fontFace'] ) ) {
				foreach ( $font['fontFace'] as $face ) {
					// If the font is being downloaded from a URL or uploaded, it needs write permissions.
					if ( isset( $face['downloadFromUrl'] ) || isset( $face['uploadedFile'] ) ) {
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * Installs new fonts.
	 *
	 * Takes a request containing new fonts to install, downloads their assets, and adds them
	 * to the Font Library.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request The request object containing the new fonts to install
	 *                                 in the request parameters.
	 * @return WP_REST_Response|WP_Error The updated Font Library post content.
	 */
	public function install_fonts( $request ) {
		// Get new fonts to install.
		$fonts_param = $request->get_param( 'font_families' );

		/*
		 * As this is receiving form data, the font families are encoded as a string.
		 * The form data is used  because local fonts need to use that format to
		 * attach the files in the request.
		 */
		$fonts_to_install = json_decode( $fonts_param, true );

		$successes       = array();
		$errors          = array();
		$response_status = 200;

		if ( empty( $fonts_to_install ) ) {
			$errors[]        = new WP_Error(
				'no_fonts_to_install',
				__( 'No fonts to install', 'gutenberg' )
			);
			$response_status = 400;
		}

		if ( $this->needs_write_permission( $fonts_to_install ) ) {
			$upload_dir = WP_Font_Library::get_fonts_dir();
			if ( ! $this->has_upload_directory() ) {
				if ( ! wp_mkdir_p( $upload_dir ) ) {
					$errors[] = new WP_Error(
						'cannot_create_fonts_folder',
						sprintf(
							/* translators: %s: Directory path. */
							__( 'Error: Unable to create directory %s.', 'gutenberg' ),
							esc_html( $upload_dir )
						)
					);
					$response_status = 500;
				}
			}

			if ( $this->has_upload_directory() && ! $this->has_write_permission() ) {
				$errors[]        = new WP_Error(
					'cannot_write_fonts_folder',
					__( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' )
				);
				$response_status = 500;
			}
		}

		if ( ! empty( $errors ) ) {
			$data     = array(
				'successes' => $successes,
				'errors'    => $errors,
			);
			$response = rest_ensure_response( $data );
			$response->set_status( $response_status );
			return $response;
		}

		// Get uploaded files (used when installing local fonts).
		$files = $request->get_file_params();
		foreach ( $fonts_to_install as $font_data ) {
			$font   = new WP_Font_Family( $font_data );
			$result = $font->install( $files );
			if ( is_wp_error( $result ) ) {
				$errors[] = $result;
			} else {
				$successes[] = $result;
			}
		}

		$data = array(
			'successes' => $successes,
			'errors'    => $errors,
		);
		return rest_ensure_response( $data );
	}
}
