<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[56052] trunk: Editor: add navigation fallback.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/56052">56052</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/56052","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>isabel_brison</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2023-06-27 05:52:06 +0000 (Tue, 27 Jun 2023)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Editor: add navigation fallback.

Creates a fallback menu for the Navigation block including an API endpoint to retrieve it.

Props get_dave, spacedmonkey, kebbet, flixos90, mikeschroder, ramonopoly, audrasjb.
Fixes 58557.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesrestapiphp">trunk/src/wp-includes/rest-api.php</a></li>
<li><a href="#trunksrcwpsettingsphp">trunk/src/wp-settings.php</a></li>
<li><a href="#trunktestsqunitfixtureswpapigeneratedjs">trunk/tests/qunit/fixtures/wp-api-generated.js</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesclasswpclassictoblockmenuconverterphp">trunk/src/wp-includes/class-wp-classic-to-block-menu-converter.php</a></li>
<li><a href="#trunksrcwpincludesclasswpnavigationfallbackphp">trunk/src/wp-includes/class-wp-navigation-fallback.php</a></li>
<li><a href="#trunksrcwpincludesnavigationfallbackphp">trunk/src/wp-includes/navigation-fallback.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestnavigationfallbackcontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-navigation-fallback-controller.php</a></li>
<li><a href="#trunktestsphpunittestseditorclassictoblockmenuconverterphp">trunk/tests/phpunit/tests/editor/classic-to-block-menu-converter.php</a></li>
<li><a href="#trunktestsphpunittestseditornavigationfallbackphp">trunk/tests/phpunit/tests/editor/navigation-fallback.php</a></li>
<li><a href="#trunktestsphpunittestsrestapirestnavigationfallbackcontrollerphp">trunk/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesclasswpclassictoblockmenuconverterphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/class-wp-classic-to-block-menu-converter.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-classic-to-block-menu-converter.php                                (rev 0)
+++ trunk/src/wp-includes/class-wp-classic-to-block-menu-converter.php  2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,130 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * WP_Classic_To_Block_Menu_Converter class
+ *
+ * @package WordPress
+ * @since 6.3.0
+ */
+
+/**
+ * Converts a Classic Menu to Block Menu blocks.
+ *
+ * @since 6.3.0.
+ * @access public
+ */
+class WP_Classic_To_Block_Menu_Converter {
+
+       /**
+        * Converts a Classic Menu to blocks.
+        *
+        * @since 6.3.0.
+        *
+        * @param WP_Term $menu The Menu term object of the menu to convert.
+        * @return string the serialized and normalized parsed blocks.
+        */
+       public static function convert( $menu ) {
+
+               if ( ! is_nav_menu( $menu ) ) {
+                       return new WP_Error(
+                               'invalid_menu',
+                               __( 'The menu provided is not a valid menu.' )
+                       );
+               }
+
+               $menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) );
+
+               if ( empty( $menu_items ) ) {
+                       return array();
+               }
+
+               // Set up the $menu_item variables.
+               // Adds the class property classes for the current context, if applicable.
+               _wp_menu_item_classes_by_context( $menu_items );
+
+               $menu_items_by_parent_id = static::group_by_parent_id( $menu_items );
+
+               $first_menu_item = isset( $menu_items_by_parent_id[0] )
+                       ? $menu_items_by_parent_id[0]
+                       : array();
+
+               $inner_blocks = static::to_blocks(
+                       $first_menu_item,
+                       $menu_items_by_parent_id
+               );
+
+               return serialize_blocks( $inner_blocks );
+       }
+
+       /**
+        * Returns an array of menu items grouped by the id of the parent menu item.
+        *
+        * @since 6.3.0.
+        *
+        * @param array $menu_items An array of menu items.
+        * @return array
+        */
+       private static function group_by_parent_id( $menu_items ) {
+               $menu_items_by_parent_id = array();
+
+               foreach ( $menu_items as $menu_item ) {
+                       $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item;
+               }
+
+               return $menu_items_by_parent_id;
+       }
+
+       /**
+        * Turns menu item data into a nested array of parsed blocks
+        *
+        * @since 6.3.0.
+        *
+        * @param array $menu_items               An array of menu items that represent
+        *                                        an individual level of a menu.
+        * @param array $menu_items_by_parent_id  An array keyed by the id of the
+        *                                        parent menu where each element is an
+        *                                        array of menu items that belong to
+        *                                        that parent.
+        * @return array An array of parsed block data.
+        */
+       private static function to_blocks( $menu_items, $menu_items_by_parent_id ) {
+
+               if ( empty( $menu_items ) ) {
+                       return array();
+               }
+
+               $blocks = array();
+
+               foreach ( $menu_items as $menu_item ) {
+                       $class_name       = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null;
+                       $id               = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null;
+                       $opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target;
+                       $rel              = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null;
+                       $kind             = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom';
+
+                       $block = array(
+                               'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link',
+                               'attrs'     => array(
+                                       'className'     => $class_name,
+                                       'description'   => $menu_item->description,
+                                       'id'            => $id,
+                                       'kind'          => $kind,
+                                       'label'         => $menu_item->title,
+                                       'opensInNewTab' => $opens_in_new_tab,
+                                       'rel'           => $rel,
+                                       'title'         => $menu_item->attr_title,
+                                       'type'          => $menu_item->object,
+                                       'url'           => $menu_item->url,
+                               ),
+                       );
+
+                       $block['innerBlocks']  = isset( $menu_items_by_parent_id[ $menu_item->ID ] )
+                       ? static::to_blocks( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id )
+                       : array();
+                       $block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] );
+
+                       $blocks[] = $block;
+               }
+
+               return $blocks;
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/class-wp-classic-to-block-menu-converter.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesclasswpnavigationfallbackphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/class-wp-navigation-fallback.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-navigation-fallback.php                            (rev 0)
+++ trunk/src/wp-includes/class-wp-navigation-fallback.php      2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,247 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * WP_Navigation_Fallback class
+ *
+ * Manages fallback behavior for Navigation menus.
+ *
+ * @package WordPress
+ * @subpackage Navigation
+ * @since 6.3.0
+ */
+
+/**
+ * Manages fallback behavior for Navigation menus.
+ *
+ * @access public
+ * @since 6.3.0.
+ */
+class WP_Navigation_Fallback {
+
+       /**
+        * Gets (and/or creates) an appropriate fallback Navigation Menu.
+        *
+        * @since 6.3.0.
+        *
+        * @return WP_Post|null the fallback Navigation Post or null.
+        */
+       public static function get_fallback() {
+
+               $fallback = static::get_most_recently_published_navigation();
+
+               if ( $fallback ) {
+                       return $fallback;
+               }
+
+               $fallback = static::create_classic_menu_fallback();
+
+               if ( $fallback && ! is_wp_error( $fallback ) ) {
+                       // Return the newly created fallback post object which will now be the most recently created navigation menu.
+                       return $fallback instanceof WP_Post ? $fallback : static::get_most_recently_published_navigation();
+               }
+
+               $fallback = static::create_default_fallback();
+
+               if ( $fallback && ! is_wp_error( $fallback ) ) {
+                       // Return the newly created fallback post object which will now be the most recently created navigation menu.
+                       return $fallback instanceof WP_Post ? $fallback : static::get_most_recently_published_navigation();
+               }
+
+               return null;
+       }
+
+       /**
+        * Finds the most recently published `wp_navigation` post type.
+        *
+        * @since 6.3.0.
+        *
+        * @return WP_Post|null the first non-empty Navigation or null.
+        */
+       private static function get_most_recently_published_navigation() {
+
+               $parsed_args = array(
+                       'post_type'              => 'wp_navigation',
+                       'no_found_rows'          => true,
+                       'update_post_meta_cache' => false,
+                       'update_post_term_cache' => false,
+                       'order'                  => 'DESC',
+                       'orderby'                => 'date',
+                       'post_status'            => 'publish',
+                       'posts_per_page'         => 1,
+               );
+
+               $navigation_post = new WP_Query( $parsed_args );
+
+               if ( count( $navigation_post->posts ) > 0 ) {
+                       return $navigation_post->posts[0];
+               }
+
+               return null;
+       }
+
+       /**
+        * Creates a Navigation Menu post from a Classic Menu.
+        *
+        * @since 6.3.0.
+        *
+        * @return int|WP_Error The post ID of the default fallback menu or a WP_Error object.
+        */
+       private static function create_classic_menu_fallback() {
+               // See if we have a classic menu.
+               $classic_nav_menu = static::get_fallback_classic_menu();
+
+               if ( ! $classic_nav_menu ) {
+                       return new WP_Error( 'no_classic_menus', __( 'No Classic Menus found.' ) );
+               }
+
+               // If there is a classic menu then convert it to blocks.
+               $classic_nav_menu_blocks = WP_Classic_To_Block_Menu_Converter::convert( $classic_nav_menu );
+
+               if ( empty( $classic_nav_menu_blocks ) ) {
+                       return new WP_Error( 'cannot_convert_classic_menu', __( 'Unable to convert Classic Menu to blocks.' ) );
+               }
+
+               // Create a new navigation menu from the classic menu.
+               $classic_menu_fallback = wp_insert_post(
+                       array(
+                               'post_content' => $classic_nav_menu_blocks,
+                               'post_title'   => $classic_nav_menu->name,
+                               'post_name'    => $classic_nav_menu->slug,
+                               'post_status'  => 'publish',
+                               'post_type'    => 'wp_navigation',
+                       ),
+                       true // So that we can check whether the result is an error.
+               );
+
+               return $classic_menu_fallback;
+       }
+
+       /**
+        * Determine the most appropriate classic navigation menu to use as a fallback.
+        *
+        * @since 6.3.0.
+        *
+        * @return WP_Term|null The most appropriate classic navigation menu to use as a fallback.
+        */
+       private static function get_fallback_classic_menu() {
+               $classic_nav_menus = wp_get_nav_menus();
+
+               if ( ! $classic_nav_menus || is_wp_error( $classic_nav_menus ) ) {
+                       return null;
+               }
+
+               $nav_menu = static::get_nav_menu_at_primary_location();
+
+               if ( $nav_menu ) {
+                       return $nav_menu;
+               }
+
+               $nav_menu = static::get_nav_menu_with_primary_slug( $classic_nav_menus );
+
+               if ( $nav_menu ) {
+                       return $nav_menu;
+               }
+
+               return static::get_most_recently_created_nav_menu( $classic_nav_menus );
+       }
+
+
+       /**
+        * Sorts the classic menus and returns the most recently created one.
+        *
+        * @since 6.3.0.
+        *
+        * @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects.
+        * @return WP_Term The most recently created classic nav menu.
+        */
+       private static function get_most_recently_created_nav_menu( $classic_nav_menus ) {
+               usort(
+                       $classic_nav_menus,
+                       static function( $a, $b ) {
+                               return $b->term_id - $a->term_id;
+                       }
+               );
+
+               return $classic_nav_menus[0];
+       }
+
+       /**
+        * Returns the classic menu with the slug `primary` if it exists.
+        *
+        * @since 6.3.0.
+        *
+        * @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects.
+        * @return WP_Term|null The classic nav menu with the slug `primary` or null.
+        */
+       private static function get_nav_menu_with_primary_slug( $classic_nav_menus ) {
+               foreach ( $classic_nav_menus as $classic_nav_menu ) {
+                       if ( 'primary' === $classic_nav_menu->slug ) {
+                               return $classic_nav_menu;
+                       }
+               }
+
+               return null;
+       }
+
+
+       /**
+        * Gets the classic menu assigned to the `primary` navigation menu location
+        * if it exists.
+        *
+        * @since 6.3.0.
+        *
+        * @return WP_Term|null The classic nav menu assigned to the `primary` location or null.
+        */
+       private static function get_nav_menu_at_primary_location() {
+               $locations = get_nav_menu_locations();
+
+               if ( isset( $locations['primary'] ) ) {
+                       $primary_menu = wp_get_nav_menu_object( $locations['primary'] );
+
+                       if ( $primary_menu ) {
+                               return $primary_menu;
+                       }
+               }
+
+               return null;
+       }
+
+       /**
+        * Creates a default Navigation Block Menu fallback.
+        *
+        * @since 6.3.0.
+        *
+        * @return int|WP_Error The post ID of the default fallback menu or a WP_Error object.
+        */
+       private static function create_default_fallback() {
+
+               $default_blocks = static::get_default_fallback_blocks();
+
+               // Create a new navigation menu from the fallback blocks.
+               $default_fallback = wp_insert_post(
+                       array(
+                               'post_content' => $default_blocks,
+                               'post_title'   => _x( 'Navigation', 'Title of a Navigation menu' ),
+                               'post_name'    => 'navigation',
+                               'post_status'  => 'publish',
+                               'post_type'    => 'wp_navigation',
+                       ),
+                       true // So that we can check whether the result is an error.
+               );
+
+               return $default_fallback;
+       }
+
+       /**
+        * Gets the rendered markup for the default fallback blocks.
+        *
+        * @since 6.3.0.
+        *
+        * @return string default blocks markup to use a the fallback.
+        */
+       private static function get_default_fallback_blocks() {
+               $registry = WP_Block_Type_Registry::get_instance();
+
+               // If `core/page-list` is not registered then use empty blocks.
+               return $registry->is_registered( 'core/page-list' ) ? '<!-- wp:page-list /-->' : '';
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/class-wp-navigation-fallback.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesnavigationfallbackphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/navigation-fallback.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/navigation-fallback.php                             (rev 0)
+++ trunk/src/wp-includes/navigation-fallback.php       2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,41 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Navigation Fallback
+ *
+ * Functions required for managing Navigation fallbacks behavior.
+ *
+ * @package WordPress
+ * @since 6.3.0
+ */
+
+/**
+ * Expose additional fields in the embeddable links of the
+ * Navigation Fallback REST endpoint.
+ *
+ * The endpoint may embed the full Navigation Menu object into the
+ * response as the `self` link. By default the Posts Controller
+ * will only exposes a limited subset of fields but the editor requires
+ * additional fields to be available in order to utilise the menu.
+ *
+ * @since 6.3.0
+ *
+ * @param array $schema the schema for the `wp_navigation` post.
+ * @return array the modified schema.
+ */
+function wp_add_fields_to_navigation_fallback_embeded_links( $schema ) {
+       // Expose top level fields.
+       $schema['properties']['status']['context']  = array_merge( $schema['properties']['status']['context'], array( 'embed' ) );
+       $schema['properties']['content']['context'] = array_merge( $schema['properties']['content']['context'], array( 'embed' ) );
+
+       // Expose sub properties of content field.
+       $schema['properties']['content']['properties']['raw']['context']           = array_merge( $schema['properties']['content']['properties']['raw']['context'], array( 'embed' ) );
+       $schema['properties']['content']['properties']['rendered']['context']      = array_merge( $schema['properties']['content']['properties']['rendered']['context'], array( 'embed' ) );
+       $schema['properties']['content']['properties']['block_version']['context'] = array_merge( $schema['properties']['content']['properties']['block_version']['context'], array( 'embed' ) );
+
+       return $schema;
+}
+
+add_filter(
+       'rest_wp_navigation_item_schema',
+       'wp_add_fields_to_navigation_fallback_embeded_links'
+);
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/navigation-fallback.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesrestapiendpointsclasswprestnavigationfallbackcontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-navigation-fallback-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-navigation-fallback-controller.php                         (rev 0)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-navigation-fallback-controller.php   2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,193 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * WP_REST_Navigation_Fallback_Controller class
+ *
+ * REST Controller to create/fetch a fallback Navigation Menu.
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 6.3.0
+ */
+
+/**
+ * REST Controller to fetch a fallback Navigation Block Menu. If needed it creates one.
+ *
+ * @since 6.3.0.
+ */
+class WP_REST_Navigation_Fallback_Controller extends WP_REST_Controller {
+
+       /**
+        * The Post Type for the Controller
+        *
+        * @since 6.3.0.
+        *
+        * @var string
+        */
+       private $post_type;
+
+       /**
+        * Constructs the controller.
+        *
+        * @since 6.3.0.
+        */
+       public function __construct() {
+               $this->namespace = 'wp-block-editor/v1';
+               $this->rest_base = 'navigation-fallback';
+               $this->post_type = 'wp_navigation';
+       }
+
+       /**
+        * Registers the controllers routes.
+        *
+        * @since 6.3.0.
+        *
+        * @return void
+        */
+       public function register_routes() {
+
+               // Lists a single nav item based on the given id or slug.
+               register_rest_route(
+                       $this->namespace,
+                       '/' . $this->rest_base,
+                       array(
+                               array(
+                                       'methods'             => WP_REST_Server::READABLE,
+                                       'callback'            => array( $this, 'get_item' ),
+                                       'permission_callback' => array( $this, 'get_item_permissions_check' ),
+                                       'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::READABLE ),
+                               ),
+                               'schema' => array( $this, 'get_item_schema' ),
+                       )
+               );
+       }
+
+       /**
+        * Checks if a given request has access to read fallbacks.
+        *
+        * @since 6.3.0.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
+        */
+       public function get_item_permissions_check( $request ) {
+
+               $post_type = get_post_type_object( $this->post_type );
+
+               // Getting fallbacks requires creating and reading `wp_navigation` posts.
+               if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( 'edit_theme_options' ) || ! current_user_can( 'edit_posts' ) ) {
+                       return new WP_Error(
+                               'rest_cannot_create',
+                               __( 'Sorry, you are not allowed to create Navigation Menus as this user.' ),
+                               array( 'status' => rest_authorization_required_code() )
+                       );
+               }
+
+               if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
+                       return new WP_Error(
+                               'rest_forbidden_context',
+                               __( 'Sorry, you are not allowed to edit Navigation Menus as this user.' ),
+                               array( 'status' => rest_authorization_required_code() )
+                       );
+               }
+
+               return true;
+       }
+
+       /**
+        * Gets the most appropriate fallback Navigation Menu.
+        *
+        * @since 6.3.0.
+        *
+        * @param WP_REST_Request $request Full details about the request.
+        * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+        */
+       public function get_item( $request ) {
+               $post = WP_Navigation_Fallback::get_fallback();
+
+               if ( empty( $post ) ) {
+                       return rest_ensure_response( new WP_Error( 'no_fallback_menu', __( 'No fallback menu found.' ), array( 'status' => 404 ) ) );
+               }
+
+               $response = $this->prepare_item_for_response( $post, $request );
+
+               return $response;
+       }
+
+       /**
+        * Retrieves the fallbacks' schema, conforming to JSON Schema.
+        *
+        * @since 6.3.0.
+        *
+        * @return array Item schema data.
+        */
+       public function get_item_schema() {
+               if ( $this->schema ) {
+                       return $this->add_additional_fields_schema( $this->schema );
+               }
+
+               $this->schema = array(
+                       '$schema'    => 'http://json-schema.org/draft-04/schema#',
+                       'title'      => 'navigation-fallback',
+                       'type'       => 'object',
+                       'properties' => array(
+                               'id' => array(
+                                       'description' => __( 'The unique identifier for the Navigation Menu.' ),
+                                       'type'        => 'integer',
+                                       'context'     => array( 'view', 'edit', 'embed' ),
+                                       'readonly'    => true,
+                               ),
+                       ),
+               );
+
+               return $this->add_additional_fields_schema( $this->schema );
+       }
+
+       /**
+        * Matches the post data to the schema we want.
+        *
+        * @since 6.3.0.
+        *
+        * @param WP_Post         $item     The wp_navigation Post object whose response is being prepared.
+        * @param WP_REST_Request $request  Request object.
+        * @return WP_REST_Response $response The response data.
+        */
+       public function prepare_item_for_response( $item, $request ) {
+               $data = array();
+
+               $fields = $this->get_fields_for_response( $request );
+
+               if ( rest_is_field_included( 'id', $fields ) ) {
+                       $data['id'] = (int) $item->ID;
+               }
+
+               $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+               $data    = $this->add_additional_fields_to_object( $data, $request );
+               $data    = $this->filter_response_by_context( $data, $context );
+
+               $response = rest_ensure_response( $data );
+
+               if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
+                       $links = $this->prepare_links( $item );
+                       $response->add_links( $links );
+               }
+
+               return $response;
+       }
+
+       /**
+        * Prepares the links for the request.
+        *
+        * @since 6.3.0.
+        *
+        * @param WP_Post $post the Navigation Menu post object.
+        * @return array Links for the given request.
+        */
+       private function prepare_links( $post ) {
+               return array(
+                       'self' => array(
+                               'href'       => rest_url( rest_get_route_for_post( $post->ID ) ),
+                               'embeddable' => true,
+                       ),
+               );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-navigation-fallback-controller.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunksrcwpincludesrestapiphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/rest-api.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/rest-api.php        2023-06-27 05:34:12 UTC (rev 56051)
+++ trunk/src/wp-includes/rest-api.php  2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -377,6 +377,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">        // Site Editor Export.
</span><span class="cx" style="display: block; padding: 0 10px">        $controller = new WP_REST_Edit_Site_Export_Controller();
</span><span class="cx" style="display: block; padding: 0 10px">        $controller->register_routes();
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       // Navigation Fallback.
+       $controller = new WP_REST_Navigation_Fallback_Controller();
+       $controller->register_routes();
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span></span></pre></div>
<a id="trunksrcwpsettingsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-settings.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-settings.php 2023-06-27 05:34:12 UTC (rev 56051)
+++ trunk/src/wp-settings.php   2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -291,6 +291,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-widgets-controller.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-templates-controller.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-url-details-controller.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-navigation-fallback-controller.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php';
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -321,6 +322,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-block-parser-block.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-block-parser-frame.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/class-wp-block-parser.php';
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+require ABSPATH . WPINC . '/class-wp-classic-to-block-menu-converter.php';
+require ABSPATH . WPINC . '/class-wp-navigation-fallback.php';
</ins><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/blocks.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/blocks/index.php';
</span><span class="cx" style="display: block; padding: 0 10px"> require ABSPATH . WPINC . '/block-editor.php';
</span></span></pre></div>
<a id="trunktestsphpunittestseditorclassictoblockmenuconverterphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/editor/classic-to-block-menu-converter.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/editor/classic-to-block-menu-converter.php                              (rev 0)
+++ trunk/tests/phpunit/tests/editor/classic-to-block-menu-converter.php        2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,221 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests WP_Classic_To_Block_Menu_Converter_Test
+ *
+ * @package WordPress
+ */
+
+/**
+ * Tests for the WP_Classic_To_Block_Menu_Converter_Test class.
+ */
+class WP_Classic_To_Block_Menu_Converter_Test extends WP_UnitTestCase {
+
+       /**
+        * @ticket 58557
+        * @covers WP_Classic_To_Block_Menu_Converter::get_fallback
+        */
+       public function test_class_exists() {
+               $this->assertTrue( class_exists( 'WP_Classic_To_Block_Menu_Converter' ) );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_Classic_To_Block_Menu_Converter::convert
+        * @dataProvider provider_test_passing_non_menu_object_to_converter_returns_wp_error
+        */
+       public function test_passing_non_menu_object_to_converter_returns_wp_error( $data ) {
+
+               $result = WP_Classic_To_Block_Menu_Converter::convert( $data );
+
+               $this->assertTrue( is_wp_error( $result ), 'Should be a WP_Error instance' );
+
+               $this->assertEquals( 'invalid_menu', $result->get_error_code(), 'Error code should indicate invalidity of menu argument.' );
+
+               $this->assertEquals( 'The menu provided is not a valid menu.', $result->get_error_message(), 'Error message should communicate invalidity of menu argument.' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_Classic_To_Block_Menu_Converter::convert
+        */
+       public function provider_test_passing_non_menu_object_to_converter_returns_wp_error() {
+               return array(
+                       array( 1 ),
+                       array( -1 ),
+                       array( '1' ),
+                       array( 'not a menu object' ),
+                       array( true ),
+                       array( false ),
+                       array( array() ),
+                       array( new stdClass() ),
+               );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_Classic_To_Block_Menu_Converter::convert
+        */
+       public function test_can_convert_classic_menu_to_blocks() {
+
+               $menu_id = wp_create_nav_menu( 'Classic Menu' );
+
+               wp_update_nav_menu_item(
+                       $menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'Classic Menu Item 1',
+                               'menu-item-url'    => '/classic-menu-item-1',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               $second_menu_item_id = wp_update_nav_menu_item(
+                       $menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'Classic Menu Item 2',
+                               'menu-item-url'    => '/classic-menu-item-2',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               wp_update_nav_menu_item(
+                       $menu_id,
+                       0,
+                       array(
+                               'menu-item-title'     => 'Nested Menu Item 1',
+                               'menu-item-url'       => '/nested-menu-item-1',
+                               'menu-item-status'    => 'publish',
+                               'menu-item-parent-id' => $second_menu_item_id,
+                       )
+               );
+
+               $classic_nav_menu = wp_get_nav_menu_object( $menu_id );
+
+               $blocks = WP_Classic_To_Block_Menu_Converter::convert( $classic_nav_menu );
+
+               $this->assertNotEmpty( $blocks );
+
+               $parsed_blocks = parse_blocks( $blocks );
+
+               $first_block  = $parsed_blocks[0];
+               $second_block = $parsed_blocks[1];
+               $nested_block = $parsed_blocks[1]['innerBlocks'][0];
+
+               $this->assertEquals( 'core/navigation-link', $first_block['blockName'], 'First block name should be "core/navigation-link"' );
+
+               $this->assertEquals( 'Classic Menu Item 1', $first_block['attrs']['label'], 'First block label should match.' );
+
+               $this->assertEquals( '/classic-menu-item-1', $first_block['attrs']['url'], 'First block URL should match.' );
+
+               // Assert parent of nested menu item is a submenu block.
+               $this->assertEquals( 'core/navigation-submenu', $second_block['blockName'], 'Second block name should be "core/navigation-submenu"' );
+
+               $this->assertEquals( 'Classic Menu Item 2', $second_block['attrs']['label'], 'Second block label should match.' );
+
+               $this->assertEquals( '/classic-menu-item-2', $second_block['attrs']['url'], 'Second block URL should match.' );
+
+               $this->assertEquals( 'core/navigation-link', $nested_block['blockName'], 'Nested block name should be "core/navigation-link"' );
+
+               $this->assertEquals( 'Nested Menu Item 1', $nested_block['attrs']['label'], 'Nested block label should match.' );
+
+               $this->assertEquals( '/nested-menu-item-1', $nested_block['attrs']['url'], 'Nested block URL should match.' );
+
+               wp_delete_nav_menu( $menu_id );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_Classic_To_Block_Menu_Converter::convert
+        */
+       public function test_does_not_convert_menu_items_with_non_publish_status() {
+
+                       $menu_id = wp_create_nav_menu( 'Classic Menu' );
+
+                       wp_update_nav_menu_item(
+                               $menu_id,
+                               0,
+                               array(
+                                       'menu-item-title'  => 'Classic Menu Item 1',
+                                       'menu-item-url'    => '/classic-menu-item-1',
+                                       'menu-item-status' => 'publish',
+                               )
+                       );
+
+                       wp_update_nav_menu_item(
+                               $menu_id,
+                               0,
+                               array(
+                                       'menu-item-status' => 'draft',
+                                       'menu-item-title'  => 'Draft Menu Item',
+                                       'menu-item-url'    => '/draft-menu-item',
+                               )
+                       );
+
+                       wp_update_nav_menu_item(
+                               $menu_id,
+                               0,
+                               array(
+                                       'menu-item-status' => 'private',
+                                       'menu-item-title'  => 'Private Item',
+                                       'menu-item-url'    => '/private-menu-item',
+                               )
+                       );
+
+                       wp_update_nav_menu_item(
+                               $menu_id,
+                               0,
+                               array(
+                                       'menu-item-status' => 'pending',
+                                       'menu-item-title'  => 'Pending Menu Item',
+                                       'menu-item-url'    => '/pending-menu-item',
+                               )
+                       );
+
+                       wp_update_nav_menu_item(
+                               $menu_id,
+                               0,
+                               array(
+                                       'menu-item-status' => 'future',
+                                       'menu-item-title'  => 'Future Menu Item',
+                                       'menu-item-url'    => '/future-menu-item',
+                               )
+                       );
+
+                       $classic_nav_menu = wp_get_nav_menu_object( $menu_id );
+
+                       $blocks = WP_Classic_To_Block_Menu_Converter::convert( $classic_nav_menu );
+
+                       $this->assertNotEmpty( $blocks );
+
+                       $parsed_blocks = parse_blocks( $blocks );
+
+                       $this->assertCount( 1, $parsed_blocks, 'Should only be one block in the array.' );
+
+                       $this->assertEquals( 'core/navigation-link', $parsed_blocks[0]['blockName'], 'First block name should be "core/navigation-link"' );
+
+                       $this->assertEquals( 'Classic Menu Item 1', $parsed_blocks[0]['attrs']['label'], 'First block label should match.' );
+
+                       $this->assertEquals( '/classic-menu-item-1', $parsed_blocks[0]['attrs']['url'], 'First block URL should match.' );
+
+                       wp_delete_nav_menu( $menu_id );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_Classic_To_Block_Menu_Converter::convert
+        */
+       public function test_returns_empty_array_for_menus_with_no_items() {
+               $menu_id = wp_create_nav_menu( 'Empty Menu' );
+
+               $classic_nav_menu = wp_get_nav_menu_object( $menu_id );
+
+               $blocks = WP_Classic_To_Block_Menu_Converter::convert( $classic_nav_menu );
+
+               $this->assertEmpty( $blocks, 'Result should be empty.' );
+
+               $this->assertIsArray( $blocks, 'Result should be empty array.' );
+
+               wp_delete_nav_menu( $menu_id );
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/editor/classic-to-block-menu-converter.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestseditornavigationfallbackphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/editor/navigation-fallback.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/editor/navigation-fallback.php                          (rev 0)
+++ trunk/tests/phpunit/tests/editor/navigation-fallback.php    2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,348 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests WP_Navigation_Fallback
+ *
+ * @package WordPress
+ */
+
+/**
+ * Tests for the WP_Navigation_Fallback class.
+ */
+class WP_Navigation_Fallback_Test extends WP_UnitTestCase {
+
+       protected static $admin_user;
+       protected static $editor_user;
+
+       public static function wpSetUpBeforeClass( $factory ) {
+               self::$admin_user = $factory->user->create( array( 'role' => 'administrator' ) );
+
+               self::$editor_user = $factory->user->create( array( 'role' => 'editor' ) );
+       }
+
+       public function set_up() {
+               parent::set_up();
+
+               wp_set_current_user( self::$admin_user );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller
+        */
+       public function test_it_exists() {
+               $this->assertTrue( class_exists( 'WP_Navigation_Fallback' ), 'WP_Navigation_Fallback class should exist.' );
+       }
+
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::get_fallback
+        */
+       public function test_should_return_a_default_fallback_navigation_menu_in_absence_of_other_fallbacks() {
+               $data = WP_Navigation_Fallback::get_fallback();
+
+               $this->assertInstanceOf( 'WP_Post', $data, 'Response should be of the correct type.' );
+
+               $this->assertEquals( 'wp_navigation', $data->post_type, 'Fallback menu type should be `wp_navigation`' );
+
+               $this->assertEquals( 'Navigation', $data->post_title, 'Fallback menu title should be the default fallback title' );
+
+               $this->assertEquals( 'navigation', $data->post_name, 'Fallback menu slug (post_name) should be the default slug' );
+
+               $this->assertEquals( '<!-- wp:page-list /-->', $data->post_content );
+
+               $navs_in_db = $this->get_navigations_in_database();
+
+               $this->assertCount( 1, $navs_in_db, 'The fallback Navigation post should be the only one in the database.' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::get_fallback
+        */
+       public function test_should_return_a_default_fallback_navigation_menu_with_no_blocks_if_page_list_block_is_not_registered() {
+
+               $original_page_list_block = WP_Block_Type_Registry::get_instance()->get_registered( 'core/page-list' );
+
+               unregister_block_type( 'core/page-list' );
+
+               $data = WP_Navigation_Fallback::get_fallback();
+
+               $this->assertInstanceOf( 'WP_Post', $data, 'Response should be of the correct type.' );
+
+               $this->assertNotEquals( '<!-- wp:page-list /-->', $data->post_content, 'Navigation Menu should not contain a Page List block.' );
+
+               $this->assertEmpty( $data->post_content, 'Menu should be empty.' );
+
+               register_block_type( 'core/page-list', $original_page_list_block );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::get_fallback
+        */
+       public function test_should_handle_consecutive_invocations() {
+               // Invoke the method multiple times to ensure that it doesn't create a new fallback menu on each invocation.
+               WP_Navigation_Fallback::get_fallback();
+               WP_Navigation_Fallback::get_fallback();
+
+               // Assert on the final invocation.
+               $data = WP_Navigation_Fallback::get_fallback();
+
+               $this->assertInstanceOf( 'WP_Post', $data, 'Response should be of the correct type.' );
+
+               $this->assertEquals( 'Navigation', $data->post_title, 'Fallback menu title should be the default title' );
+
+               $navs_in_db = $this->get_navigations_in_database();
+
+               $this->assertCount( 1, $navs_in_db, 'The fallback Navigation post should be the only one in the database.' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::get_fallback
+        */
+       public function test_should_return_the_most_recently_created_navigation_menu() {
+
+               self::factory()->post->create_and_get(
+                       array(
+                               'post_type'    => 'wp_navigation',
+                               'post_title'   => 'Existing Navigation Menu 1',
+                               'post_content' => '<!-- wp:page-list /-->',
+                       )
+               );
+
+               $most_recently_published_nav = self::factory()->post->create_and_get(
+                       array(
+                               'post_type'    => 'wp_navigation',
+                               'post_title'   => 'Existing Navigation Menu 2',
+                               'post_content' => '<!-- wp:navigation-link {"label":"Hello world","type":"post","id":1,"url":"/hello-world","kind":"post-type"} /-->',
+                       )
+               );
+
+               $data = WP_Navigation_Fallback::get_fallback();
+
+               $this->assertInstanceOf( 'WP_Post', $data, 'Response should be of the correct type.' );
+
+               $this->assertEquals( $most_recently_published_nav->post_title, $data->post_title, 'Fallback menu title should be the same as the most recently created menu.' );
+
+               $this->assertEquals( $most_recently_published_nav->post_name, $data->post_name, 'Post name should be the same as the most recently created menu.' );
+
+               $this->assertEquals( $most_recently_published_nav->post_content, $data->post_content, 'Post content should be the same as the most recently created menu.' );
+
+               // Check that no new Navigation menu was created.
+               $navs_in_db = $this->get_navigations_in_database();
+
+               $this->assertCount( 2, $navs_in_db, 'Only the existing Navigation menus should be present in the database.' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::get_fallback
+        */
+       public function test_should_return_fallback_navigation_from_existing_classic_menu_if_no_navigation_menus_exist() {
+               $menu_id = wp_create_nav_menu( 'Existing Classic Menu' );
+
+               wp_update_nav_menu_item(
+                       $menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'Classic Menu Item 1',
+                               'menu-item-url'    => '/classic-menu-item-1',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               $data = WP_Navigation_Fallback::get_fallback();
+
+               $this->assertInstanceOf( 'WP_Post', $data, 'Response should be of the correct type.' );
+
+               $this->assertEquals( 'Existing Classic Menu', $data->post_title, 'Fallback menu title should be the same as the classic menu.' );
+
+               // Assert that the fallback contains a navigation-link block.
+               $this->assertStringContainsString( '<!-- wp:navigation-link', $data->post_content, 'The fallback Navigation Menu should contain a `core/navigation-link` block.' );
+
+               // Assert that fallback post_content contains the expected menu item title.
+               $this->assertStringContainsString( '"label":"Classic Menu Item 1"', $data->post_content, 'The fallback Navigation Menu should contain menu item with a label matching the title of the menu item from the Classic Menu.' );
+
+               // Assert that fallback post_content contains the expected menu item url.
+               $this->assertStringContainsString( '"url":"/classic-menu-item-1"', $data->post_content, 'The fallback Navigation Menu should contain menu item with a url matching the slug of the menu item from the Classic Menu.' );
+
+               // Check that only a single Navigation fallback was created.
+               $navs_in_db = $this->get_navigations_in_database();
+               $this->assertCount( 1, $navs_in_db, 'A single Navigation menu should be present in the database.' );
+
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::get_fallback
+        */
+       public function test_should_prioritise_fallback_to_classic_menu_in_primary_location() {
+               $pl_menu_id = wp_create_nav_menu( 'Classic Menu in Primary Location' );
+
+               wp_update_nav_menu_item(
+                       $pl_menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'PL Classic Menu Item',
+                               'menu-item-url'    => '/pl-classic-menu-item',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               $another_menu_id = wp_create_nav_menu( 'Another Classic Menu' );
+
+               wp_update_nav_menu_item(
+                       $another_menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'Another Classic Menu Item',
+                               'menu-item-url'    => '/another-classic-menu-item',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               $locations            = get_nav_menu_locations();
+               $locations['primary'] = $pl_menu_id;
+               $locations['header']  = $another_menu_id;
+               set_theme_mod( 'nav_menu_locations', $locations );
+
+               $data = WP_Navigation_Fallback::get_fallback();
+
+               $this->assertInstanceOf( 'WP_Post', $data, 'Response should be of the correct type.' );
+
+               $this->assertEquals( 'Classic Menu in Primary Location', $data->post_title, 'Fallback menu title should match the menu in the "primary" location.' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::get_fallback
+        */
+       public function test_should_fallback_to_classic_menu_with_primary_slug() {
+
+               // Creates a classic menu with the slug "primary".
+               $primary_menu_id = wp_create_nav_menu( 'Primary' );
+
+               wp_update_nav_menu_item(
+                       $primary_menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'Classic Menu Item',
+                               'menu-item-url'    => '/classic-menu-item',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               $another_menu_id = wp_create_nav_menu( 'Another Classic Menu' );
+
+               wp_update_nav_menu_item(
+                       $another_menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'Another Classic Menu Item',
+                               'menu-item-url'    => '/another-classic-menu-item',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               $data = WP_Navigation_Fallback::get_fallback();
+
+               $this->assertInstanceOf( 'WP_Post', $data, 'Response should be of the correct type.' );
+
+               $this->assertEquals( 'Primary', $data->post_title, 'Fallback menu title should match the menu with the slug "primary".' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::get_fallback
+        */
+       public function test_should_fallback_to_most_recently_created_classic_menu() {
+
+               // Creates a classic menu with the slug "primary".
+               $primary_menu_id = wp_create_nav_menu( 'Older Classic Menu' );
+
+               wp_update_nav_menu_item(
+                       $primary_menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'Classic Menu Item',
+                               'menu-item-url'    => '/classic-menu-item',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               $most_recent_menu_id = wp_create_nav_menu( 'Most Recent Classic Menu' );
+
+               wp_update_nav_menu_item(
+                       $most_recent_menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'Another Classic Menu Item',
+                               'menu-item-url'    => '/another-classic-menu-item',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               $data = WP_Navigation_Fallback::get_fallback();
+
+               $this->assertInstanceOf( 'WP_Post', $data, 'Response should be of the correct type.' );
+
+               $this->assertEquals( 'Most Recent Classic Menu', $data->post_title, 'Fallback menu title should match the menu that was created most recently.' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::get_fallback
+        */
+       public function test_should_not_create_fallback_from_classic_menu_if_a_navigation_menu_already_exists() {
+               $menu_id = wp_create_nav_menu( 'Existing Classic Menu' );
+
+               wp_update_nav_menu_item(
+                       $menu_id,
+                       0,
+                       array(
+                               'menu-item-title'  => 'Classic Menu Item 1',
+                               'menu-item-url'    => '/classic-menu-item-1',
+                               'menu-item-status' => 'publish',
+                       )
+               );
+
+               $existing_navigation_menu = self::factory()->post->create_and_get(
+                       array(
+                               'post_type'    => 'wp_navigation',
+                               'post_title'   => 'Existing Navigation Menu 1',
+                               'post_content' => '<!-- wp:page-list /-->',
+                       )
+               );
+
+               $data = WP_Navigation_Fallback::get_fallback();
+
+               $this->assertInstanceOf( 'WP_Post', $data, 'Response should be of the correct type.' );
+
+               $this->assertEquals( $existing_navigation_menu->post_title, $data->post_title, 'Fallback menu title should be the same as the existing Navigation menu.' );
+
+               $this->assertNotEquals( 'Existing Classic Menu', $data->post_title, 'Fallback menu title should not be the same as the Classic Menu.' );
+
+               // Check that only a single Navigation fallback was created.
+               $navs_in_db = $this->get_navigations_in_database();
+
+               $this->assertCount( 1, $navs_in_db, 'Only the existing Navigation menus should be present in the database.' );
+
+       }
+
+       private function get_navigations_in_database() {
+               $navs_in_db = new WP_Query(
+                       array(
+                               'post_type'      => 'wp_navigation',
+                               'post_status'    => 'publish',
+                               'posts_per_page' => -1,
+                               'orderby'        => 'date',
+                               'order'          => 'DESC',
+                       )
+               );
+
+               return $navs_in_db->posts ? $navs_in_db->posts : array();
+       }
+
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/editor/navigation-fallback.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsrestapirestnavigationfallbackcontrollerphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php                                (rev 0)
+++ trunk/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php  2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,201 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Unit tests covering WP_REST_Navigation_Fallback_Controller functionality.
+ *
+ * Note: that these tests are designed to provide high level coverage only. The majority of the tests
+ * are made directly against the WP_Navigation_Fallback class as this:
+ *
+ * - is where the bulk of the logic is.
+ * - is also consumed by the Navigation block's server side rendering.
+ *
+ * @package WordPress
+ * @subpackage REST API
+ *
+ * @covers WP_REST_Navigation_Fallback_Controller
+ */
+
+/**
+ * @group restapi
+ * @group navigation
+ */
+class WP_REST_Navigation_Fallback_Controller_Test extends WP_Test_REST_Controller_Testcase {
+
+       protected static $admin_user;
+       protected static $editor_user;
+
+       public static function wpSetUpBeforeClass( $factory ) {
+               self::$admin_user = $factory->user->create( array( 'role' => 'administrator' ) );
+
+               self::$editor_user = $factory->user->create( array( 'role' => 'editor' ) );
+       }
+
+       public function set_up() {
+               parent::set_up();
+
+               wp_set_current_user( self::$admin_user );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller::register_routes
+        *
+        * @since 6.3.0 Added Navigation Fallbacks endpoint.
+        */
+       public function test_register_routes() {
+               $routes = rest_get_server()->get_routes();
+
+               $this->assertArrayHasKey( '/wp-block-editor/v1/navigation-fallback', $routes, 'Fallback route should be registered.' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller
+        *
+        * @since 6.3.0 Added Navigation Fallbacks endpoint.
+        */
+       public function test_should_not_return_menus_for_users_without_permissions() {
+
+               wp_set_current_user( self::$editor_user );
+
+               $request  = new WP_REST_Request( 'GET', '/wp-block-editor/v1/navigation-fallback' );
+               $response = rest_get_server()->dispatch( $request );
+               $data     = $response->get_data();
+
+               $this->assertEquals( 403, $response->get_status(), 'Response should indicate user does not have permission.' );
+
+               $this->assertEquals( 'rest_cannot_create', $data['code'], 'Response should indicate user cannot create.' );
+
+               $this->assertEquals( 'Sorry, you are not allowed to create Navigation Menus as this user.', $data['message'], 'Response should indicate failed request status.' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller
+        *
+        * @since 6.3.0 Added Navigation Fallbacks endpoint.
+        */
+       public function test_get_item() {
+
+               $request  = new WP_REST_Request( 'GET', '/wp-block-editor/v1/navigation-fallback' );
+               $response = rest_get_server()->dispatch( $request );
+               $data     = $response->get_data();
+
+               $this->assertEquals( 200, $response->get_status(), 'Status should indicate successful request.' );
+
+               $this->assertIsArray( $data, 'Response should be of correct type.' );
+
+               $this->assertArrayHasKey( 'id', $data, 'Response should contain expected fields.' );
+
+               $this->assertEquals( 'wp_navigation', get_post_type( $data['id'] ), '"id" field should represent a post of type "wp_navigation"' );
+
+               // Check that only a single Navigation fallback was created.
+               $navs_in_db = $this->get_navigations_in_database();
+
+               $this->assertCount( 1, $navs_in_db, 'Only a single Navigation menu should be present in the database.' );
+
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller
+        *
+        * @since 6.3.0 Added Navigation Fallbacks endpoint.
+        */
+       public function test_get_item_schema() {
+               $request  = new WP_REST_Request( 'OPTIONS', '/wp-block-editor/v1/navigation-fallback' );
+               $response = rest_get_server()->dispatch( $request );
+               $data     = $response->get_data();
+
+               $this->assertEquals( 200, $response->get_status(), 'Status should indicate successful request.' );
+
+               $this->assertArrayHasKey( 'schema', $data, '"schema" key should exist in response.' );
+
+               $schema = $data['schema'];
+
+               $this->assertEquals( 'object', $schema['type'], 'The schema type should match the expected type.' );
+
+               $this->assertArrayHasKey( 'id', $schema['properties'], 'Schema should have an "id" property.' );
+               $this->assertEquals( 'integer', $schema['properties']['id']['type'], 'Schema "id" property should be an integer.' );
+               $this->assertTrue( $schema['properties']['id']['readonly'], 'Schema "id" property should be readonly.' );
+       }
+
+       /**
+        * @ticket 58557
+        * @covers WP_REST_Navigation_Fallback_Controller
+        *
+        * @since 6.3.0 Added Navigation Fallbacks endpoint.
+        */
+       public function test_adds_links() {
+               $request  = new WP_REST_Request( 'GET', '/wp-block-editor/v1/navigation-fallback' );
+               $response = rest_get_server()->dispatch( $request );
+               $data     = $response->get_data();
+
+               $navigation_post_id = $data['id'];
+
+               $links = $response->get_links();
+
+               $this->assertNotEmpty( $links, 'Response should contain links.' );
+
+               $this->assertArrayHasKey( 'self', $links, 'Response should contain a "self" link.' );
+
+               $this->assertStringContainsString( 'wp/v2/navigation/' . $navigation_post_id, $links['self'][0]['href'], 'Self link should reference the correct Navigation Menu post resource url.' );
+
+               $this->assertTrue( $links['self'][0]['attributes']['embeddable'], 'Self link should be embeddable.' );
+       }
+
+       private function get_navigations_in_database() {
+               $navs_in_db = new WP_Query(
+                       array(
+                               'post_type'      => 'wp_navigation',
+                               'post_status'    => 'publish',
+                               'posts_per_page' => -1,
+                               'orderby'        => 'date',
+                               'order'          => 'DESC',
+                       )
+               );
+
+               return $navs_in_db->posts ? $navs_in_db->posts : array();
+       }
+
+       /**
+        * @doesNotPerformAssertions
+        */
+       public function test_prepare_item() {
+               // Covered by the core test.
+       }
+
+       /**
+        * @doesNotPerformAssertions
+        */
+       public function test_context_param() {
+               // Covered by the core test.
+       }
+
+       /**
+        * @doesNotPerformAssertions
+        */
+       public function test_get_items() {
+               // Covered by the core test.
+       }
+
+       /**
+        * @doesNotPerformAssertions
+        */
+       public function test_create_item() {
+               // Controller does not implement create_item().
+       }
+
+       /**
+        * @doesNotPerformAssertions
+        */
+       public function test_update_item() {
+               // Controller does not implement update_item().
+       }
+
+       /**
+        * @doesNotPerformAssertions
+        */
+       public function test_delete_item() {
+               // Controller does not implement delete_item().
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsqunitfixtureswpapigeneratedjs"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/qunit/fixtures/wp-api-generated.js</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/qunit/fixtures/wp-api-generated.js    2023-06-27 05:34:12 UTC (rev 56051)
+++ trunk/tests/qunit/fixtures/wp-api-generated.js      2023-06-27 05:52:06 UTC (rev 56052)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -11021,6 +11021,27 @@
</span><span class="cx" style="display: block; padding: 0 10px">                     }
</span><span class="cx" style="display: block; padding: 0 10px">                 ]
</span><span class="cx" style="display: block; padding: 0 10px">             }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        },
+        "/wp-block-editor/v1/navigation-fallback": {
+            "namespace": "wp-block-editor/v1",
+            "methods": [
+                "GET"
+            ],
+            "endpoints": [
+                {
+                    "methods": [
+                        "GET"
+                    ],
+                    "args": []
+                }
+            ],
+            "_links": {
+                "self": [
+                    {
+                        "href": "http://example.org/index.php?rest_route=/wp-block-editor/v1/navigation-fallback"
+                    }
+                ]
+            }
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px">     },
</span><span class="cx" style="display: block; padding: 0 10px">     "site_logo": 0,
</span></span></pre>
</div>
</div>

</body>
</html>