From abecb1dd2a0ab5d2c82541f7aab72ae429129e69 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 29 Sep 2023 19:32:43 -0300 Subject: [PATCH] Font Library: refactor endpoint permissions (#54829) * break the checking of user permission and file write permissions * return error 500 if the request to the install fonts endpoint needs write permissions and wordpress doens't have write permission on the server * do not ask file write permission on uninstall endpoint * Standardize the output of install and uninstall fonts endpoints Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Jeff Ong <5375500+jffng@users.noreply.github.com> * Handle standardized output from install and uninstall endpoints in the frontend Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Jeff Ong <5375500+jffng@users.noreply.github.com> * Update the install and unintall fonts endpoints unit tests for the new standardized output format Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Jeff Ong <5375500+jffng@users.noreply.github.com> * fix the refersh call for the library Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Jeff Ong <5375500+jffng@users.noreply.github.com> --------- Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Jeff Ong <5375500+jffng@users.noreply.github.com> --- .../class-wp-rest-font-library-controller.php | 133 +++++++++++---- .../font-library-modal/context.js | 49 +++--- .../font-library-modal/font-collection.js | 53 ++++-- .../font-library-modal/installed-fonts.js | 38 ++++- .../font-library-modal/local-fonts.js | 23 ++- .../font-library-modal/style.scss | 4 + .../utils/get-notice-from-response.js | 62 +++++++ .../installFonts.php | 161 ++++++++++-------- .../uninstallFonts.php | 5 +- 9 files changed, 368 insertions(+), 160 deletions(-) create mode 100644 packages/edit-site/src/components/global-styles/font-library-modal/utils/get-notice-from-response.js diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php index dcf94d93012a2..90fb3c6973ca3 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php @@ -289,19 +289,39 @@ public function uninstall_schema() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function uninstall_fonts( $request ) { - $fonts_param = $request->get_param( 'fontFamilies' ); + $fonts_to_uninstall = $request->get_param( 'fontFamilies' ); + + $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_param as $font_data ) { + foreach ( $fonts_to_uninstall as $font_data ) { $font = new WP_Font_Family( $font_data ); $result = $font->uninstall(); - - // If there was an error uninstalling the font, return the error. if ( is_wp_error( $result ) ) { - return $result; + $errors[] = $result; + } else { + $successes[] = $result; } } - - return new WP_REST_Response( __( 'Font family uninstalled successfully.', 'gutenberg' ), 200 ); + $data = array( + 'successes' => $successes, + 'errors' => $errors, + ); + return rest_ensure_response( $data ); } /** @@ -321,23 +341,48 @@ public function update_font_library_permissions_check() { ) ); } + return true; + } + /** + * Checks whether the user has write permissions to the temp and fonts directories. + * + * @since 6.4.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_upload_dir()['basedir']; + $upload_dir = WP_Font_Library::get_fonts_dir(); if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) { - return new WP_Error( - 'rest_cannot_write_fonts_folder', - __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ), - array( - 'status' => 500, - ) - ); + return false; } - return true; } + /** + * Checks whether the request needs write permissions. + * + * @since 6.4.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. * @@ -361,38 +406,52 @@ public function install_fonts( $request ) { */ $fonts_to_install = json_decode( $fonts_param, true ); + $successes = array(); + $errors = array(); + $response_status = 200; + if ( empty( $fonts_to_install ) ) { - return new WP_Error( + $errors[] = new WP_Error( 'no_fonts_to_install', - __( 'No fonts to install', 'gutenberg' ), - array( 'status' => 400 ) + __( 'No fonts to install', 'gutenberg' ) ); + $response_status = 400; } - // Get uploaded files (used when installing local fonts). - $files = $request->get_file_params(); - - // Iterates the fonts data received and creates a new WP_Font_Family object for each one. - $fonts_installed = array(); - foreach ( $fonts_to_install as $font_data ) { - $font = new WP_Font_Family( $font_data ); - $font->install( $files ); - $fonts_installed[] = $font; + if ( $this->needs_write_permission( $fonts_to_install ) && ! $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( $fonts_installed ) ) { - return new WP_Error( - 'error_installing_fonts', - __( 'Error installing fonts. No font was installed.', 'gutenberg' ), - array( 'status' => 500 ) + if ( ! empty( $errors ) ) { + $data = array( + 'successes' => $successes, + 'errors' => $errors, ); + $response = rest_ensure_response( $data ); + $response->set_status( $response_status ); + return $response; } - $response = array(); - foreach ( $fonts_installed as $font ) { - $response[] = $font->get_data(); + // 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; + } } - return new WP_REST_Response( $response ); + $data = array( + 'successes' => $successes, + 'errors' => $errors, + ); + return rest_ensure_response( $data ); } } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 78a238e1ba51b..8ab067495fcc0 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -9,8 +9,6 @@ import { useEntityRecords, store as coreStore, } from '@wordpress/core-data'; -import { store as noticesStore } from '@wordpress/notices'; -import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -51,8 +49,6 @@ function FontLibraryProvider( { children } ) { const fontFamiliesHasChanges = !! globalStyles?.edits?.settings?.typography?.fontFamilies; - const { createErrorNotice } = useDispatch( noticesStore ); - const [ isInstalling, setIsInstalling ] = useState( false ); const [ refreshKey, setRefreshKey ] = useState( 0 ); @@ -202,7 +198,8 @@ function FontLibraryProvider( { children } ) { // Prepare formData to install. const formData = makeFormDataFromFontFamilies( fonts ); // Install the fonts (upload the font files to the server and create the post in the database). - const fontsInstalled = await fetchInstallFonts( formData ); + const response = await fetchInstallFonts( formData ); + const fontsInstalled = response?.successes || []; // Get intersecting font faces between the fonts we tried to installed and the fonts that were installed // (to avoid activating a non installed font). const fontToBeActivated = getIntersectingFontFaces( @@ -217,36 +214,40 @@ function FontLibraryProvider( { children } ) { ] ); refreshLibrary(); setIsInstalling( false ); - return true; - } catch ( e ) { + + return response; + } catch ( error ) { setIsInstalling( false ); - return false; + return { + errors: [ error ], + }; } } async function uninstallFont( font ) { try { // Uninstall the font (remove the font files from the server and the post from the database). - await fetchUninstallFonts( [ font ] ); + const response = await fetchUninstallFonts( [ font ] ); // Deactivate the font family (remove the font family from the global styles). - deactivateFontFamily( font ); - // Save the global styles to the database. - await saveSpecifiedEntityEdits( - 'root', - 'globalStyles', - globalStylesId, - [ 'settings.typography.fontFamilies' ] - ); + if ( ! response.errors ) { + deactivateFontFamily( font ); + // Save the global styles to the database. + await saveSpecifiedEntityEdits( + 'root', + 'globalStyles', + globalStylesId, + [ 'settings.typography.fontFamilies' ] + ); + } // Refresh the library (the the library font families from database). refreshLibrary(); - return true; - } catch ( e ) { + return response; + } catch ( error ) { // eslint-disable-next-line no-console - console.error( e ); - createErrorNotice( __( 'Error uninstalling fonts.' ), { - type: 'snackbar', - } ); - return false; + console.error( error ); + return { + errors: [ error ], + }; } } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 56d1a03e63b32..a3b697efcfb8b 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -12,6 +12,7 @@ import { FlexItem, Flex, Button, + Notice, } from '@wordpress/components'; import { debounce } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; @@ -29,6 +30,7 @@ import CollectionFontDetails from './collection-font-details'; import { toggleFont } from './utils/toggleFont'; import { getFontsOutline } from './utils/fonts-outline'; import GoogleFontsConfirmDialog from './google-fonts-confirm-dialog'; +import { getNoticeFromInstallResponse } from './utils/get-notice-from-response'; const DEFAULT_CATEGORY = { id: 'all', @@ -45,13 +47,15 @@ function FontCollection( { id } ) { ); }; + const [ notice, setNotice ] = useState( null ); const [ selectedFont, setSelectedFont ] = useState( null ); const [ fontsToInstall, setFontsToInstall ] = useState( [] ); const [ filters, setFilters ] = useState( {} ); const [ renderConfirmDialog, setRenderConfirmDialog ] = useState( requiresPermission && ! getGoogleFontsPermissionFromStorage() ); - const { collections, getFontCollection } = useContext( FontLibraryContext ); + const { collections, getFontCollection, installFonts } = + useContext( FontLibraryContext ); const selectedCollection = collections.find( ( collection ) => collection.id === id ); @@ -76,6 +80,16 @@ function FontCollection( { id } ) { setSelectedFont( null ); }, [ id ] ); + // Reset notice after 5 seconds + useEffect( () => { + if ( notice ) { + const timeout = setTimeout( () => { + setNotice( null ); + }, 5000 ); + return () => clearTimeout( timeout ); + } + }, [ notice ] ); + const collectionFonts = useMemo( () => selectedCollection?.data?.fontFamilies ?? [], [ selectedCollection ] @@ -122,6 +136,13 @@ function FontCollection( { id } ) { setFontsToInstall( [] ); }; + const handleInstall = async () => { + const response = await installFonts( fontsToInstall ); + const installNotice = getNoticeFromInstallResponse( response ); + setNotice( installNotice ); + resetFontsToInstall(); + }; + return ( 0 && ( -