diff --git a/packages/block-library/src/footnotes/index.php b/packages/block-library/src/footnotes/index.php index bd7734d7d02d1..5924db3a190c2 100644 --- a/packages/block-library/src/footnotes/index.php +++ b/packages/block-library/src/footnotes/index.php @@ -34,7 +34,7 @@ function render_block_core_footnotes( $attributes, $content, $block ) { $footnotes = json_decode( $footnotes, true ); - if ( count( $footnotes ) === 0 ) { + if ( ! is_array( $footnotes ) || count( $footnotes ) === 0 ) { return ''; } @@ -98,7 +98,7 @@ function wp_save_footnotes_meta( $revision_id ) { if ( $footnotes ) { // Can't use update_post_meta() because it doesn't allow revisions. - update_metadata( 'post', $revision_id, 'footnotes', $footnotes ); + update_metadata( 'post', $revision_id, 'footnotes', wp_slash( $footnotes ) ); } } } @@ -154,15 +154,14 @@ function wp_add_footnotes_revisions_to_post_meta( $post ) { if ( $footnotes ) { // Can't use update_post_meta() because it doesn't allow revisions. - update_metadata( 'post', $wp_temporary_footnote_revision_id, 'footnotes', $footnotes ); + update_metadata( 'post', $wp_temporary_footnote_revision_id, 'footnotes', wp_slash( $footnotes ) ); } } } } -foreach ( array( 'post', 'page' ) as $post_type ) { - add_action( "rest_after_insert_{$post_type}", 'wp_add_footnotes_revisions_to_post_meta' ); -} +add_action( 'rest_after_insert_post', 'wp_add_footnotes_revisions_to_post_meta' ); +add_action( 'rest_after_insert_page', 'wp_add_footnotes_revisions_to_post_meta' ); /** * Restores the footnotes meta value from the revision. @@ -176,7 +175,7 @@ function wp_restore_footnotes_from_revision( $post_id, $revision_id ) { $footnotes = get_post_meta( $revision_id, 'footnotes', true ); if ( $footnotes ) { - update_post_meta( $post_id, 'footnotes', $footnotes ); + update_post_meta( $post_id, 'footnotes', wp_slash( $footnotes ) ); } else { delete_post_meta( $post_id, 'footnotes' ); } @@ -243,7 +242,7 @@ function _wp_rest_api_autosave_meta( $autosave ) { return; } - update_post_meta( $id, 'footnotes', $body['meta']['footnotes'] ); + update_post_meta( $id, 'footnotes', wp_slash( $body['meta']['footnotes'] ) ); } // See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L391C1-L391C1. add_action( 'wp_creating_autosave', '_wp_rest_api_autosave_meta' ); diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index 6cc1e021841b4..3039105d7c55c 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -229,6 +229,15 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) { ); function updateAttributes( attributes ) { + // Only attempt to update attributes, if attributes is an object. + if ( + ! attributes || + Array.isArray( attributes ) || + typeof attributes !== 'object' + ) { + return attributes; + } + attributes = { ...attributes }; for ( const key in attributes ) { diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 73e050af14df2..46f9a0a258e6b 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -31,7 +31,7 @@ import { InterfaceSkeleton, store as interfaceStore, } from '@wordpress/interface'; -import { useState, useEffect, useCallback } from '@wordpress/element'; +import { useState, useEffect, useCallback, useMemo } from '@wordpress/element'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as noticesStore } from '@wordpress/notices'; @@ -70,6 +70,57 @@ const interfaceLabels = { footer: __( 'Editor footer' ), }; +function useEditorStyles() { + const { hasThemeStyleSupport, editorSettings } = useSelect( + ( select ) => ( { + hasThemeStyleSupport: + select( editPostStore ).isFeatureActive( 'themeStyles' ), + editorSettings: select( editorStore ).getEditorSettings(), + } ), + [] + ); + + // Compute the default styles. + return useMemo( () => { + const presetStyles = + editorSettings.styles?.filter( + ( style ) => + style.__unstableType && style.__unstableType !== 'theme' + ) ?? []; + + const defaultEditorStyles = [ + ...editorSettings.defaultEditorStyles, + ...presetStyles, + ]; + + // Has theme styles if the theme supports them and if some styles were not preset styles (in which case they're theme styles). + const hasThemeStyles = + hasThemeStyleSupport && + presetStyles.length !== ( editorSettings.styles?.length ?? 0 ); + + // If theme styles are not present or displayed, ensure that + // base layout styles are still present in the editor. + if ( ! editorSettings.disableLayoutStyles && ! hasThemeStyles ) { + defaultEditorStyles.push( { + css: getLayoutStyles( { + style: {}, + selector: 'body', + hasBlockGapSupport: false, + hasFallbackGapSupport: true, + fallbackGapValue: '0.5em', + } ), + } ); + } + + return hasThemeStyles ? editorSettings.styles : defaultEditorStyles; + }, [ + editorSettings.defaultEditorStyles, + editorSettings.disableLayoutStyles, + editorSettings.styles, + hasThemeStyleSupport, + ] ); +} + function Layout() { const isMobileViewport = useViewportMatch( 'medium', '<' ); const isHugeViewport = useViewportMatch( 'huge', '>=' ); @@ -94,45 +145,10 @@ function Layout() { showBlockBreadcrumbs, isTemplateMode, documentLabel, - styles, } = useSelect( ( select ) => { const { getEditorSettings, getPostTypeLabel } = select( editorStore ); - const { isFeatureActive } = select( editPostStore ); const editorSettings = getEditorSettings(); const postTypeLabel = getPostTypeLabel(); - const hasThemeStyles = isFeatureActive( 'themeStyles' ); - - const themeStyles = []; - const presetStyles = []; - editorSettings.styles?.forEach( ( style ) => { - if ( ! style.__unstableType || style.__unstableType === 'theme' ) { - themeStyles.push( style ); - } else { - presetStyles.push( style ); - } - } ); - - const defaultEditorStyles = [ - ...editorSettings.defaultEditorStyles, - ...presetStyles, - ]; - - // If theme styles are not present or displayed, ensure that - // base layout styles are still present in the editor. - if ( - ! editorSettings.disableLayoutStyles && - ! ( hasThemeStyles && themeStyles.length ) - ) { - defaultEditorStyles.push( { - css: getLayoutStyles( { - style: {}, - selector: 'body', - hasBlockGapSupport: false, - hasFallbackGapSupport: true, - fallbackGapValue: '0.5em', - } ), - } ); - } return { isTemplateMode: select( editPostStore ).isEditingTemplate(), @@ -165,13 +181,11 @@ function Layout() { ), // translators: Default label for the Document in the Block Breadcrumb. documentLabel: postTypeLabel || _x( 'Document', 'noun' ), - styles: - hasThemeStyles && themeStyles.length - ? editorSettings.styles - : defaultEditorStyles, }; }, [] ); + const styles = useEditorStyles(); + const openSidebarPanel = () => openGeneralSidebar( hasBlockSelected ? 'edit-post/block' : 'edit-post/document' diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index b96d4530cb499..04c5bed76bd26 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -291,7 +291,8 @@ test.describe( 'Footnotes', () => { await editor.clickBlockToolbarButton( 'More' ); await page.locator( 'button:text("Footnote")' ).click(); - await page.keyboard.type( 'first footnote' ); + // Check if content is correctly slashed on save and restore. + await page.keyboard.type( 'first footnote"' ); const id1 = await editor.canvas.evaluate( () => { return document.activeElement.id; @@ -316,7 +317,7 @@ test.describe( 'Footnotes', () => { id: id2, }, { - content: 'first footnote', + content: 'first footnote"', id: id1, }, ] ); @@ -329,7 +330,7 @@ test.describe( 'Footnotes', () => { // This also saves the post! expect( await getFootnotes( page ) ).toMatchObject( [ { - content: 'first footnote', + content: 'first footnote"', id: id1, }, { @@ -338,8 +339,20 @@ test.describe( 'Footnotes', () => { }, ] ); + const editorPage = page; + const previewPage = await editor.openPreviewPage(); + + await expect( + previewPage.locator( 'ol.wp-block-footnotes' ) + ).toHaveText( 'first footnote” ↩︎second footnote ↩︎' ); + + await previewPage.close(); + await editorPage.bringToFront(); + // Open revisions. await editor.openDocumentSettingsSidebar(); + // Ensure the preview dropdown popover closes. + await editor.canvas.click( 'body' ); await page .getByRole( 'region', { name: 'Editor settings' } ) .getByRole( 'button', { name: 'Post' } ) @@ -355,10 +368,19 @@ test.describe( 'Footnotes', () => { id: id2, }, { - content: 'first footnote', + content: 'first footnote"', id: id1, }, ] ); + + const previewPage2 = await editor.openPreviewPage(); + + await expect( + previewPage2.locator( 'ol.wp-block-footnotes' ) + ).toHaveText( 'second footnote ↩︎first footnote” ↩︎' ); + + await previewPage2.close(); + await editorPage.bringToFront(); } ); test( 'can be previewed when published', async ( { editor, page } ) => { @@ -392,12 +414,14 @@ test.describe( 'Footnotes', () => { // path). await editor.canvas.click( 'ol.wp-block-footnotes li span' ); await page.keyboard.press( 'End' ); - await page.keyboard.type( '3' ); + // Test slashing. + await page.keyboard.type( '3"' ); const previewPage2 = await editor.openPreviewPage(); + // Note: quote will get curled by wptexturize. await expect( previewPage2.locator( 'ol.wp-block-footnotes li' ) - ).toHaveText( '123 ↩︎' ); + ).toHaveText( '123″ ↩︎' ); } ); } );