Skip to content

Commit

Permalink
Font Library: refactor endpoint permissions (#54829)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed Sep 29, 2023
1 parent 83044ca commit abecb1d
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 160 deletions.
Expand Up @@ -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 );
}

/**
Expand All @@ -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.
*
Expand All @@ -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 );
}
}
Expand Up @@ -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
Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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(
Expand All @@ -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 ],
};
}
}

Expand Down
Expand Up @@ -12,6 +12,7 @@ import {
FlexItem,
Flex,
Button,
Notice,
} from '@wordpress/components';
import { debounce } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
Expand All @@ -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',
Expand 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
);
Expand All @@ -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 ]
Expand Down Expand Up @@ -122,6 +136,13 @@ function FontCollection( { id } ) {
setFontsToInstall( [] );
};

const handleInstall = async () => {
const response = await installFonts( fontsToInstall );
const installNotice = getNoticeFromInstallResponse( response );
setNotice( installNotice );
resetFontsToInstall();
};

return (
<TabLayout
title={
Expand All @@ -135,10 +156,7 @@ function FontCollection( { id } ) {
handleBack={ !! selectedFont && handleUnselectFont }
footer={
fontsToInstall.length > 0 && (
<Footer
fontsToInstall={ fontsToInstall }
resetFontsToInstall={ resetFontsToInstall }
/>
<Footer handleInstall={ handleInstall } />
)
}
>
Expand All @@ -153,6 +171,22 @@ function FontCollection( { id } ) {
<Spinner />
) }

{ notice && (
<>
<FlexItem>
<Spacer margin={ 2 } />
<Notice
isDismissible={ false }
status={ notice.type }
className="font-library-modal__font-collection__notice"
>
{ notice.message }
</Notice>
</FlexItem>
<Spacer margin={ 2 } />
</>
) }

{ ! renderConfirmDialog && ! selectedFont && (
<Flex>
<FlexItem>
Expand Down Expand Up @@ -232,13 +266,8 @@ function FontCollection( { id } ) {
);
}

function Footer( { fontsToInstall, resetFontsToInstall } ) {
const { installFonts, isInstalling } = useContext( FontLibraryContext );

const handleInstall = async () => {
await installFonts( fontsToInstall );
resetFontsToInstall();
};
function Footer( { handleInstall } ) {
const { isInstalling } = useContext( FontLibraryContext );

return (
<Flex justify="flex-end">
Expand Down

0 comments on commit abecb1d

Please sign in to comment.