From 10b9dcb93b6c9eeba0e7b8ea12724aafb4ab2b3e Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 30 May 2024 12:05:17 +0200 Subject: [PATCH] Handle font licenses when editing theme metadata (#649) * handle font licenses when editing theme metadata * move download file util * split string Co-authored-by: Vicente Canales <1157901+vcanales@users.noreply.github.com> * remove console log Co-authored-by: Vicente Canales <1157901+vcanales@users.noreply.github.com> * fix handle error in getting license --------- Co-authored-by: Vicente Canales <1157901+vcanales@users.noreply.github.com> --- src/editor-sidebar/metadata-editor-modal.js | 59 ++++++++ src/landing-page/landing-page.js | 2 +- src/plugin-sidebar.js | 2 +- src/resolvers.js | 11 ++ src/{utils.js => utils/download-file.js} | 8 +- src/utils/fonts.js | 141 ++++++++++++++++++++ 6 files changed, 220 insertions(+), 3 deletions(-) rename src/{utils.js => utils/download-file.js} (80%) create mode 100644 src/utils/fonts.js diff --git a/src/editor-sidebar/metadata-editor-modal.js b/src/editor-sidebar/metadata-editor-modal.js index f42c2f95..b1d55af0 100644 --- a/src/editor-sidebar/metadata-editor-modal.js +++ b/src/editor-sidebar/metadata-editor-modal.js @@ -28,6 +28,7 @@ import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor'; * Internal dependencies */ import { postUpdateThemeMetadata, fetchReadmeData } from '../resolvers'; +import { getFontsCreditsText } from '../utils/fonts'; const ALLOWED_SCREENSHOT_MEDIA_TYPES = [ 'image/png', @@ -48,6 +49,7 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => { author_uri: '', tags_custom: '', recommended_plugins: '', + font_credits: '', subfolder: '', } ); @@ -56,6 +58,7 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => { useSelect( async ( select ) => { const themeData = select( 'core' ).getCurrentTheme(); const readmeData = await fetchReadmeData(); + setTheme( { name: themeData.name.raw, description: themeData.description.raw, @@ -66,6 +69,7 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => { tags_custom: themeData.tags.rendered, screenshot: themeData.screenshot, recommended_plugins: readmeData.recommended_plugins, + font_credits: readmeData.fonts, subfolder: themeData.stylesheet.lastIndexOf( '/' ) > 1 ? themeData.stylesheet.substring( @@ -99,6 +103,26 @@ export const ThemeMetadataEditorModal = ( { onRequestClose } ) => { } ); }; + const updateFontCredits = async () => { + try { + const credits = await getFontsCreditsText(); + setTheme( { ...theme, font_credits: credits } ); + } catch ( error ) { + // eslint-disable-next-line no-alert + alert( + sprintf( + /* translators: %1: error code, %2: error message */ + __( + 'Error getting font licenses. Code: %1$s. Message: %2$s', + 'create-block-theme' + ), + error.code, + error.message + ) + ); + } + }; + const onChangeTags = ( newTags ) => { setTheme( { ...theme, tags_custom: newTags.join( ', ' ) } ); }; @@ -231,6 +255,41 @@ Plugin Description`, setTheme( { ...theme, recommended_plugins: value } ) } /> + + + +
+ { __( + 'Credits and licensing information for fonts used in the theme.', + 'create-block-theme' + ) } +
+ + { __( 'Read more.', 'create-block-theme' ) } + + + } + placeholder={ `${ __( 'Font Name', 'create-block-theme' ) } +${ __( 'Copyright', 'create-block-theme' ) } +${ __( 'License', 'create-block-theme' ) } +${ __( 'Source', 'create-block-theme' ) }` } + value={ theme.font_credits } + onChange={ ( value ) => + setTheme( { ...theme, font_credits: value } ) + } + /> + { __( 'Screenshot', 'create-block-theme' ) } diff --git a/src/landing-page/landing-page.js b/src/landing-page/landing-page.js index 1d4682bc..e954e5a9 100644 --- a/src/landing-page/landing-page.js +++ b/src/landing-page/landing-page.js @@ -18,7 +18,7 @@ import { * Internal dependencies */ import { downloadExportedTheme } from '../resolvers'; -import { downloadFile } from '../utils'; +import downloadFile from '../utils/download-file'; import { CreateThemeModal } from './create-modal'; export default function LandingPage() { diff --git a/src/plugin-sidebar.js b/src/plugin-sidebar.js index 91049e05..2a08ca59 100644 --- a/src/plugin-sidebar.js +++ b/src/plugin-sidebar.js @@ -48,7 +48,7 @@ import { CreateVariationPanel } from './editor-sidebar/create-variation-panel'; import { ThemeMetadataEditorModal } from './editor-sidebar/metadata-editor-modal'; import ScreenHeader from './editor-sidebar/screen-header'; import { downloadExportedTheme } from './resolvers'; -import { downloadFile } from './utils'; +import downloadFile from './utils/download-file'; const CreateBlockThemePlugin = () => { const [ isEditorOpen, setIsEditorOpen ] = useState( false ); diff --git a/src/resolvers.js b/src/resolvers.js index 65f725ab..e7a81f57 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -133,3 +133,14 @@ export async function downloadExportedTheme() { parse: false, } ); } + +export async function getFontFamilies() { + const response = await apiFetch( { + path: '/create-block-theme/v1/font-families', + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } ); + return response.data; +} diff --git a/src/utils.js b/src/utils/download-file.js similarity index 80% rename from src/utils.js rename to src/utils/download-file.js index 9fd49593..5e159b78 100644 --- a/src/utils.js +++ b/src/utils/download-file.js @@ -1,4 +1,10 @@ -export async function downloadFile( response ) { +/* + * Download a file from in a browser. + * + * @param {Response} response The response object from a fetch request. + * @return {void} + */ +export default async function downloadFile( response ) { const blob = await response.blob(); const filename = response.headers .get( 'Content-Disposition' ) diff --git a/src/utils/fonts.js b/src/utils/fonts.js new file mode 100644 index 00000000..2152eb3d --- /dev/null +++ b/src/utils/fonts.js @@ -0,0 +1,141 @@ +/** + * Internal dependencies + */ +import { getFontFamilies } from '../resolvers'; +import { Font } from '../lib/lib-font/lib-font.browser'; + +/** + * Fetch a file from a URL and return it as an ArrayBuffer. + * + * @param {string} url The URL of the file to fetch. + * @return {Promise} The file as an ArrayBuffer. + */ +async function fetchFileAsArrayBuffer( url ) { + const response = await fetch( url ); + if ( ! response.ok ) { + throw new Error( 'Network response was not ok.' ); + } + const arrayBuffer = await response.arrayBuffer(); + return arrayBuffer; +} + +/** + * Retrieves the licensing information of a font file given its URL. + * + * This function fetches the file as an ArrayBuffer, initializes a font object, and extracts licensing details from the font's OpenType tables. + * + * @param {string} url - The URL pointing directly to the font file. The URL should be a direct link to the file and publicly accessible. + * @return {Promise} A promise that resolves to an object containing the font's licensing details. + * + * The returned object includes the following properties (if available in the font's OpenType tables): + * - fontName: The full font name. + * - copyright: Copyright notice. + * - source: Unique identifier for the font's source. + * - license: License description. + * - licenseURL: URL to the full license text. + */ +async function getFontFileLicenseFromUrl( url ) { + const buffer = await fetchFileAsArrayBuffer( url ); + const fontObj = new Font( 'Uploaded Font' ); + fontObj.fromDataBuffer( buffer, url ); + // Assuming that fromDataBuffer triggers onload event and returning a Promise + const onloadEvent = await new Promise( + ( resolve ) => ( fontObj.onload = resolve ) + ); + const font = onloadEvent.detail.font; + const { name: nameTable } = font.opentype.tables; + return { + fontName: nameTable.get( 16 ) || nameTable.get( 1 ), + copyright: nameTable.get( 0 ), + source: nameTable.get( 11 ), + license: nameTable.get( 13 ), + licenseURL: nameTable.get( 14 ), + }; +} + +/** + * Get the license for a font family. + * + * @param {Object} fontFamily The font family in theme.json format. + * @return {Promise} A promise that resolved to the font license object if sucessful or null if the font family does not have a fontFace property. + */ +async function getFamilyLicense( fontFamily ) { + // If the font family does not have a fontFace property, return an empty string. + if ( ! fontFamily.fontFace?.length ) { + return null; + } + + // Load the fontFace from the first fontFace object in the font family. + const fontFace = fontFamily.fontFace[ 0 ]; + const faceUrl = Array.isArray( fontFace.src ) + ? fontFace.src[ 0 ] + : fontFace.src; + + // Get the license from the font face url. + return await getFontFileLicenseFromUrl( faceUrl ); +} + +/** + * Get the text for the font licenses of all the fonts defined in the theme. + * + * @return {Promise} A promise that resolves to an array containing font credits objects. + */ +async function getFontsCreditsArray() { + const fontFamilies = await getFontFamilies(); + + //Remove duplicates. Removes the font families that have the same fontFamily property. + const uniqueFontFamilies = fontFamilies.filter( + ( fontFamily, index, self ) => + index === + self.findIndex( ( t ) => t.fontFamily === fontFamily.fontFamily ) + ); + + const credits = []; + + // Iterate over fontFamilies and get the license for each family + for ( const fontFamily of uniqueFontFamilies ) { + const fontCredits = await getFamilyLicense( fontFamily ); + if ( fontCredits ) { + credits.push( fontCredits ); + } + } + + return credits; +} + +/** + * Get the text for the font licenses of all the fonts defined in the theme. + * + * @return {Promise} A promise that resolves to an string containing the formatted font licenses. + */ +export async function getFontsCreditsText() { + const creditsArray = await getFontsCreditsArray(); + const credits = creditsArray + .reduce( ( acc, credit ) => { + // skip if fontName is not available + if ( ! credit.fontName ) { + // continue + return acc; + } + + acc.push( credit.fontName ); + + if ( credit.copyright ) { + acc.push( credit.copyright ); + } + + if ( credit.source ) { + acc.push( `Source: ${ credit.source }` ); + } + + if ( credit.license ) { + acc.push( `License: ${ credit.license }` ); + } + + acc.push( '' ); + + return acc; + }, [] ) + .join( '\n' ); + return credits; +}