diff --git a/src/Controller/Controller_Custom_Fonts.php b/src/Controller/Controller_Custom_Fonts.php index b5b3b16c2..d8e5c0090 100644 --- a/src/Controller/Controller_Custom_Fonts.php +++ b/src/Controller/Controller_Custom_Fonts.php @@ -18,6 +18,8 @@ use GFPDF\Helper\Helper_Abstract_Options; use GFPDF\Helper\Helper_Data; use GFPDF\Model\Model_Custom_Fonts; +use GFPDF\Model\Model_Font_Downloader; +use GFPDF\Model\Model_Required_Fonts; use GFPDF\Statics\Upload_Size; use GFPDF_Vendor\GravityPdf\Upload\ErrorCode; use GFPDF_Vendor\GravityPdf\Upload\Exception as UploadException; @@ -113,8 +115,17 @@ public function register_endpoints(): void { [ [ 'methods' => WP_REST_Server::READABLE, - 'callback' => [ $this, 'get_all_items' ], + 'callback' => [ $this, 'get_scoped_items' ], 'permission_callback' => Closure::fromCallable( [ $this, 'check_permissions' ] ), + 'args' => [ + 'scope' => [ + 'description' => __( 'Which to return: custom fonts, every font the Font Manager lists, or why fonts are in use', 'gravity-pdf' ), + 'type' => 'string', + 'enum' => [ 'custom', 'all', 'usage' ], + 'default' => 'custom', + 'validate_callback' => 'rest_validate_request_arg', + ], + ], ], [ @@ -138,18 +149,25 @@ public function register_endpoints(): void { '/fonts/(?P[a-z0-9\-]+)', [ 'args' => [ - 'id' => [ + 'id' => [ 'description' => __( 'Unique identifier for the object.', 'default' ), 'type' => 'string', 'validate_callback' => [ $this->model, 'check_font_id_valid' ], 'required' => true, ], + 'scope' => [ + 'description' => __( 'Whether the ID is a custom font or a downloadable core font. POST installs a core font.', 'gravity-pdf' ), + 'type' => 'string', + 'enum' => [ 'custom', 'core' ], + 'default' => 'custom', + 'validate_callback' => 'rest_validate_request_arg', + ], ], [ 'methods' => WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'update_item' ], - 'permission_callback' => Closure::fromCallable( [ $this, 'check_permissions' ] ), + 'callback' => [ $this, 'update_or_install_item' ], + 'permission_callback' => Closure::fromCallable( [ $this, 'check_item_permissions' ] ), 'args' => [ 'label' => [ 'description' => __( 'The font label used for the object', 'gravity-pdf' ), @@ -185,8 +203,8 @@ public function register_endpoints(): void { [ 'methods' => WP_REST_Server::DELETABLE, - 'callback' => [ $this, 'delete_item' ], - 'permission_callback' => Closure::fromCallable( [ $this, 'check_permissions' ] ), + 'callback' => [ $this, 'delete_scoped_item' ], + 'permission_callback' => Closure::fromCallable( [ $this, 'check_item_permissions' ] ), ], ] ); @@ -210,6 +228,44 @@ public function get_all_items(): array { return array_values( $this->model->get_custom_fonts() ); } + /** + * The fonts route's `scope`. Usage is separate from the list as it walks every form, and on Multisite every site. + * + * @return array|object|WP_Error Usage is an object, so none in use is still a JSON map + * + * @since 6.19.0 + */ + public function get_scoped_items( WP_REST_Request $request ) { + $scope = $request->get_param( 'scope' ); + if ( $scope === 'custom' ) { + return $this->get_all_items(); + } + + $downloader = $this->get_downloader(); + if ( ! $downloader instanceof Model_Font_Downloader ) { + return $downloader; + } + + return $scope === 'all' ? $downloader->get_installed_fonts() : (object) $downloader->get_font_usage(); + } + + /** + * The font route's POST: an update, or with the `core` scope an install + * + * @return array|WP_Error + * + * @since 6.19.0 + */ + public function update_or_install_item( WP_REST_Request $request ) { + if ( $request->get_param( 'scope' ) !== 'core' ) { + return $this->update_item( $request ); + } + + $downloader = $this->get_downloader(); + + return $downloader instanceof Model_Font_Downloader ? $downloader->install_font( (string) $request->get_param( 'id' ) ) : $downloader; + } + /** * Our `Create` CRUD for custom fonts * @@ -258,7 +314,7 @@ public function add_item( WP_REST_Request $request ) { FlushCache::flush( $font['id'] ); - return $font; + return $this->get_font_row( $font ); } catch ( UploadException $e ) { $message = $e->getMessage()[0] === '{' ? json_decode( $e->getMessage(), true ) : $e->getMessage(); return new WP_Error( 'font_validation_error', $message, [ 'status' => 400 ] ); @@ -377,7 +433,7 @@ public function update_item( WP_REST_Request $request ) { FlushCache::flush( $font['id'] ); - return $font; + return $this->get_font_row( $font ); } catch ( UploadException $e ) { $message = $e->getMessage()[0] === '{' ? json_decode( $e->getMessage(), true ) : $e->getMessage(); return new WP_Error( 'font_validation_error', $message, [ 'status' => 400 ] ); @@ -402,6 +458,86 @@ public function update_item( WP_REST_Request $request ) { } } + /** + * A saved font as the Font Manager lists it + * + * @since 6.19.0 + */ + protected function get_font_row( array $font ): array { + $downloader = $this->get_downloader(); + + return $downloader instanceof Model_Font_Downloader ? $downloader->get_custom_font_row( $font ) : $font; + } + + /** + * The downloader, unless an add-on has unhooked it + * + * @return Model_Font_Downloader|WP_Error + * + * @since 6.19.0 + */ + protected function get_downloader() { + $downloader = GPDFAPI::get_mvc_class( 'Model_Font_Downloader' ); + + return $downloader instanceof Model_Font_Downloader ? $downloader : new WP_Error( 'invalid_model', '', [ 'status' => 500 ] ); + } + + /** + * Changing a core font also needs the rights over the fonts folder a Multisite shares + * + * @since 6.19.0 + */ + protected function check_item_permissions( WP_REST_Request $request ): bool { + if ( ! $this->check_permissions() ) { + return false; + } + + if ( $request->get_param( 'scope' ) !== 'core' ) { + return true; + } + + $downloader = $this->get_downloader(); + + return $downloader instanceof Model_Font_Downloader && $downloader->can_change_core_fonts(); + } + + /** + * The font route's DELETE: of a custom font, or with the `core` scope a core font + * + * @return array|void|WP_Error + * + * @since 6.19.0 + */ + public function delete_scoped_item( WP_REST_Request $request ) { + if ( $request->get_param( 'scope' ) !== 'core' ) { + return $this->delete_unused_item( $request ); + } + + $downloader = $this->get_downloader(); + + return $downloader instanceof Model_Font_Downloader ? $downloader->delete_downloaded_font( (string) $request->get_param( 'id' ) ) : $downloader; + } + + /** + * Delete a custom font, refusing one in use. delete_item() doesn't, as the upgrade to 6.19 removes records whose fonts a + * source now serves, which stay in use. + * + * @return void|WP_Error + * + * @since 6.19.0 + */ + public function delete_unused_item( WP_REST_Request $request ) { + $id = $request->get_param( 'id' ); + + $required_fonts = GPDFAPI::get_mvc_class( 'Model_Required_Fonts' ); + + if ( $this->model->matches_custom_font_id( $id ) && $required_fonts instanceof Model_Required_Fonts && $required_fonts->is_font_in_use( $id ) ) { + return new WP_Error( 'font_in_use', __( 'The font is in use, so it can\'t be deleted.', 'gravity-pdf' ), [ 'status' => 409 ] ); + } + + return $this->delete_item( $request ); + } + /** * @param WP_REST_Request $request * diff --git a/src/Helper/Helper_Data.php b/src/Helper/Helper_Data.php index ea7932ba3..3acc634ab 100644 --- a/src/Helper/Helper_Data.php +++ b/src/Helper/Helper_Data.php @@ -35,6 +35,7 @@ * @property string $template_location The current path to the PDF working directory * @property string $template_location_url The current URL to the PDF working directory * @property string $template_font_location The current path to the PDF font directory + * @property string|null $template_font_location_url The default PDF font directory's URL, or null when it's been moved * @property string $template_tmp_location The current path to the PDF tmp location * @property string $mpdf_tmp_location The current path to the mPDF tmp directory (mPDF's tempDir) * @property string $multisite_template_location The current path to the multisite PDF working directory @@ -304,33 +305,82 @@ public function get_localised_script_data( Helper_Abstract_Options $options, Hel /* Font Manager */ 'fontManagerTitle' => esc_html__( 'Font Manager', 'gravity-pdf' ), - 'fontManagerSearchPlaceHolder' => esc_html__( 'Search installed fonts', 'gravity-pdf' ), - 'fontListInstalledFonts' => esc_html__( 'Installed Fonts', 'gravity-pdf' ), + 'fontManagerSearchPlaceHolder' => esc_html__( 'Search fonts', 'gravity-pdf' ), + 'fontListSize' => esc_html__( 'Size', 'gravity-pdf' ), + 'fontListInUse' => esc_html__( 'In use', 'gravity-pdf' ), + 'fontListNotInUse' => esc_html__( 'Not in use', 'gravity-pdf' ), + 'fontManagerDeleteTitle' => esc_html__( 'Delete font?', 'gravity-pdf' ), + /* translators: %s: the font name */ + 'fontManagerFontDeleted' => esc_html__( '%s deleted.', 'gravity-pdf' ), + 'fontManagerCoreHint' => esc_html__( 'Downloaded when a PDF needs them. A deleted font downloads again if it\'s needed.', 'gravity-pdf' ), + 'fontManagerCoreHintOffline' => esc_html__( 'Font downloads are turned off, so a deleted font won\'t come back on its own.', 'gravity-pdf' ), + 'fontManagerOpensNewTab' => esc_html__( '(opens in a new tab)', 'gravity-pdf' ), + 'fontsUrl' => $this->template_font_location_url ?? (string) \GPDFAPI::get_misc_class()->convert_path_to_url( $this->template_font_location ?? '' ), + /* translators: %d: the number of fonts */ + 'fontManagerFontInstalled' => esc_html__( '%d font installed', 'gravity-pdf' ), + /* translators: %d: the number of fonts */ + 'fontManagerFontsInstalled' => esc_html__( '%d fonts installed', 'gravity-pdf' ), + 'fontManagerAddNew' => esc_html__( '+ Add new font', 'gravity-pdf' ), + /* translators: %d: the number of styles */ + 'fontManagerStyle' => esc_html__( '%d style', 'gravity-pdf' ), + /* translators: %d: the number of styles */ + 'fontManagerStyles' => esc_html__( '%d styles', 'gravity-pdf' ), + 'fontManagerSetActive' => esc_html__( 'Set as active', 'gravity-pdf' ), + 'fontManagerBundledWith' => esc_html__( 'Bundled with Gravity PDF', 'gravity-pdf' ), + 'fontManagerBundledDesc' => esc_html__( 'Included with Gravity PDF, so it can\'t be deleted.', 'gravity-pdf' ), + 'fontManagerTemplateUse' => esc_html__( 'Template use:', 'gravity-pdf' ), + 'fontManagerPreview' => esc_html__( 'Preview', 'gravity-pdf' ), + 'fontManagerPreviewStyle' => esc_html__( 'Style to preview', 'gravity-pdf' ), + 'fontManagerPreviewAll' => esc_html__( 'Show all', 'gravity-pdf' ), + 'fontManagerPreviewUnsupported' => esc_html__( 'Browsers can\'t display this font file, so it can\'t be previewed.', 'gravity-pdf' ), + 'fontManagerPreviewFailed' => esc_html__( 'Couldn\'t load this style', 'gravity-pdf' ), + 'fontManagerPreviewLoad' => esc_html__( 'Load preview', 'gravity-pdf' ), + 'fontManagerPreviewUnavailable' => esc_html__( 'Previews aren\'t available because the fonts folder has no web address.', 'gravity-pdf' ), + 'fontManagerSampleText' => esc_html__( 'Sample text', 'gravity-pdf' ), + 'fontManagerPreviewText' => esc_html__( 'Sphinx of black quartz, judge my vow', 'gravity-pdf' ), + 'fontManagerPreviewUnit' => esc_html__( 'pt', 'gravity-pdf' ), + 'fontManagerFontNamePlaceholder' => esc_html__( 'e.g. Helvetica Neue, Roboto Slab, Source Sans 3', 'gravity-pdf' ), + 'fontManagerNoFile' => esc_html__( 'No .ttf file added', 'gravity-pdf' ), + 'fontManagerDropFile' => esc_html__( 'Drop .ttf to upload', 'gravity-pdf' ), + 'fontManagerUpload' => esc_html__( '+ Upload', 'gravity-pdf' ), + 'fontManagerReplace' => esc_html__( 'Replace', 'gravity-pdf' ), + /* translators: %s: the font style, e.g. Bold */ + 'fontManagerDeleteStyle' => esc_html__( 'Delete %s', 'gravity-pdf' ), 'fontUserDefinedGroup' => esc_html__( 'User-Defined Fonts', 'gravity-pdf' ), 'fontListRegular' => esc_html__( 'Regular', 'gravity-pdf' ), - 'fontListItalics' => esc_html__( 'Italics', 'gravity-pdf' ), + 'fontListItalics' => esc_html__( 'Italic', 'gravity-pdf' ), 'fontListBold' => esc_html__( 'Bold', 'gravity-pdf' ), - 'fontListBoldItalics' => esc_html__( 'Bold Italics', 'gravity-pdf' ), - 'fontManagerAddTitle' => esc_html__( 'Add Font', 'gravity-pdf' ), + 'fontListBoldItalics' => esc_html__( 'Bold Italic', 'gravity-pdf' ), + 'fontManagerAddTitle' => esc_html__( 'Add font', 'gravity-pdf' ), 'fontManagerUpdateTitle' => esc_html__( 'Update Font', 'gravity-pdf' ), - 'fontManagerAddDesc' => esc_html__( 'Install new fonts for use in your PDF documents.', 'gravity-pdf' ), 'fontManagerUpdateDesc' => esc_html__( 'Once saved, PDFs configured to use this font will have your changes applied automatically for newly-generated documents.', 'gravity-pdf' ), - 'fontManagerFontNameLabel' => esc_html__( 'Font Name', 'gravity-pdf' ), + 'fontManagerFontNameLabel' => esc_html__( 'Font name', 'gravity-pdf' ), 'fontManagerRequiredLabel' => esc_html__( '(required)', 'gravity-pdf' ), - 'fontManagerFontNameDesc' => esc_html__( 'The font name can only contain letters, numbers and spaces.', 'gravity-pdf' ), + 'fontManagerFontNameDesc' => esc_html__( 'Letters, numbers, and spaces only', 'gravity-pdf' ), 'fontManagerFontNameValidationError' => esc_html__( 'Please choose a name contains letters and/or numbers (and a space if you want it).', 'gravity-pdf' ), - 'fontManagerFontFilesLabel' => esc_html__( 'Font Files', 'gravity-pdf' ), - 'fontManagerFontFilesDesc' => esc_html__( 'Select or drag and drop your .ttf font file for the variants below. Only the Regular type is required.', 'gravity-pdf' ), + 'fontManagerFontFilesLabel' => esc_html__( 'Font files', 'gravity-pdf' ), + 'fontManagerFontFilesDesc' => esc_html__( 'Add a .ttf file for each style you want to use in your PDFs. Regular is required; Italic, Bold, and Bold Italic are optional and only used when your templates need them.', 'gravity-pdf' ), 'fontManagerFontFileRequiredRegular' => esc_html__( 'Add a .ttf font file.', 'gravity-pdf' ), 'fontManagerTemplateTooltipLabel' => esc_html__( 'View template usage', 'gravity-pdf' ), - 'fontManagerCancelButtonText' => esc_html__( '← Cancel', 'gravity-pdf' ), 'fontManagerDeleteFontConfirmation' => esc_html__( 'Are you sure you want to delete this font?', 'gravity-pdf' ), /* translators: 1: Opening tag (custom template link), 2: Opening tag (font setting link), 3: Closing tag */ 'fontManagerTemplateTooltipDesc' => esc_html__( 'Add this snippet %1$sin a custom template%3$s to selectively set the font on blocks of text. If you want to apply the font to the entire PDF, %2$suse the Font setting%3$s when configuring the PDF on the form.', 'gravity-pdf' ), - 'fontManagerAddFontAriaLabel' => esc_html__( 'Add font', 'gravity-pdf' ), 'fontManagerUpdateFontAriaLabel' => esc_html__( 'Update font', 'gravity-pdf' ), - 'fontManagerSelectFontAriaLabel' => esc_html__( 'Select font', 'gravity-pdf' ), 'fontManagerDeleteFontAriaLabel' => esc_html__( 'Delete font', 'gravity-pdf' ), + 'fontManagerTypeCustom' => esc_html__( 'Custom', 'gravity-pdf' ), + 'fontManagerTypeCore' => esc_html__( 'Core', 'gravity-pdf' ), + 'fontManagerLanguages' => esc_html__( 'Languages and scripts', 'gravity-pdf' ), + 'fontManagerTypeAvailable' => esc_html__( 'Available to install', 'gravity-pdf' ), + 'fontManagerInstall' => esc_html__( 'Install', 'gravity-pdf' ), + 'fontManagerAvailableHint' => esc_html__( 'Downloaded when a PDF needs it, or install it now to have it ready.', 'gravity-pdf' ), + /* translators: %s: the font name */ + 'fontManagerFontJustInstalled' => esc_html__( '%s installed.', 'gravity-pdf' ), + 'fontManagerTypeBundled' => esc_html__( 'Bundled', 'gravity-pdf' ), + 'fontManagerNetworkOnly' => esc_html__( 'Only a network administrator can install or delete core fonts, as every site in the network shares them.', 'gravity-pdf' ), + /* translators: %s: the font name */ + 'fontManagerCoreConfirm' => esc_html__( 'Delete %s? It downloads again when a PDF needs it.', 'gravity-pdf' ), + /* translators: %s: the font name */ + 'fontManagerCoreConfirmOffline' => esc_html__( 'Delete %s? Font downloads are turned off, so it won\'t come back on its own: copy its files into the fonts folder again to restore it.', 'gravity-pdf' ), /* Font Manager API response */ 'fontListEmpty' => esc_html__( 'Font list empty.', 'gravity-pdf' ), diff --git a/src/Model/Model_Font_Downloader.php b/src/Model/Model_Font_Downloader.php index bcab58fcb..9f3b0bc64 100644 --- a/src/Model/Model_Font_Downloader.php +++ b/src/Model/Model_Font_Downloader.php @@ -4,6 +4,7 @@ namespace GFPDF\Model; +use GFPDF\Helper\Fonts\FlushCache; use GFPDF\Helper\Helper_Abstract_Model; use GFPDF\Helper\Helper_Abstract_Options; use GFPDF\Helper\Helper_Data; @@ -11,6 +12,7 @@ use GFPDF\Helper\Mpdf\On_Demand_Font_File_Finder; use GFPDF\Statics\Git_Blob; use GFPDF_Vendor\Psr\Log\LoggerInterface; +use WP_Error; /** * @package Gravity PDF @@ -30,6 +32,18 @@ */ class Model_Font_Downloader extends Helper_Abstract_Model { + /** + * A font row's style fields, as keys to intersect a row with + * + * @since 6.19.0 + */ + protected const FONT_STYLE_KEYS = [ + 'regular' => true, + 'italics' => true, + 'bold' => true, + 'bolditalics' => true, + ]; + /** * Failed files awaiting their one retry, and those that gave up, which the admin notice lists * @@ -350,6 +364,473 @@ public function prune( ?array $keep = null ): array { return $removed; } + /** + * Every font the Font Manager lists: custom and listed bundled fonts by name, then core fonts, on disk or available to + * install, in the font dropdown's order. + * `can_delete` is whether this user may delete the font when it's unused; get_font_usage() says what's in use. + * + * @return array, base_url?: string, auto_install?: bool, can_install?: bool, languages?: string[], sample?: string, preview?: bool}> + * + * @since 6.19.0 + */ + public function get_installed_fonts(): array { + $custom = $this->font_sources->get_custom_fonts(); + $labels = $this->get_font_labels(); + $fonts = []; + + foreach ( $custom as $font ) { + $fonts[] = $this->get_custom_font_row( $font ); + } + + foreach ( $this->font_sources->get_sources() as $source ) { + if ( empty( $source['manifest'] ) ) { + foreach ( $this->font_sources->get_listed_families( $source ) as $key => $family ) { + $fonts[] = $this->get_font_row( (string) $key, $labels[ $key ] ?? (string) $key, 'bundled', $family, false, $source['dir'] ) + [ 'base_url' => PDF_PLUGIN_URL . 'fonts/' ]; + } + } + } + + usort( + $fonts, + static function ( array $a, array $b ): int { + return strcasecmp( $a['font_name'], $b['font_name'] ); + } + ); + + /* The labels are in the dropdown's order, and a family without one follows them */ + $families = array_diff_key( $this->get_core_families(), $custom ); + foreach ( array_replace( array_intersect_key( $labels, $families ), $families ) as $key => $family ) { + $row = $this->get_core_font_row( (string) $key, $family, $labels ); + if ( $row !== null ) { + $fonts[] = $row; + } + } + + return $fonts; + } + + /** + * Why each font in use can't be deleted, as phrases for the Font Manager. Walks every form, and on Multisite every + * site, so the list loads without it. + * + * @return array}> By font key + * + * @since 6.19.0 + */ + public function get_font_usage(): array { + $custom = $this->font_sources->get_custom_fonts(); + $locales = $this->font_sources->get_language_font_locales(); + $pdfs = $this->required_fonts->get_font_pdfs(); + $usage = []; + + /* Other sites only lock core fonts, which only a super admin can delete, so nobody else pays for walking them */ + foreach ( $this->required_fonts->get_font_reasons( is_multisite() && $this->can_change_core_fonts() ) as $key => $reason ) { + /* A site's custom fonts are its own, so other sites don't count */ + if ( isset( $custom[ $key ] ) ) { + $reason['sites'] = 0; + } + + $in_use = $this->get_in_use( $reason, $locales[ $key ] ?? [] ); + if ( count( $in_use ) > 0 || $reason['pdfs'] > 0 ) { + $usage[ $key ] = [ + 'reasons' => $in_use, + /* translators: %d: the number of PDFs */ + 'pdfs_label' => $reason['pdfs'] > 0 ? sprintf( _n( 'Used by %d PDF', 'Used by %d PDFs', $reason['pdfs'], 'gravity-pdf' ), $reason['pdfs'] ) : '', + 'pdfs' => $pdfs[ $key ] ?? [], + ]; + } + } + + return $usage; + } + + /** + * Every site shares the fonts folder, so only a super admin deletes core fonts on Multisite + * + * @since 6.19.0 + */ + public function can_change_core_fonts(): bool { + return ! is_multisite() || is_super_admin(); + } + + /** + * A saved custom font as the Font Manager lists it + * + * @param array $font A custom font record + * + * @return array The record with its `type`, `can_delete`, `size_label` and `file_sizes` + * + * @since 6.19.0 + */ + public function get_custom_font_row( array $font ): array { + $files = array_intersect_key( $font, self::FONT_STYLE_KEYS ); + + return array_merge( + $font, + [ + 'type' => 'custom', + 'can_delete' => true, + ], + $this->get_size_fields( $files, $this->data->template_font_location ) + ); + } + + /** + * A source's font in the shape of a custom font record + * + * @param array $files File names by style (R, B, I, BI) + * @param string $dir The folder holding the files + * + * @return array{id: string, font_name: string, type: string, regular: string, italics: string, bold: string, bolditalics: string, can_delete: bool, size_label: string, file_sizes: array} + * + * @since 6.19.0 + */ + protected function get_font_row( string $key, string $label, string $type, array $files, bool $can_delete, string $dir ): array { + $styles = [ + 'regular' => $files['R'] ?? '', + 'italics' => $files['I'] ?? '', + 'bold' => $files['B'] ?? '', + 'bolditalics' => $files['BI'] ?? '', + ]; + + return [ + 'id' => $key, + 'font_name' => $label, + 'type' => $type, + ] + $styles + [ + 'can_delete' => $can_delete, + ] + $this->get_size_fields( $styles, $dir ); + } + + /** + * A core family's Font Manager row, `available` when none of it is on disk + * + * @param array{files: array, sizes: array, bytes: int} $family + * @param array $labels Font list labels by key + * + * @return array|null Null for a family with nothing on disk that can't or shouldn't be installed + * + * @since 6.19.0 + */ + protected function get_core_font_row( string $key, array $family, array $labels ): ?array { + $on_disk = array_filter( + $family['files'], + static function ( string $file ) use ( $family ): bool { + return isset( $family['sizes'][ $file ] ); + } + ); + + $can_download = $this->can_download_all( $family['files'] ); + $installed = count( $on_disk ) > 0; + + /* A family without a label can't be chosen for a PDF (a fallback or barcode font), so isn't offered */ + if ( ! $installed && ( ! $can_download || ! isset( $labels[ $key ] ) ) ) { + return null; + } + + $can_change = $this->can_change_core_fonts(); + $row = $this->get_font_row( $key, $labels[ $key ] ?? $key, $installed ? 'core' : 'available', $installed ? $on_disk : $family['files'], $installed && $can_change, $this->data->template_font_location ); + + if ( ! $installed ) { + $manifest = $this->font_sources->get_manifest(); + + $row['size_label'] = (string) size_format( $family['bytes'], 1 ); + $row['file_sizes'] = array_map( + static function ( string $file ) use ( $manifest ): int { + return (int) $manifest[ $file ]['size']; + }, + array_filter( array_intersect_key( $row, self::FONT_STYLE_KEYS ) ) + ); + + /* Nothing on this site to preview yet, so the browser reads it from where it would be downloaded */ + $row['base_url'] = dirname( $manifest[ reset( $family['files'] ) ]['url'] ) . '/'; + } + + $languages = $this->font_sources->get_font_languages()[ $key ] ?? []; + + return $row + [ + 'auto_install' => $can_download, + 'can_install' => $can_change && $can_download && count( $on_disk ) < count( $family['files'] ), + 'languages' => $languages['languages'] ?? [], + 'sample' => $languages['sample'] ?? '', + 'preview' => $languages['preview'] ?? true, + ]; + } + + /** + * A core family's row as it stands now its files have changed + * + * @since 6.19.0 + */ + protected function get_current_core_font_row( string $key ): ?array { + return $this->get_core_font_row( $key, $this->get_core_families()[ $key ], $this->get_font_labels() ); + } + + /** + * A font's sizes, found by name in the folder as stored custom font paths can predate a move + * + * @param string[] $files File names or paths by style, '' for a style it doesn't have + * + * @return array{size_label: string, file_sizes: array} `file_sizes` lets the preview ask before loading a large file + * + * @since 6.19.0 + */ + protected function get_size_fields( array $files, string $dir ): array { + $sizes = []; + foreach ( array_filter( $files ) as $style => $file ) { + $path = $dir . basename( $file ); + if ( is_file( $path ) ) { + $sizes[ $style ] = (int) filesize( $path ); + } + } + + return [ + 'size_label' => count( $sizes ) > 0 ? (string) size_format( array_sum( $sizes ), 1 ) : '', + 'file_sizes' => $sizes, + ]; + } + + /** + * Delete an unused downloaded family's files whatever their hash, keeping the licence files + * + * @return array{id: string, removed: string[], bytes: int, font: array|null}|WP_Error `font` is its row now, if it can + * be installed again. 404 when + * nothing is on disk, 409 when in use + * + * @since 6.19.0 + */ + public function delete_downloaded_font( string $key ) { + $family = $this->get_core_families()[ $key ] ?? null; + if ( $family === null || count( $family['sizes'] ) === 0 ) { + return new WP_Error( 'font_not_downloaded', __( 'The font is not downloaded.', 'gravity-pdf' ), [ 'status' => 404 ] ); + } + + $files = array_values( array_unique( $family['files'] ) ); + if ( $this->required_fonts->is_font_in_use( $key, is_multisite() ) ) { + return new WP_Error( 'font_in_use', __( 'The font is in use, so it would be downloaded again.', 'gravity-pdf' ), [ 'status' => 409 ] ); + } + + $removed = []; + $bytes = 0; + foreach ( $files as $file ) { + $this->clear_failure( $file ); + $this->delete_back_off( $file ); + + $size = $family['sizes'][ $file ] ?? null; + if ( $size === null ) { + continue; + } + + if ( ! wp_delete_file_from_directory( $this->get_font_path( $file ), $this->data->template_font_location ) ) { + $this->log->warning( 'Downloaded font file could not be removed', [ 'name' => $file ] ); + continue; + } + + $removed[] = $file; + $bytes += $size; + } + + FlushCache::flush( $key ); + + $this->log->notice( + 'Downloaded font deleted', + [ + 'font' => $key, + 'files' => $removed, + 'bytes' => $bytes, + ] + ); + + return [ + 'id' => $key, + 'removed' => $removed, + 'bytes' => $bytes, + 'font' => $this->get_current_core_font_row( $key ), + ]; + } + + /** + * Download a core family's missing files, ignoring any back-off as someone asked for it + * + * @return array|WP_Error The font's row. 404 for a family not on the manifest or replaced by a custom font, 409 with + * downloads turned off, 502 when a file couldn't be downloaded + * + * @since 6.19.0 + */ + public function install_font( string $key ) { + $family = $this->get_core_families()[ $key ] ?? null; + if ( $family === null || isset( $this->font_sources->get_custom_fonts()[ $key ] ) ) { + return new WP_Error( 'font_not_available', __( 'The font is not available to install.', 'gravity-pdf' ), [ 'status' => 404 ] ); + } + + if ( ! $this->can_download_all( $family['files'] ) ) { + return new WP_Error( 'font_downloads_off', __( 'Font downloads are turned off.', 'gravity-pdf' ), [ 'status' => 409 ] ); + } + + $failed = []; + foreach ( array_unique( $family['files'] ) as $file ) { + $this->delete_back_off( $file ); + + if ( ! $this->download( $file ) ) { + $failed[] = $file; + } + } + + if ( count( $failed ) > 0 ) { + $this->log->error( + 'Font could not be installed', + [ + 'font' => $key, + 'files' => $failed, + ] + ); + + return new WP_Error( 'font_download_failed', __( 'The font could not be downloaded. Please try again.', 'gravity-pdf' ), [ 'status' => 502 ] ); + } + + $this->log->notice( 'Font installed', [ 'font' => $key ] ); + + return $this->get_current_core_font_row( $key ); + } + + /** + * The downloadable sources' families: their files on the manifest by style (R, B, I, BI), the sizes of those on disk + * and the bytes of them all + * + * @return array, sizes: array, bytes: int}> + * + * @since 6.19.0 + */ + protected function get_core_families(): array { + $families = []; + + foreach ( $this->font_sources->get_sources() as $id => $source ) { + if ( empty( $source['manifest'] ) ) { + continue; + } + + $manifest = $this->font_sources->get_source_manifest( (string) $id ); + foreach ( $source['families'] ?? [] as $key => $family ) { + $key = (string) $key; + if ( ! empty( $families[ $key ]['sizes'] ) || ! is_array( $family ) ) { + continue; + } + + $files = []; + foreach ( [ 'R', 'B', 'I', 'BI' ] as $style ) { + $file = $family[ $style ] ?? null; + if ( is_string( $file ) && isset( $manifest[ $file ] ) && $this->font_sources->is_core_font_file( $file ) ) { + $files[ $style ] = $file; + } + } + + $sizes = []; + $bytes = 0; + foreach ( $files as $file ) { + $bytes += (int) $manifest[ $file ]['size']; + $path = $this->get_font_path( $file ); + if ( is_file( $path ) ) { + $sizes[ $file ] = (int) filesize( $path ); + } + } + + if ( count( $files ) > 0 ) { + $families[ $key ] = [ + 'files' => $files, + 'sizes' => $sizes, + 'bytes' => $bytes, + ]; + } + } + } + + return $families; + } + + /** + * Font list labels by key + * + * @return array + * + * @since 6.19.0 + */ + protected function get_font_labels(): array { + $labels = []; + foreach ( (array) $this->options->get_installed_fonts() as $group ) { + foreach ( is_array( $group ) ? $group : [] as $key => $label ) { + if ( is_string( $label ) && ! isset( $labels[ $key ] ) ) { + $labels[ (string) $key ] = $label; + } + } + } + + return $labels; + } + + /** + * A font's get_font_reasons() entry as phrases for the Font Manager, other than its PDFs, which are linked to + * + * @param array $reasons A get_font_reasons() entry + * @param string[] $locales The locales it's a language font for + * + * @return string[] + * + * @since 6.19.0 + */ + protected function get_in_use( array $reasons, array $locales = [] ): array { + $phrases = []; + + if ( $reasons['default'] ) { + $phrases[] = __( 'Default font', 'gravity-pdf' ); + } + + if ( $reasons['language'] ) { + $names = array_map( [ $this, 'get_language_name' ], $locales ); + /* translators: %s: the site's languages, e.g. ไทย */ + $phrases[] = count( $names ) > 0 ? sprintf( __( 'Site language (%s)', 'gravity-pdf' ), implode( ', ', $names ) ) : __( 'Site language', 'gravity-pdf' ); + } + + if ( $reasons['source'] ) { + $phrases[] = __( 'Required by an add-on', 'gravity-pdf' ); + } + + if ( $reasons['sites'] > 0 ) { + /* translators: %d: the number of sites */ + $phrases[] = sprintf( _n( 'Used on %d other site', 'Used on %d other sites', $reasons['sites'], 'gravity-pdf' ), $reasons['sites'] ); + } + + return $phrases; + } + + /** + * A locale's native name when WordPress has the translations list cached, else the locale + * + * @since 6.19.0 + */ + protected function get_language_name( string $locale ): string { + $translations = get_site_transient( 'available_translations' ); + $name = is_array( $translations ) ? ( $translations[ $locale ]['native_name'] ?? '' ) : ''; + + return is_string( $name ) && $name !== '' ? $name : $locale; + } + + /** + * Whether every file would download again if deleted + * + * @param string[] $files + * + * @since 6.19.0 + */ + protected function can_download_all( array $files ): bool { + foreach ( $files as $file ) { + if ( ! $this->is_auto_install_enabled( $file ) ) { + return false; + } + } + + return true; + } + /** * @param array{url: string, size: int, sha: string} $entry * diff --git a/src/Model/Model_Font_Languages.php b/src/Model/Model_Font_Languages.php new file mode 100644 index 000000000..48f6d8751 --- /dev/null +++ b/src/Model/Model_Font_Languages.php @@ -0,0 +1,199 @@ + By font key + * + * @since 6.19.0 + */ + public function get_font_languages(): array { + $latin = __( 'Latin', 'gravity-pdf' ); + $greek = __( 'Greek', 'gravity-pdf' ); + $cyrillic = __( 'Cyrillic', 'gravity-pdf' ); + $armenian = __( 'Armenian', 'gravity-pdf' ); + $georgian = __( 'Georgian', 'gravity-pdf' ); + $hebrew = __( 'Hebrew', 'gravity-pdf' ); + $arabic = __( 'Arabic', 'gravity-pdf' ); + $persian = __( 'Persian', 'gravity-pdf' ); + $urdu = __( 'Urdu', 'gravity-pdf' ); + $syriac = __( 'Syriac', 'gravity-pdf' ); + $thaana = __( 'Thaana', 'gravity-pdf' ); + $thai = __( 'Thai', 'gravity-pdf' ); + $lao = __( 'Lao', 'gravity-pdf' ); + $cherokee = __( 'Cherokee', 'gravity-pdf' ); + $ucas = __( 'Canadian Aboriginal', 'gravity-pdf' ); + $tifinagh = __( 'Tifinagh', 'gravity-pdf' ); + $burmese = __( 'Burmese', 'gravity-pdf' ); + $tibetan = __( 'Tibetan', 'gravity-pdf' ); + $sinhala = __( 'Sinhala', 'gravity-pdf' ); + $korean = __( 'Korean', 'gravity-pdf' ); + $ethiopic = __( 'Ethiopic', 'gravity-pdf' ); + $gothic = __( 'Gothic', 'gravity-pdf' ); + $coptic = __( 'Coptic', 'gravity-pdf' ); + $italic = __( 'Old Italic', 'gravity-pdf' ); + $ugaritic = __( 'Ugaritic', 'gravity-pdf' ); + $phoenic = __( 'Phoenician', 'gravity-pdf' ); + $linear_b = __( 'Linear B', 'gravity-pdf' ); + $cypriot = __( 'Cypriot', 'gravity-pdf' ); + $tai_le = __( 'Tai Le', 'gravity-pdf' ); + $bugis = __( 'Buginese', 'gravity-pdf' ); + $vai = __( 'Vai', 'gravity-pdf' ); + $runic = __( 'Runic', 'gravity-pdf' ); + $devan = __( 'Devanagari', 'gravity-pdf' ); + $bengali = __( 'Bengali', 'gravity-pdf' ); + $gurmukhi = __( 'Gurmukhi', 'gravity-pdf' ); + $gujarati = __( 'Gujarati', 'gravity-pdf' ); + $oriya = __( 'Oriya', 'gravity-pdf' ); + $tamil = __( 'Tamil', 'gravity-pdf' ); + $telugu = __( 'Telugu', 'gravity-pdf' ); + $kannada = __( 'Kannada', 'gravity-pdf' ); + $malayalm = __( 'Malayalam', 'gravity-pdf' ); + $khmer = __( 'Khmer', 'gravity-pdf' ); + + $dejavu_sans = [ $latin, $greek, $cyrillic, $armenian, $georgian, $hebrew, $arabic, $lao, __( "N'Ko", 'gravity-pdf' ), $tifinagh, $ucas ]; + $dejavu_serif = [ $latin, $greek, $cyrillic, $armenian, $georgian ]; + $burmese_font = [ + 'languages' => [ $burmese ], + 'sample' => 'မင်္ဂလာပါ', + ]; + + return [ + 'dejavusanscondensed' => [ 'languages' => $dejavu_sans ], + 'dejavusans' => [ 'languages' => $dejavu_sans ], + 'dejavuserifcondensed' => [ 'languages' => $dejavu_serif ], + 'dejavuserif' => [ 'languages' => $dejavu_serif ], + 'dejavusansmono' => [ 'languages' => [ $latin, $greek, $cyrillic, $armenian, $georgian, $arabic, $lao ] ], + 'freesans' => [ 'languages' => [ $latin, $greek, $cyrillic, $armenian, $georgian, $hebrew, $syriac, $devan, $bengali, $gurmukhi, $malayalm, $cherokee, $ucas, $vai, $tifinagh, $ugaritic, $phoenic ] ], + 'freeserif' => [ 'languages' => [ $latin, $greek, $cyrillic, $armenian, $georgian, $hebrew, $arabic, $thaana, $devan, $bengali, $gurmukhi, $gujarati, $oriya, $tamil, $malayalm, $sinhala, $thai, $ethiopic, $cherokee, $tai_le, $bugis, $gothic, $coptic ] ], + 'freemono' => [ 'languages' => [ $latin, $greek, $cyrillic, $armenian, $georgian, $hebrew, $arabic, $cherokee, $runic, $italic ] ], + 'mph2bdamase' => [ 'languages' => [ $latin, $greek, $cyrillic, $armenian, $georgian, $hebrew, $arabic, $thaana, $cherokee, __( 'Limbu', 'gravity-pdf' ), $tai_le, $bugis, $linear_b, $italic, $gothic, $ugaritic, $phoenic, $cypriot, $coptic, $tifinagh ] ], + 'quivira' => [ 'languages' => [ $latin, $greek, $cyrillic, $armenian, $georgian, $hebrew, $thai, $cherokee, $ucas, __( 'Ogham', 'gravity-pdf' ), $runic, __( 'Tagalog', 'gravity-pdf' ), $vai, $italic, $gothic, $phoenic, $coptic, $tifinagh ] ], + 'sun-exta' => [ + 'languages' => [ __( 'Chinese', 'gravity-pdf' ), __( 'Japanese', 'gravity-pdf' ), $korean, $latin, $greek, $cyrillic, $armenian, $georgian, $hebrew, $arabic, $syriac, $thaana, $devan, $bengali, $gurmukhi, $gujarati, $oriya, $tamil, $telugu, $kannada, $malayalm, $thai, $lao, $tibetan, $burmese, $ethiopic, $cherokee, $ucas, $khmer, __( 'Mongolian', 'gravity-pdf' ) ], + 'sample' => '你好,世界 こんにちは 안녕하세요', + /* Its vhea table claims more metrics than it has glyphs, so browsers reject the file after downloading 22 MB */ + 'preview' => false, + ], + 'unbatang' => [ + 'languages' => [ $korean ], + 'sample' => '안녕하세요', + ], + 'lohitkannada' => [ + 'languages' => [ $kannada ], + 'sample' => 'ನಮಸ್ಕಾರ', + ], + 'pothana2000' => [ + 'languages' => [ $telugu ], + 'sample' => 'నమస్కారం', + ], + 'xbriyaz' => [ + 'languages' => [ $arabic, $persian, $urdu ], + 'sample' => 'مرحبا بالعالم', + ], + 'lateef' => [ + 'languages' => [ $arabic, $persian, $urdu, __( 'Sindhi', 'gravity-pdf' ) ], + 'sample' => 'مرحبا بالعالم', + ], + 'kfgqpcuthmantahanaskh' => [ + 'languages' => [ __( 'Quranic Arabic', 'gravity-pdf' ) ], + 'sample' => 'بسم الله الرحمن الرحيم', + ], + 'estrangeloedessa' => [ + 'languages' => [ $syriac ], + 'sample' => 'ܫܠܡܐ', + ], + 'kaputaunicode' => [ + 'languages' => [ $sinhala ], + 'sample' => 'ආයුබෝවන්', + ], + 'abyssinicasil' => [ + 'languages' => [ __( 'Amharic', 'gravity-pdf' ), __( 'Tigrinya', 'gravity-pdf' ), __( "Ge'ez", 'gravity-pdf' ) ], + 'sample' => 'ሰላም', + ], + 'aboriginalsans' => [ + 'languages' => [ $cherokee, __( 'Cree', 'gravity-pdf' ), __( 'Inuktitut', 'gravity-pdf' ), __( 'Ojibwe', 'gravity-pdf' ) ], + 'sample' => 'ᎣᏏᏲ ᐊᐃ', + ], + 'jomolhari' => [ + 'languages' => [ $tibetan, __( 'Dzongkha', 'gravity-pdf' ) ], + 'sample' => 'བཀྲ་ཤིས་བདེ་ལེགས།', + ], + 'sundaneseunicode' => [ + 'languages' => [ __( 'Sundanese', 'gravity-pdf' ) ], + 'sample' => 'ᮃ ᮄ ᮅ ᮊ ᮋ', + ], + 'taiheritagepro' => [ + 'languages' => [ __( 'Tai Dam', 'gravity-pdf' ) ], + 'sample' => 'ꪀ ꪁ ꪂ ꪃ ꪄ', + ], + 'aegyptus' => [ + 'languages' => [ __( 'Ancient Egyptian', 'gravity-pdf' ) ], + 'sample' => '𓀀 𓁐 𓃀 𓆣 𓋹', + ], + 'akkadian' => [ + 'languages' => [ __( 'Akkadian', 'gravity-pdf' ), __( 'Sumerian', 'gravity-pdf' ) ], + 'sample' => '𒀀 𒀭 𒁲 𒂗 𒆠', + ], + 'aegean' => [ + 'languages' => [ $greek, $linear_b, $cypriot, $phoenic, $ugaritic, $italic ], + 'sample' => '𐀀 𐀁 𐀂 𐀃 𐀄', + ], + 'lannaalif' => [ + 'languages' => [ __( 'Northern Thai', 'gravity-pdf' ) ], + 'sample' => 'ᨠ ᨡ ᨢ ᨣ ᨤ', + ], + 'daibannasilbook' => [ + 'languages' => [ __( 'Tai Lü', 'gravity-pdf' ) ], + 'sample' => 'ᦀ ᦁ ᦂ ᦃ ᦄ', + ], + 'garuda' => [ + 'languages' => [ $thai ], + 'sample' => 'สวัสดี', + ], + 'khmeros' => [ + 'languages' => [ $khmer ], + 'sample' => 'សួស្តី', + ], + 'dhyana' => [ + 'languages' => [ $lao ], + 'sample' => 'ສະບາຍດີ', + ], + 'tharlon' => $burmese_font, + 'padaukbook' => $burmese_font, + 'ayar' => $burmese_font, + /* Zawgyi orders vowels differently to Unicode, so a word would come out wrong */ + 'zawgyi-one' => [ + 'languages' => [ $burmese ], + 'sample' => 'က ခ ဂ ဃ င', + ], + 'taameydavidclm' => [ + 'languages' => [ $hebrew ], + 'sample' => 'שלום עולם', + ], + ]; + } +} diff --git a/src/Model/Model_Font_Sources.php b/src/Model/Model_Font_Sources.php index 48af64c9d..730166388 100644 --- a/src/Model/Model_Font_Sources.php +++ b/src/Model/Model_Font_Sources.php @@ -109,6 +109,12 @@ class Model_Font_Sources extends Helper_Abstract_Model { */ protected $language_font_keys; + /** + * @var array|null Locales by font key, once get_language_font_locales() maps them + * @since 6.19.0 + */ + protected $language_font_locales; + /** * Every source's downloadable files by name, once get_manifest() reads them * @@ -117,6 +123,12 @@ class Model_Font_Sources extends Helper_Abstract_Model { */ protected $manifest; + /** + * @var array|null What each core font supports, once read + * @since 6.19.0 + */ + protected $font_languages; + /** * Each source's manifest by ID, once get_source_manifest() reads it * @@ -160,6 +172,17 @@ public function get_sources(): array { return $this->sources; } + /** + * The custom fonts by ID + * + * @return array + * + * @since 6.19.0 + */ + public function get_custom_fonts(): array { + return $this->custom_fonts->get_custom_fonts(); + } + /** * Whether a font source provides the mPDF font key * @@ -303,14 +326,30 @@ public function get_font_data(): array { return $this->font_data; } + /** + * What each core font supports, by font key + * + * @return array + * + * @since 6.19.0 + */ + public function get_font_languages(): array { + if ( $this->font_languages === null ) { + $this->font_languages = ( new Model_Font_Languages() )->get_font_languages(); + } + + return $this->font_languages; + } + /** * Forget the font data and language fonts, e.g. once a language pack installs * * @since 6.19.0 */ public function flush(): void { - $this->font_data = null; - $this->language_font_keys = null; + $this->font_data = null; + $this->language_font_keys = null; + $this->language_font_locales = null; delete_transient( self::LANGUAGE_FONT_KEYS ); } @@ -335,20 +374,7 @@ public function get_language_font_keys(): array { return $this->language_font_keys; } - $mapper = $this->get_language_mapper(); - $locales = array_filter( array_merge( (array) get_available_languages(), [ $locale ] ), 'is_string' ); - $keys = []; - - foreach ( array_unique( $locales ) as $language ) { - /* Adobe CJK fonts are off in Gravity PDF's config */ - $options = $mapper->getLanguageOptions( str_replace( '_', '-', $language ), false ); - $key = is_array( $options ) && is_string( $options[1] ?? null ) ? $options[1] : ''; - if ( $key !== '' ) { - $keys[] = $key; - } - } - - $this->language_font_keys = array_values( array_unique( $keys ) ); + $this->language_font_keys = array_map( 'strval', array_keys( $this->get_language_font_locales() ) ); set_transient( self::LANGUAGE_FONT_KEYS, @@ -362,6 +388,50 @@ public function get_language_font_keys(): array { return $this->language_font_keys; } + /** + * The installed languages and the locale mapped to their mPDF font keys, once per request + * + * @return array Locales by font key + * + * @since 6.19.0 + */ + public function get_language_font_locales(): array { + if ( $this->language_font_locales !== null ) { + return $this->language_font_locales; + } + + $this->language_font_locales = $this->map_language_font_locales( + array_merge( (array) get_available_languages(), [ get_locale() ] ) + ); + + return $this->language_font_locales; + } + + /** + * Map locales to the mPDF font keys they need, skipping those the default font covers + * + * @param mixed[] $locales + * + * @return array Locales by font key + * + * @since 6.19.0 + */ + public function map_language_font_locales( array $locales ): array { + $mapper = $this->get_language_mapper(); + $keys = []; + + foreach ( array_unique( array_filter( $locales, 'is_string' ) ) as $language ) { + /* Adobe CJK fonts are off in Gravity PDF's config */ + $options = $mapper->getLanguageOptions( str_replace( '_', '-', $language ), false ); + $key = is_array( $options ) && is_string( $options[1] ?? null ) ? $options[1] : ''; + if ( $key !== '' ) { + $keys[ $key ][] = $language; + } + } + + return $keys; + } + /** * mPDF's backupSubsFont, Arimo first. mPDF reads which characters a font has from its ranges file and downloads only a * font that has them, but Sun-ExtA is listed only once it's on disk, as it would download 22.9 MB mid-render. @@ -554,8 +624,7 @@ public function register_font_directories( $config ): array { * @since 6.19.0 */ public function add_font_list_groups( $fonts ): array { - $fonts = is_array( $fonts ) ? $fonts : []; - $custom_fonts = $this->custom_fonts->get_custom_fonts(); + $fonts = is_array( $fonts ) ? $fonts : []; foreach ( $this->get_sources() as $source ) { if ( empty( $source['group'] ) ) { @@ -563,10 +632,8 @@ public function add_font_list_groups( $fonts ): array { } $list = []; - foreach ( $source['families'] as $key => $family ) { - if ( ( $family['listed'] ?? true ) && ! isset( $custom_fonts[ $key ] ) ) { - $list[ $key ] = $family['name'] ?? $key; - } + foreach ( $this->get_listed_families( $source ) as $key => $family ) { + $list[ $key ] = $family['name'] ?? $key; } $group = $fonts[ $source['group'] ] ?? []; @@ -576,6 +643,27 @@ public function add_font_list_groups( $fonts ): array { return $fonts; } + /** + * A source's families that users can choose: not hidden, and not replaced by a custom font of the same key + * + * @param array $source A get_sources() value + * + * @return array + * + * @since 6.19.0 + */ + public function get_listed_families( array $source ): array { + $custom_fonts = $this->custom_fonts->get_custom_fonts(); + + return array_filter( + $source['families'] ?? [], + static function ( $family, $key ) use ( $custom_fonts ): bool { + return is_array( $family ) && ( $family['listed'] ?? true ) && ! isset( $custom_fonts[ $key ] ); + }, + ARRAY_FILTER_USE_BOTH + ); + } + /** * Remove the custom font records a source now serves (keyed by one of its families, every file a known copy). * Edited records stay and keep overriding. diff --git a/src/Model/Model_Install.php b/src/Model/Model_Install.php index 68c56e4a5..5ee3180a3 100644 --- a/src/Model/Model_Install.php +++ b/src/Model/Model_Install.php @@ -198,7 +198,11 @@ public function setup_template_location() { $this->data->template_location_url = trailingslashit( apply_filters( 'gfpdf_template_location_uri', $template_url, $working_folder, $upload_dir_url ) ); /* needs to be accessible from the web */ /* See https://docs.gravitypdf.com/developers/filters/gfpdf_font_location/ for more details about this filter */ - $this->data->template_font_location = trailingslashit( apply_filters( 'gfpdf_font_location', $this->data->template_location . 'fonts/', $working_folder, $upload_dir ) ); /* can be in a directory not accessible via the web */ + $default_fonts = $this->data->template_location . 'fonts/'; + $this->data->template_font_location = trailingslashit( apply_filters( 'gfpdf_font_location', $default_fonts, $working_folder, $upload_dir ) ); /* can be in a directory not accessible via the web */ + + /* Null once the folder is moved, as only the Font Manager needs that path mapped to a URL */ + $this->data->template_font_location_url = $this->data->template_font_location === $default_fonts ? $this->data->template_location_url . 'fonts/' : null; /* See https://docs.gravitypdf.com/developers/filters/gfpdf_tmp_location/ for more details about this filter */ $this->data->template_tmp_location = trailingslashit( apply_filters( 'gfpdf_tmp_location', $this->data->template_location . 'tmp/', $working_folder, $upload_dir_url ) ); /* encouraged to move this to a directory not accessible via the web */ diff --git a/src/Model/Model_Required_Fonts.php b/src/Model/Model_Required_Fonts.php index d5c3705ae..b17218bb5 100644 --- a/src/Model/Model_Required_Fonts.php +++ b/src/Model/Model_Required_Fonts.php @@ -55,10 +55,10 @@ class Model_Required_Fonts extends Helper_Abstract_Model { protected $log; /** - * @var string[]|null + * @var array>|null * @since 6.19.0 */ - protected $selected_font_keys; + protected $font_pdfs; /** * Work out the required fonts from the font sources and the saved settings, logging forms it can't read @@ -77,7 +77,7 @@ public function __construct( Model_Font_Sources $font_sources, Helper_Abstract_O * @since 6.19.0 */ public function flush(): void { - $this->selected_font_keys = null; + $this->font_pdfs = null; } /** @@ -114,15 +114,9 @@ public function get_font_files( $key ): array { * @since 6.19.0 */ public function get_required_font_files(): array { - $keys = array_merge( - [ $this->normalise_font( $this->options->get_option( 'default_font', '' ) ) ], - $this->font_sources->get_language_font_keys(), - $this->get_selected_font_keys() - ); - $files = []; - foreach ( array_unique( $keys ) as $key ) { - $files = array_merge( $files, $this->get_font_files( $key ) ); + foreach ( array_keys( $this->get_font_reasons() ) as $key ) { + $files = array_merge( $files, $this->get_font_files( (string) $key ) ); } foreach ( $this->font_sources->get_sources() as $id => $source ) { @@ -134,6 +128,106 @@ public function get_required_font_files(): array { return array_values( array_unique( $files ) ); } + /** + * Why each required font key is required: the default font, a site language's, selected by that many PDFs, in a + * required source and, with $network, used by that many other sites + * + * @return array + * + * @since 6.19.0 + */ + public function get_font_reasons( bool $network = false ): array { + $none = [ + 'default' => false, + 'language' => false, + 'pdfs' => 0, + 'source' => false, + 'sites' => 0, + ]; + $reasons = $this->get_settings_font_reasons(); + + foreach ( array_map( 'count', $this->get_font_pdfs() ) as $key => $count ) { + $reasons[ $key ]['pdfs'] = $count; + } + + foreach ( $network ? $this->get_other_site_font_counts() : [] as $key => $count ) { + $reasons[ $key ]['sites'] = $count; + } + + return array_map( + static function ( array $reason ) use ( $none ): array { + return array_merge( $none, $reason ); + }, + $reasons + ); + } + + /** + * Whether anything requires the font key, so deleting it would break or re-download it + * + * @since 6.19.0 + */ + public function is_font_in_use( string $key, bool $network = false ): bool { + if ( isset( $this->get_settings_font_reasons()[ $key ] ) || isset( $this->font_pdfs[ $key ] ) ) { + return true; + } + + if ( $this->font_pdfs === null && $this->yields_font( $this->each_pdf_font(), $key ) ) { + return true; + } + + foreach ( $network ? $this->get_other_site_ids() : [] as $site_id ) { + if ( $this->yields_font( $this->each_site_font_key( $site_id ), $key ) ) { + return true; + } + } + + return false; + } + + /** + * Whether a generator keyed by font key reaches the font, reading no further than it + * + * @since 6.19.0 + */ + protected function yields_font( \Generator $fonts, string $key ): bool { + foreach ( $fonts as $font => $unused ) { + if ( $font === $key ) { + return true; + } + } + + return false; + } + + /** + * The reasons that need no form read: the default font, a site language's and a required source's + * + * @return array> + * + * @since 6.19.0 + */ + protected function get_settings_font_reasons(): array { + $reasons = []; + + $default = $this->normalise_font( $this->options->get_option( 'default_font', '' ) ); + if ( $default !== '' ) { + $reasons[ $default ]['default'] = true; + } + + foreach ( $this->font_sources->get_language_font_keys() as $key ) { + $reasons[ $key ]['language'] = true; + } + + foreach ( $this->font_sources->get_sources() as $source ) { + foreach ( empty( $source['required'] ) ? [] : array_keys( $source['families'] ?? [] ) as $key ) { + $reasons[ $key ]['source'] = true; + } + } + + return $reasons; + } + /** * The registered fonts among a PDF's settings, so template font pickers count too * @@ -267,60 +361,172 @@ public function get_form_pdfs( $display_meta, int $form_id ): array { } /** - * The fonts selected in every active form's PDFs + * The active forms' PDFs that select each font, read once per request * - * @return string[] + * @return array> PDFs by font key + * + * @since 6.19.0 + */ + public function get_font_pdfs(): array { + if ( $this->font_pdfs !== null ) { + return $this->font_pdfs; + } + + $pdfs = []; + + foreach ( $this->each_pdf_font() as $font => list( $row, $pdf_id, $pdf ) ) { + $pdfs[ $font ][] = [ + 'form' => (string) ( $row['title'] ?? '' ), + 'name' => is_string( $pdf['name'] ?? null ) ? $pdf['name'] : '', + 'url' => admin_url( sprintf( 'admin.php?page=gf_edit_forms&view=settings&subview=PDF&id=%d&pid=%s', (int) $row['form_id'], rawurlencode( $pdf_id ) ) ), + ]; + } + + $this->font_pdfs = $pdfs; + + return $pdfs; + } + + /** + * Each font selected by each PDF of a site's active forms, read lazily so a caller can stop at the one it wants + * + * @param string $prefix Another site's table prefix, or '' for this site + * + * @return \Generator Font key => form row, PDF ID and PDF settings * * @since 6.19.0 */ - protected function get_selected_font_keys(): array { - if ( $this->selected_font_keys === null ) { - $keys = []; - - /* A page at a time, so a site with many forms doesn't hold every form's meta at once */ - $offset = 0; - do { - $page = $this->get_pdf_form_meta( $offset, $this->form_page ); - foreach ( $page as $row ) { - foreach ( $this->get_form_pdfs( $row['display_meta'], (int) $row['form_id'] ) as $pdf ) { - $keys = array_merge( $keys, $this->get_pdf_fonts( $pdf ) ); + protected function each_pdf_font( string $prefix = '' ): \Generator { + /* A page at a time, so a site with many forms doesn't hold every form's meta at once */ + $after = 0; + do { + $page = $this->get_pdf_form_meta( $after, $this->form_page, $prefix ); + foreach ( $page as $row ) { + $after = (int) $row['form_id']; + foreach ( $this->get_form_pdfs( $row['display_meta'], (int) $row['form_id'] ) as $pdf_id => $pdf ) { + foreach ( $this->get_pdf_fonts( $pdf ) as $font ) { + yield $font => [ $row, (string) $pdf_id, $pdf ]; } } + } + + $full = count( $page ) === $this->form_page; + } while ( $full ); + } + + /** + * How many other sites of the network use each font. Read without switch_to_blog(), so no other site's plugins or + * filters run. + * + * @return array + * + * @since 6.19.0 + */ + protected function get_other_site_font_counts(): array { + $counts = []; + + foreach ( $this->get_other_site_ids() as $site_id ) { + foreach ( $this->get_site_font_keys( $site_id ) as $key ) { + $counts[ $key ] = ( $counts[ $key ] ?? 0 ) + 1; + } + } + + return $counts; + } + + /** + * @return int[] Every other site of the network, or none when not a Multisite + * + * @since 6.19.0 + */ + protected function get_other_site_ids(): array { + if ( ! is_multisite() ) { + return []; + } - $offset += $this->form_page; - $full = count( $page ) === $this->form_page; - } while ( $full ); + return array_map( + 'intval', + get_sites( + [ + 'fields' => 'ids', + 'number' => 0, + 'site__not_in' => [ get_current_blog_id() ], + ] + ) + ); + } - $this->selected_font_keys = array_values( array_unique( $keys ) ); + /** + * The fonts a site uses (its default font, locale and PDF fonts), read from its tables by prefix rather than by + * switching to it + * + * @return string[] Font keys + * + * @since 6.19.0 + */ + public function get_site_font_keys( int $site_id ): array { + return array_map( 'strval', array_keys( iterator_to_array( $this->each_site_font_key( $site_id ) ) ) ); + } + + /** + * Each font key a site uses, the settings' before its forms' so a caller can stop without reading them + * + * @return \Generator Keyed by font key, which may repeat + * + * @since 6.19.0 + */ + protected function each_site_font_key( int $site_id ): \Generator { + global $wpdb; + + $prefix = $wpdb->get_blog_prefix( $site_id ); + $options = $wpdb->get_results( "SELECT option_name, option_value FROM {$prefix}options WHERE option_name IN ( 'gfpdf_settings', 'gf_db_version', 'WPLANG' )", OBJECT_K ); //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $options = wp_list_pluck( (array) $options, 'option_value' ); + + $settings = maybe_unserialize( $options['gfpdf_settings'] ?? '' ); + $default = is_array( $settings ) ? ( $settings['default_font'] ?? '' ) : ''; + $locales = $this->font_sources->map_language_font_locales( [ $options['WPLANG'] ?? '' ] ); + + foreach ( array_merge( array_map( 'strval', array_keys( $locales ) ), [ $default ] ) as $key ) { + $key = $this->normalise_font( $key ); + if ( $key !== '' ) { + yield $key => true; + } } - return $this->selected_font_keys; + /* Gravity Forms records its version as it creates a site's tables, so a site without one has no forms */ + if ( isset( $options['gf_db_version'] ) ) { + yield from $this->each_pdf_font( $prefix ); + } } /** - * A page of the stored meta of the active forms with PDFs, read raw so Gravity Forms doesn't build their fields + * A page of the stored meta of the active forms with PDFs, read raw so Gravity Forms doesn't build their fields. + * Pages by ID, as OFFSET would run the LIKE over every form it skips. The LIKE compares bytes, about 6x faster than + * the column's collation. + * + * @param string $prefix Another site's table prefix, or '' for this site * * @return array[] Rows of `form_id` and `display_meta` * * @since 6.19.0 */ - protected function get_pdf_form_meta( int $offset, int $limit ): array { + protected function get_pdf_form_meta( int $after_id, int $limit, string $prefix = '' ): array { global $wpdb; - $meta_table = GFFormsModel::get_meta_table_name(); - $form_table = GFFormsModel::get_form_table_name(); + $meta_table = $prefix === '' ? GFFormsModel::get_meta_table_name() : $prefix . 'gf_form_meta'; + $form_table = $prefix === '' ? GFFormsModel::get_form_table_name() : $prefix . 'gf_form'; /* The table names come from Gravity Forms */ //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = $wpdb->get_results( //phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( - "SELECT meta.form_id, meta.display_meta FROM {$meta_table} meta + "SELECT meta.form_id, meta.display_meta, form.title FROM {$meta_table} meta INNER JOIN {$form_table} form ON form.id = meta.form_id - WHERE form.is_active = 1 AND form.is_trash = 0 AND meta.display_meta LIKE %s - ORDER BY meta.form_id LIMIT %d OFFSET %d", + WHERE form.is_active = 1 AND form.is_trash = 0 AND meta.form_id > %d AND meta.display_meta LIKE CAST( %s AS BINARY ) + ORDER BY meta.form_id LIMIT %d", + $after_id, '%' . $wpdb->esc_like( '"gfpdf_form_settings"' ) . '%', - $limit, - $offset + $limit ), ARRAY_A ); diff --git a/src/assets/js/react/actions/fontManager.js b/src/assets/js/react/actions/fontManager.js index 965e98aea..3e857ebfc 100644 --- a/src/assets/js/react/actions/fontManager.js +++ b/src/assets/js/react/actions/fontManager.js @@ -10,15 +10,19 @@ export const EDIT_FONT_SUCCESS = 'EDIT_FONT_SUCCESS'; export const EDIT_FONT_ERROR = 'EDIT_FONT_ERROR'; export const VALIDATION_ERROR = 'VALIDATION_ERROR'; export const DELETE_VARIANT_ERROR = 'DELETE_VARIANT_ERROR'; +export const GET_FONT_USAGE_SUCCESS = 'GET_FONT_USAGE_SUCCESS'; +export const GET_FONT_USAGE_ERROR = 'GET_FONT_USAGE_ERROR'; export const DELETE_FONT = 'DELETE_FONT'; export const DELETE_FONT_SUCCESS = 'DELETE_FONT_SUCCESS'; export const DELETE_FONT_ERROR = 'DELETE_FONT_ERROR'; +export const INSTALL_FONT = 'INSTALL_FONT'; +export const INSTALL_FONT_SUCCESS = 'INSTALL_FONT_SUCCESS'; +export const INSTALL_FONT_ERROR = 'INSTALL_FONT_ERROR'; export const CLEAR_ADD_FONT_MSG = 'CLEAR_ADD_FONT_MSG'; export const CLEAR_DROPZONE_ERROR = 'CLEAR_DROPZONE_ERROR'; export const RESET_SEARCH_RESULT = 'RESET_SEARCH_RESULT'; export const SEARCH_FONT_LIST = 'SEARCH_FONT_LIST'; export const SELECT_FONT = 'SELECT_FONT'; -export const MOVE_SELECTED_FONT_TO_TOP = 'MOVE_SELECTED_FONT_TO_TOP'; /** * @package Gravity PDF @@ -105,15 +109,33 @@ export const deleteVariantError = (fontVariant) => { * Delete font request * * @param { string } id + * @param { string } fontType The row's `type`, which picks the endpoint * - * @return {{ payload: string, type: string }} action object + * @return {{ payload: string, fontType: string, type: string }} action object * * @since 6.0 */ -export const deleteFont = (id) => { +export const deleteFont = (id, fontType) => { return { type: DELETE_FONT, payload: id, + fontType, + }; +}; + +/** + * Download a core font + * + * @param { string } id + * + * @return {{ payload: string, type: string }} action object + * + * @since 6.19.0 + */ +export const installFont = (id) => { + return { + type: INSTALL_FONT, + payload: id, }; }; @@ -190,17 +212,3 @@ export const selectFont = (fontId) => { payload: fontId, }; }; - -/** - * Move selected font to top - * - * @param { string } fontId - * - * @return {{ payload: string, type: string }} action object - */ -export const moveSelectedFontToTop = (fontId) => { - return { - type: MOVE_SELECTED_FONT_TO_TOP, - payload: fontId, - }; -}; diff --git a/src/assets/js/react/api/fontManager.js b/src/assets/js/react/api/fontManager.js index 394911c0e..f80c63609 100644 --- a/src/assets/js/react/api/fontManager.js +++ b/src/assets/js/react/api/fontManager.js @@ -2,6 +2,8 @@ import { serialize } from 'object-to-formdata'; /* APIs */ import { api, getJsonString } from './api'; +/* Utilities */ +import { updateURLParameter } from '../../admin/settings/common/updateURLParameter'; /** * @package Gravity PDF @@ -21,34 +23,84 @@ import { api, getJsonString } from './api'; */ /** - * Fetch API request to obtain custom font list (GET) + * @typedef {FontItem} InstalledFont + * @property {string} type - `custom`, `core` or `bundled` + * @property {boolean} can_delete - Whether this user can delete it when it's unused + * @property {boolean} [auto_install] - Core fonts: whether it downloads again on its own + * @property {boolean} [can_install] - Core fonts: whether this user can download its missing files + */ + +/** + * The parts of a response the sagas read * - * @return {{ body: Array, text: string, status: number, ok: boolean }} Installed custom fonts + * @param {Response} response * - * @since 6.0 + * @return {{ body: *, text: string, status: number, ok: boolean }} The parsed response + * + * @since 6.19.0 */ -export const apiGetCustomFontList = async () => { - const url = GFPDF.restUrl + 'fonts/'; - - const response = await api(url, { - method: 'GET', - headers: { - 'X-WP-Nonce': GFPDF.restNonce, - Accept: 'application/json', - }, - }); - +const parseResponse = async (response) => { const text = await response.text(); - const body = getJsonString(text); return { - body, + body: getJsonString(text), text, status: response.status, ok: response.ok, }; }; +/** + * A fonts endpoint URL with its scope + * + * @param {string} id + * @param {string} scope + * + * @return {string} The URL + * + * @since 6.19.0 + */ +const fontsUrl = (id, scope) => + updateURLParameter(GFPDF.restUrl + 'fonts/' + id, 'scope', scope); + +/** + * Fetch API GET request to the fonts endpoint + * + * @param {string} scope + * + * @return {{ body: *, text: string, status: number, ok: boolean }} The parsed response + * + * @since 6.19.0 + */ +const getFonts = async (scope) => + parseResponse( + await api(fontsUrl('', scope), { + method: 'GET', + headers: { + 'X-WP-Nonce': GFPDF.restNonce, + Accept: 'application/json', + }, + }) + ); + +/** + * Fetch API request to obtain every font the Font Manager lists (GET) + * + * @return {{ body: Array, text: string, status: number, ok: boolean }} Custom, core and bundled fonts + * + * @since 6.0 + */ +export const apiGetCustomFontList = () => getFonts('all'); + +/** + * Fetch API request for why fonts are in use, which is slower than the list as it reads every form (GET) + * + * @return {{ body: Object>, text: string, status: number, ok: boolean }} Reasons by font ID + * + * @since 6.19.0 + */ +export const apiGetFontUsage = () => getFonts('usage'); + /** * Fetch API request to add new font (POST) * @@ -71,15 +123,7 @@ export const apiAddFont = async (font) => { body: formData, }); - const text = await response.text(); - const body = getJsonString(text); - - return { - body, - text, - status: response.status, - ok: response.ok, - }; + return parseResponse(response); }; /** @@ -107,33 +151,52 @@ export const apiEditFont = async ({ id, font }) => { body: formData, }); - const text = await response.text(); - const body = getJsonString(text); - - return { - body, - text, - status: response.status, - ok: response.ok, - }; + return parseResponse(response); }; /** - * Fetch API request to delete existing font (DELETE) + * Send a bodyless request about a font * - * @param {string} id - ID of the font to delete + * @param {string} method + * @param {string} url * - * @return {Promise} Raw fetch Response + * @return {{ body: Object, text: string, status: number, ok: boolean }} The response * - * @since 6.0 + * @since 6.19.0 */ -export const apiDeleteFont = (id) => { - const url = GFPDF.restUrl + 'fonts/' + id; - - return api(url, { - method: 'DELETE', +const sendFontRequest = async (method, url) => { + const response = await api(url, { + method, headers: { 'X-WP-Nonce': GFPDF.restNonce, + Accept: 'application/json', }, }); + + return parseResponse(response); }; + +/** + * Fetch API request to download a core font (POST) + * + * @param {string} id + * + * @return {{ body: Object, text: string, status: number, ok: boolean }} The response, with the font's row or why it failed + * + * @since 6.19.0 + */ +export const apiInstallFont = (id) => + sendFontRequest('POST', fontsUrl(id, 'core')); + +/** + * Fetch API request to delete existing font (DELETE) + * + * @param {string} id - ID of the font to delete + * @param {string} fontType - The row's `type` + * + * @return {{ body: Object, text: string, status: number, ok: boolean }} The response, with the server's reason when it refuses + * + * @since 6.0 + */ +export const apiDeleteFont = (id, fontType) => + sendFontRequest('DELETE', fontsUrl(id, fontType)); diff --git a/src/assets/js/react/components/FontManager/AddFont.js b/src/assets/js/react/components/FontManager/AddFont.js index 42bd9f9a1..d8a4bb502 100644 --- a/src/assets/js/react/components/FontManager/AddFont.js +++ b/src/assets/js/react/components/FontManager/AddFont.js @@ -26,9 +26,6 @@ import AddUpdateFontFooter from './AddUpdateFontFooter'; * @param { boolean } props.validateRegular * @param { Object } props.msg * @param { boolean } props.loading - * @param { string } props.tabIndexFontName - * @param { string } props.tabIndexFontFiles - * @param { string } props.tabIndexFooterButtons * * @return {JSX.Element} AddFont component * @@ -40,22 +37,18 @@ export const AddFont = ({ onHandleUpload, onHandleDeleteFontStyle, onHandleSubmit, + onHandleCancelAddFont, fontStyles, validateLabel, validateRegular, msg, loading, - tabIndexFontName, - tabIndexFontFiles, - tabIndexFooterButtons, }) => { return (
-
+

{GFPDF.fontManagerAddTitle}

-

{GFPDF.fontManagerAddDesc}

- -

- {GFPDF.fontManagerFontNameDesc} -

- onHandleInputChange(e, 'addFont')} - tabIndex={tabIndexFontName} /> +

+ {GFPDF.fontManagerFontNameDesc} +

+
{!validateLabel && ( @@ -107,17 +100,17 @@ export const AddFont = ({ onHandleUpload={onHandleUpload} onHandleDeleteFontStyle={onHandleDeleteFontStyle} msg={msg} - tabIndex={tabIndexFontFiles} - /> - - + +
); }; @@ -133,14 +126,12 @@ AddFont.propTypes = { onHandleUpload: PropTypes.func.isRequired, onHandleDeleteFontStyle: PropTypes.func.isRequired, onHandleSubmit: PropTypes.func.isRequired, + onHandleCancelAddFont: PropTypes.func, validateLabel: PropTypes.bool.isRequired, validateRegular: PropTypes.bool.isRequired, fontStyles: PropTypes.object.isRequired, msg: PropTypes.object.isRequired, loading: PropTypes.bool.isRequired, - tabIndexFontName: PropTypes.string.isRequired, - tabIndexFontFiles: PropTypes.string.isRequired, - tabIndexFooterButtons: PropTypes.string.isRequired, }; export default AddFont; diff --git a/src/assets/js/react/components/FontManager/AddUpdateFontFooter.js b/src/assets/js/react/components/FontManager/AddUpdateFontFooter.js index 002b96078..5c2f17233 100644 --- a/src/assets/js/react/components/FontManager/AddUpdateFontFooter.js +++ b/src/assets/js/react/components/FontManager/AddUpdateFontFooter.js @@ -1,15 +1,10 @@ /* Dependencies */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; import { sprintf } from 'sprintf-js'; /* Components */ +import FontDeleteButton from './FontDeleteButton'; import Spinner from '../Spinner'; -/* Redux actions */ -import { - selectFont as selectFontAction, - deleteFont as deleteFontAction, -} from '../../actions/fontManager'; import TemplateTooltip from './TemplateTooltip'; /** @@ -24,13 +19,12 @@ import TemplateTooltip from './TemplateTooltip'; * * @param state * @param id + * @param formId + * @param type * @param disabled * @param onHandleCancelEditFont - * @param onHandleCancelEditFontKeypress - * @param success - * @param error + * @param msg * @param loading - * @param tabIndex * * @since 6.0 */ @@ -43,95 +37,12 @@ export class AddUpdateFontFooter extends Component { static propTypes = { state: PropTypes.string, id: PropTypes.string, + formId: PropTypes.string, type: PropTypes.string, disabled: PropTypes.bool, onHandleCancelEditFont: PropTypes.func, - onHandleCancelEditFontKeypress: PropTypes.func, - selectedFont: PropTypes.string.isRequired, - selectFont: PropTypes.func.isRequired, - deleteFont: PropTypes.func, msg: PropTypes.object.isRequired, loading: PropTypes.bool.isRequired, - tabIndex: PropTypes.string.isRequired, - }; - - /** - * Handle the functionality to select and set default font type to be used in PDFs - * (Under 'update font' panel) - * - * @param { string } fontId - * @param { string } selectedFont - * - * @since 6.0 - */ - handleSelectFont = (fontId, selectedFont) => { - const { selectFont } = this.props; - - if (fontId === selectedFont) { - return selectFont(''); - } - - selectFont(fontId); - }; - - /** - * Handle the functionality to select and set default font type to be used in PDFs - * (Under 'update font' panel - Keyboard press) - * - * @param { KeyboardEvent } e - * @param { string } fontId - * @param { string } selectedFont - * - * @since 6.0 - */ - handleSelectFontKeypress = (e, fontId, selectedFont) => { - const enter = 'Enter'; - const space = ' '; - const { selectFont } = this.props; - - if (e.key === enter || e.key === space) { - if (fontId === selectedFont) { - return selectFont(''); - } - - selectFont(fontId); - } - }; - - /** - * Handle request of font deletion (Under 'update font' panel) - * - * @param { string } fontId - * - * @since 6.0 - */ - handleDeleteFont = (fontId) => { - /* Fire a native window alert box to confirm deletion request */ - if (window.confirm(GFPDF.fontManagerDeleteFontConfirmation)) { - /* Call redux action deleteFont */ - this.props.deleteFont(fontId); - } - }; - - /** - * Handle request of font deletion (Under 'update font' panel - Keyboard press) - * - * @param { KeyboardEvent } e - * @param { string } fontId - * - * @since 6.0 - */ - handleDeleteFontKeypress = (e, fontId) => { - const enter = 'Enter'; - const space = ' '; - - if (e.key === enter || e.key === space) { - /* Fire a native window alert box to confirm deletion request */ - if (window.confirm(GFPDF.fontManagerDeleteFontConfirmation)) { - /* Call redux action deleteFont */ - this.props.deleteFont(fontId); - } - } }; render() { @@ -139,15 +50,12 @@ export class AddUpdateFontFooter extends Component { state, type, id, + formId, disabled, onHandleCancelEditFont, - onHandleCancelEditFontKeypress, - selectedFont, msg: { success, error }, loading, - tabIndex, } = this.props; - const cancelButton = document.querySelector('.footer button.cancel'); const errorFontList = error && error.fontList; const successAddFont = success && success.addFont; const showSuccessAddFont = @@ -164,8 +72,6 @@ export class AddUpdateFontFooter extends Component { '', '' ); - const selectedBoxStyle = - id !== '' && id === selectedFont ? ' checked' : ' uncheck'; /* Display error message for uploading invalid font file */ const displayInvalidFileErrorMessage = errorAddFont && errorFontValidation; @@ -173,83 +79,8 @@ export class AddUpdateFontFooter extends Component { const displayGenericErrorMessage = errorAddFont && !errorFontValidation; return ( -