<!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>[57868] trunk: Editor: Prevent font folder naive filtering causing infinite loops.</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/57868">57868</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/57868","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>peterwilsoncc</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-03-22 22:59:01 +0000 (Fri, 22 Mar 2024)</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: Prevent font folder naive filtering causing infinite loops.

This modifies the font directory API to more closely reflect the upload directory API to help account for naive filtering when uploading fonts.

This moves the protection of infinite loops to the new function `_wp_filter_font_directory()` to allow developers extending and maintaining the font library to apply the filter without the need for a closure.

These changes also ensure both the `upload_dir` and `font_dir` filter are applied consistently when both creating and deleting fonts faces. Prior to this commit the `upload_dir` filter was only fired when creating fonts faces via the REST API.

Applying the font directory filter to the `upload_dir` filter is now done by adding the `_wp_filter_font_directory` function rather than `wp_get_font_dir()`. Developers who have previously modified the font upload directory using the `font_dir` filter will NOT need to upload their code.

Extenders wishing to upload files to the font directory can do so via the code:

{{{#!php
<?php
add_filter( 'upload_dir', '_wp_filter_font_directory' );
// Your code to upload or sideload a font file.
remove_filter( 'upload_dir', '_wp_filter_font_directory' );
}}}

Introduces:

* `wp_font_dir()`: Attempt to create and retrieve the font upload directory. The equivalent to `wp_upload_dir()`.
* `_wp_filter_font_directory()`: To run on the `upload_dir` filter, this sets the default destination of the fonts directory and fires the `font_dir` filter. 

`wp_get_font_dir()` has been modified to be a lightweight getter for the font directory. It returns the location without attempting to create it. The equivalent to `wp_get_upload_dir()`.

Follow up to <a href="https://core.trac.wordpress.org/changeset/57740">[57740]</a>.

Props peterwilsoncc, mukesh27, mikachan, costdev, mmaattiiaass, swissspidy, youknowriad, dd32, grantmkin.
Fixes <a href="https://core.trac.wordpress.org/ticket/60652">#60652</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesfontsphp">trunk/src/wp-includes/fonts.php</a></li>
<li><a href="#trunksrcwpincludesrestapiendpointsclasswprestfontfacescontrollerphp">trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibraryfontLibraryHooksphp">trunk/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php</a></li>
<li><a href="#trunktestsphpunittestsfontsfontlibrarywpFontsDirphp">trunk/tests/phpunit/tests/fonts/font-library/wpFontsDir.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesfontsphp"></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/fonts.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/fonts.php   2024-03-22 22:05:25 UTC (rev 57867)
+++ trunk/src/wp-includes/fonts.php     2024-03-22 22:59:01 UTC (rev 57868)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -92,12 +92,30 @@
</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">+ * Retrieves font uploads directory information.
+ *
+ * Same as wp_font_dir() but "light weight" as it doesn't attempt to create the font uploads directory.
+ * Intended for use in themes, when only 'basedir' and 'baseurl' are needed, generally in all cases
+ * when not uploading files.
+ *
+ * @since 6.5.0
+ *
+ * @see wp_font_dir()
+ *
+ * @return array See wp_font_dir() for description.
+ */
+function wp_get_font_dir() {
+       return wp_font_dir( false );
+}
+
+/**
</ins><span class="cx" style="display: block; padding: 0 10px">  * Returns an array containing the current fonts upload directory's path and URL.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 6.5.0
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @return array $defaults {
- *     Array of information about the upload directory.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @param bool $create_dir Optional. Whether to check and create the font uploads directory. Default true.
+ * @return array {
+ *     Array of information about the font upload directory.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  *     @type string       $path    Base directory and subdirectory or full path to the fonts upload directory.
</span><span class="cx" style="display: block; padding: 0 10px">  *     @type string       $url     Base URL and subdirectory or absolute URL to the fonts upload directory.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -107,13 +125,54 @@
</span><span class="cx" style="display: block; padding: 0 10px">  *     @type string|false $error   False or error message.
</span><span class="cx" style="display: block; padding: 0 10px">  * }
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-function wp_get_font_dir() {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function wp_font_dir( $create_dir = true ) {
+       /*
+        * Allow extenders to manipulate the font directory consistently.
+        *
+        * Ensures the upload_dir filter is fired both when calling this function
+        * directly and when the upload directory is filtered in the Font Face
+        * REST API endpoint.
+        */
+       add_filter( 'upload_dir', '_wp_filter_font_directory' );
+       $font_dir = wp_upload_dir( null, $create_dir, false );
+       remove_filter( 'upload_dir', '_wp_filter_font_directory' );
+       return $font_dir;
+}
+
+/**
+ * Returns the font directory for use by the font library.
+ *
+ * This function is a callback for the {@see 'upload_dir'} filter. It is not
+ * intended to be called directly. Use wp_get_font_dir() instead.
+ *
+ * The function can be used when extending the font library to modify the upload
+ * destination for font files via the upload_dir filter. The recommended way to
+ * do this is:
+ *
+ * ```php
+ * add_filter( 'upload_dir', '_wp_filter_font_directory' );
+ * // Your code to upload or sideload a font file.
+ * remove_filter( 'upload_dir', '_wp_filter_font_directory' );
+ * ```
+ *
+ * @since 6.5.0
+ * @access private
+ *
+ * @param string $font_dir The font directory.
+ * @return string The modified font directory.
+ */
+function _wp_filter_font_directory( $font_dir ) {
+       if ( doing_filter( 'font_dir' ) ) {
+               // Avoid an infinite loop.
+               return $font_dir;
+       }
+
</ins><span class="cx" style="display: block; padding: 0 10px">         $site_path = '';
</span><span class="cx" style="display: block; padding: 0 10px">        if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $site_path = '/sites/' . get_current_blog_id();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $defaults = array(
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $font_dir = array(
</ins><span class="cx" style="display: block; padding: 0 10px">                 'path'    => path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path,
</span><span class="cx" style="display: block; padding: 0 10px">                'url'     => untrailingslashit( content_url( 'fonts' ) ) . $site_path,
</span><span class="cx" style="display: block; padding: 0 10px">                'subdir'  => '',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -129,9 +188,18 @@
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 6.5.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         * @param array $defaults The original fonts directory data.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  * @param array $font_dir {
+        *     Array of information about the font upload directory.
+        *
+        *     @type string       $path    Base directory and subdirectory or full path to the fonts upload directory.
+        *     @type string       $url     Base URL and subdirectory or absolute URL to the fonts upload directory.
+        *     @type string       $subdir  Subdirectory
+        *     @type string       $basedir Path without subdir.
+        *     @type string       $baseurl URL path without subdir.
+        *     @type string|false $error   False or error message.
+        * }
</ins><span class="cx" style="display: block; padding: 0 10px">          */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        return apply_filters( 'font_dir', $defaults );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ return apply_filters( 'font_dir', $font_dir );
</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="trunksrcwpincludesrestapiendpointsclasswprestfontfacescontrollerphp"></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/endpoints/class-wp-rest-font-faces-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-font-faces-controller.php  2024-03-22 22:05:25 UTC (rev 57867)
+++ trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-font-faces-controller.php    2024-03-22 22:59:01 UTC (rev 57868)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -856,22 +856,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        protected function handle_font_file_upload( $file ) {
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Filter the upload directory to return the fonts directory.
+               add_filter( 'upload_dir', '_wp_filter_font_directory' );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                /*
-                * Set the upload directory to the fonts directory.
-                *
-                * wp_get_font_dir() contains the 'font_dir' hook, whose callbacks are
-                * likely to call wp_get_upload_dir().
-                *
-                * To avoid an infinite loop, don't hook wp_get_font_dir() to 'upload_dir'.
-                * Instead, just pass its return value to the 'upload_dir' callback.
-                */
-               $font_dir       = wp_get_font_dir();
-               $set_upload_dir = function () use ( $font_dir ) {
-                       return $font_dir;
-               };
-               add_filter( 'upload_dir', $set_upload_dir );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 $overrides = array(
</span><span class="cx" style="display: block; padding: 0 10px">                        'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ),
</span><span class="cx" style="display: block; padding: 0 10px">                        // Not testing a form submission.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -887,7 +874,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $uploaded_file = wp_handle_upload( $file, $overrides );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                remove_filter( 'upload_dir', $set_upload_dir );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         remove_filter( 'upload_dir', '_wp_filter_font_directory' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return $uploaded_file;
</span></span></pre></div>
<a id="trunktestsphpunittestsfontsfontlibraryfontLibraryHooksphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php 2024-03-22 22:05:25 UTC (rev 57867)
+++ trunk/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php   2024-03-22 22:59:01 UTC (rev 57868)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -73,13 +73,13 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $font_file_path = DIR_TESTDATA . '/fonts/' . $font_filename;
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                add_filter( 'upload_dir', 'wp_get_font_dir' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         add_filter( 'upload_dir', '_wp_filter_font_directory' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 $font_file = wp_upload_bits(
</span><span class="cx" style="display: block; padding: 0 10px">                        $font_filename,
</span><span class="cx" style="display: block; padding: 0 10px">                        null,
</span><span class="cx" style="display: block; padding: 0 10px">                        file_get_contents( $font_file_path )
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                remove_filter( 'upload_dir', 'wp_get_font_dir' );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         remove_filter( 'upload_dir', '_wp_filter_font_directory' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                return $font_file;
</span></span></pre></div>
<a id="trunktestsphpunittestsfontsfontlibrarywpFontsDirphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/fonts/font-library/wpFontsDir.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/fonts/font-library/wpFontsDir.php       2024-03-22 22:05:25 UTC (rev 57867)
+++ trunk/tests/phpunit/tests/fonts/font-library/wpFontsDir.php 2024-03-22 22:59:01 UTC (rev 57868)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -69,4 +69,44 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertSame( static::$dir_defaults, $font_dir, 'The wp_get_font_dir() method should return the default values.' );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * @ticket 60652
+        */
+       public function test_fonts_dir_filters_do_not_trigger_infinite_loop() {
+               /*
+                * Naive filtering of uploads directory to return font directory.
+                *
+                * This emulates the approach a plugin developer may take to
+                * add the filter when extending the font library functionality.
+                */
+               add_filter( 'upload_dir', '_wp_filter_font_directory' );
+
+               add_filter(
+                       'upload_dir',
+                       function ( $upload_dir ) {
+                               static $count = 0;
+                               ++$count;
+                               // The filter may be applied a couple of times, at five iterations assume an infinite loop.
+                               if ( $count >= 5 ) {
+                                       $this->fail( 'Filtering the uploads directory triggered an infinite loop.' );
+                               }
+                               return $upload_dir;
+                       },
+                       5
+               );
+
+               /*
+                * Filter the font directory to return the uploads directory.
+                *
+                * This emulates moving font files back to the uploads directory due
+                * to file system structure.
+                */
+               add_filter( 'font_dir', 'wp_get_upload_dir' );
+
+               wp_get_upload_dir();
+
+               // This will never be hit if an infinite loop is triggered.
+               $this->assertTrue( true );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>

</body>
</html>