Commit 471cc0a1599 for woocommerce

commit 471cc0a15999e821002400d5a4a32138b7167846
Author: Daniel Mallory <daniel.mallory@automattic.com>
Date:   Mon Nov 9 17:03:16 2026 +0000

    Fix an infinite loop with Real Media Library on attachment GUIDs (#70782)

    * fix(downloads): Fix an infinite loop with plugins that read GUIDs in meta filters

    Downloadable file attachment hooks, including get_the_guid, read the
    _wp_attached_file meta. Real Media Library filters that meta and reads
    the GUID to detect shortcuts, so each filter triggered the other until
    PHP ran out of memory. Atomic sites on 11.3 RC-1 with Real Media
    Library hit 512MB memory fatals on pages with media.

    Skip the downloadable file check while one is already running. The
    nested GUID read then returns the stored GUID, so shortcut detection
    still works and shortcuts to downloadable files stay hidden.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * test(downloads): Remove an assertion an existing test already covers

    The downloadable file GUID check repeats test_comments_and_guid. The
    shortcut assertion already shows that protection holds with the
    Real Media Library-style filter active.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * refactor(downloads): Rename the downloadable file check flag to in_progress

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    * refactor(downloads): Apply review feedback on the recursion fix

    Drop the regression test: it only guarded against one way an extension
    can hook the same filters, and a regression killed the PHPUnit process
    instead of failing. Clarify the in_progress docblock, and use a review
    comment for the changelog since the recursion never shipped in a
    release.

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/fix-downloadable-file-attachments-recursion b/plugins/woocommerce/changelog/fix-downloadable-file-attachments-recursion
new file mode 100644
index 00000000000..fa96c5d846d
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-downloadable-file-attachments-recursion
@@ -0,0 +1,3 @@
+Significance: patch
+Type: fix
+Comment: Fix unreleased recursion between downloadable file hiding and Real Media Library (#70198).
diff --git a/plugins/woocommerce/src/Internal/ProductDownloads/DownloadableFileAttachments.php b/plugins/woocommerce/src/Internal/ProductDownloads/DownloadableFileAttachments.php
index dd23cb2257d..5232eeeaf42 100644
--- a/plugins/woocommerce/src/Internal/ProductDownloads/DownloadableFileAttachments.php
+++ b/plugins/woocommerce/src/Internal/ProductDownloads/DownloadableFileAttachments.php
@@ -37,6 +37,14 @@ class DownloadableFileAttachments implements RegisterHooksInterface {
 	 */
 	private const DIRECTORY = 'woocommerce_uploads/';

+	/**
+	 * True while is_downloadable_file() reads an attachment's file path. Checks that the read triggers again return
+	 * false instead of recursing, so every attachment counts as visible during that read.
+	 *
+	 * @var bool
+	 */
+	private bool $in_progress = false;
+
 	/**
 	 * Register hooks and filters.
 	 */
@@ -257,12 +265,18 @@ class DownloadableFileAttachments implements RegisterHooksInterface {
 	 */
 	private function is_downloadable_file( int $attachment_id ): bool {
 		// Checking the post type first skips a meta lookup for the posts that comment and GUID hooks run on.
-		if ( $attachment_id <= 0 || 'attachment' !== get_post_type( $attachment_id ) ) {
+		if ( $this->in_progress || $attachment_id <= 0 || 'attachment' !== get_post_type( $attachment_id ) ) {
 			return false;
 		}

-		// Case-insensitive to match the SQL LIKE used for REST API collections.
-		return 0 === stripos( (string) get_post_meta( $attachment_id, '_wp_attached_file', true ), self::DIRECTORY );
+		// Plugins can filter the meta read below with a function that fires these hooks again, such as get_the_guid().
+		$this->in_progress = true;
+		try {
+			// Case-insensitive to match the SQL LIKE used for REST API collections.
+			return 0 === stripos( (string) get_post_meta( $attachment_id, '_wp_attached_file', true ), self::DIRECTORY );
+		} finally {
+			$this->in_progress = false;
+		}
 	}

 	/**