Commit a68082f24d9 for woocommerce
commit a68082f24d98b9d73d731dd45e4d9aacf1bbbe73
Author: Darren Ethier <darren@roughsmootheng.in>
Date: Fri Aug 7 16:20:13 2026 -0400
Restore approved directory synchronization review prompt (#67491)
diff --git a/plugins/woocommerce/changelog/fix-67489-approved-directory-review b/plugins/woocommerce/changelog/fix-67489-approved-directory-review
new file mode 100644
index 00000000000..6e418fc98f7
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-67489-approved-directory-review
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Restore and harden the Site Health prompt after approved download directory synchronization.
diff --git a/plugins/woocommerce/src/Internal/Admin/SiteHealth.php b/plugins/woocommerce/src/Internal/Admin/SiteHealth.php
index 3e3b74e494d..072eb6ff9ef 100644
--- a/plugins/woocommerce/src/Internal/Admin/SiteHealth.php
+++ b/plugins/woocommerce/src/Internal/Admin/SiteHealth.php
@@ -13,6 +13,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
+use WC_Admin_Notices;
use WC_Admin_Status;
use WC_Helper_Updater;
use WC_Install;
@@ -362,6 +363,38 @@ class SiteHealth {
),
),
'woocommerce_approved_download_directories_sync' => array(
+ 'label' => __( 'WooCommerce approved download directories', 'woocommerce' ),
+ 'badge' => 'security',
+ 'check' => fn() => ! WC_Admin_Notices::has_notice( 'download_directories_sync_complete' ),
+ 'good' => array(
+ 'label' => __( 'WooCommerce approved download directories do not require review', 'woocommerce' ),
+ 'description' => __( 'There is no completed approved download directory synchronization waiting for review.', 'woocommerce' ),
+ ),
+ 'fail' => array(
+ 'label' => __( 'WooCommerce approved download directories need review', 'woocommerce' ),
+ 'description' => __( 'Approved product download directory synchronization has completed. Review the list to confirm downloadable product files remain protected.', 'woocommerce' ),
+ 'actions' => array(
+ array(
+ 'url' => admin_url( 'admin.php?page=wc-settings&tab=products§ion=download_urls' ),
+ 'label' => __( 'Review approved directories', 'woocommerce' ),
+ ),
+ array(
+ 'url' => wp_nonce_url(
+ add_query_arg( 'wc-hide-notice', 'download_directories_sync_complete', admin_url( 'site-health.php' ) ),
+ 'woocommerce_hide_notices_nonce',
+ '_wc_notice_nonce'
+ ),
+ 'label' => __( 'Mark as reviewed', 'woocommerce' ),
+ ),
+ array(
+ 'url' => 'https://woocommerce.com/document/approved-download-directories',
+ 'label' => __( 'Learn more about approved directories', 'woocommerce' ),
+ 'new_tab' => true,
+ ),
+ ),
+ ),
+ ),
+ 'woocommerce_approved_download_directories_enforcement' => array(
'label' => __( 'WooCommerce approved download directories', 'woocommerce' ),
'badge' => 'security',
'check' => fn() => Download_Directories::MODE_ENABLED === wc_get_container()->get( Download_Directories::class )->get_mode(),
diff --git a/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Admin/SyncUI.php b/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Admin/SyncUI.php
index 261f13fbe90..23dc46e466c 100644
--- a/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Admin/SyncUI.php
+++ b/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Admin/SyncUI.php
@@ -108,7 +108,7 @@ class SyncUI {
public function cancel_sync() {
$this->security_check();
wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: scan has been cancelled.', 'woocommerce' ) );
- wc_get_container()->get( Synchronize::class )->stop();
+ wc_get_container()->get( Synchronize::class )->cancel();
}
/**
diff --git a/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php b/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php
index 5d214b54335..e80e11be5bd 100644
--- a/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php
+++ b/plugins/woocommerce/src/Internal/ProductDownloads/ApprovedDirectories/Synchronize.php
@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;
+use ActionScheduler_Store;
use Exception;
use Automattic\WooCommerce\Internal\Utilities\URL;
use WC_Admin_Notices;
@@ -36,6 +37,11 @@ class Synchronize {
*/
public const SYNC_TASK_PROGRESS = 'wc_product_download_dir_sync_progress';
+ /**
+ * Used to prevent an active synchronization task from continuing after cancellation.
+ */
+ private const SYNC_TASK_CANCELLED = 'wc_product_download_dir_sync_cancelled';
+
/**
* Number of downloadable products to be processed in each atomic sync task.
*/
@@ -118,27 +124,60 @@ class Synchronize {
* @return bool
*/
public function start(): bool {
- if ( null !== $this->queue->get_next( self::SYNC_TASK ) ) {
+ if ( $this->has_scheduled_task() ) {
wc_get_logger()->log( 'warning', __( 'Synchronization of approved product download directories is already in progress.', 'woocommerce' ) );
return false;
}
+ delete_option( self::SYNC_TASK_CANCELLED );
update_option( self::SYNC_TASK_PAGE, 1 );
$this->queue->schedule_single( time(), self::SYNC_TASK, array(), self::SYNC_TASK_GROUP );
wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: new scan scheduled.', 'woocommerce' ) );
return true;
}
+ /**
+ * Indicates whether a synchronization task is pending or running.
+ */
+ private function has_scheduled_task(): bool {
+ foreach ( array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_RUNNING ) as $status ) {
+ $scheduled_tasks = $this->queue->search(
+ array(
+ 'hook' => self::SYNC_TASK,
+ 'status' => $status,
+ 'per_page' => 1,
+ ),
+ 'ids'
+ );
+
+ if ( ! empty( $scheduled_tasks ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/**
* Runs the synchronization task.
*/
public function run() {
+ if ( $this->is_cancelled() ) {
+ $this->clean_up();
+ return;
+ }
+
$products = $this->get_next_set_of_downloadable_products();
foreach ( $products as $product ) {
$this->process_product( $product );
}
+ if ( $this->is_cancelled() ) {
+ $this->clean_up();
+ return;
+ }
+
// Detect if we have reached the end of the task.
if ( count( $products ) < self::SYNC_TASK_BATCH_SIZE ) {
wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: scan is complete!', 'woocommerce' ) );
@@ -158,10 +197,44 @@ class Synchronize {
}
/**
- * Stops/cancels the current synchronization task.
+ * Completes the current synchronization task and marks its results for review.
+ *
+ * The persistent notice is also the acknowledgement state used by Site Health. Existing
+ * sites can therefore keep a pending review across requests and WooCommerce updates.
*/
public function stop() {
+ if ( $this->is_cancelled() ) {
+ $this->clean_up();
+ return;
+ }
+
WC_Admin_Notices::add_notice( 'download_directories_sync_complete', true );
+ $this->clean_up();
+ }
+
+ /**
+ * Cancels the current synchronization task without marking its results for review.
+ *
+ * @since 11.1.0
+ */
+ public function cancel(): void {
+ update_option( self::SYNC_TASK_CANCELLED, true );
+ $this->clean_up();
+ }
+
+ /**
+ * Indicates whether the current synchronization was cancelled.
+ *
+ * @phpstan-impure
+ */
+ private function is_cancelled(): bool {
+ return (bool) get_option( self::SYNC_TASK_CANCELLED, false );
+ }
+
+ /**
+ * Cleans up synchronization state and scheduled actions.
+ */
+ private function clean_up(): void {
delete_option( self::SYNC_TASK_PAGE );
delete_option( self::SYNC_TASK_PROGRESS );
$this->queue->cancel( self::SYNC_TASK );
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/SiteHealthTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/SiteHealthTest.php
index 7f94ae2e01d..9e50d59fa7d 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/SiteHealthTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/SiteHealthTest.php
@@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Tests\Internal\Admin;
use Automattic\WooCommerce\Internal\Admin\SiteHealth;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
+use WC_Admin_Notices;
use WC_Unit_Test_Case;
use WP_Error;
@@ -27,6 +28,7 @@ class SiteHealthTest extends WC_Unit_Test_Case {
$this->sut = new SiteHealth();
delete_transient( '_woocommerce_upload_directory_status' );
delete_option( 'wc_downloads_approved_directories_mode' );
+ WC_Admin_Notices::remove_notice( 'download_directories_sync_complete', true );
}
/**
@@ -35,6 +37,7 @@ class SiteHealthTest extends WC_Unit_Test_Case {
public function tearDown(): void {
delete_transient( '_woocommerce_upload_directory_status' );
delete_option( 'wc_downloads_approved_directories_mode' );
+ WC_Admin_Notices::remove_notice( 'download_directories_sync_complete', true );
parent::tearDown();
}
@@ -44,7 +47,7 @@ class SiteHealthTest extends WC_Unit_Test_Case {
public function test_approved_download_directories_check_is_good_when_rules_are_enforced(): void {
wc_get_container()->get( Download_Directories::class )->set_mode( Download_Directories::MODE_ENABLED );
- $result = $this->sut->run_test( 'woocommerce_approved_download_directories_sync' );
+ $result = $this->sut->run_test( 'woocommerce_approved_download_directories_enforcement' );
$this->assertSame( 'good', $result['status'], 'Enabled approved directory rules should pass the Site Health check.' );
$this->assertSame( 'WooCommerce approved download directory rules are enforced', $result['label'], 'The result should report that approved directory rules are enforced.' );
@@ -54,7 +57,7 @@ class SiteHealthTest extends WC_Unit_Test_Case {
* @testdox Approved download directories check recommends enabling rules when they are not enforced.
*/
public function test_approved_download_directories_check_recommends_enabling_rules_when_not_enforced(): void {
- $result = $this->sut->run_test( 'woocommerce_approved_download_directories_sync' );
+ $result = $this->sut->run_test( 'woocommerce_approved_download_directories_enforcement' );
$this->assertSame( 'recommended', $result['status'], 'Disabled approved directory rules should require attention in Site Health.' );
$this->assertSame( 'WooCommerce approved download directory rules are not enforced', $result['label'], 'The result should report that approved directory rules are not enforced.' );
@@ -62,6 +65,33 @@ class SiteHealthTest extends WC_Unit_Test_Case {
$this->assertStringContainsString( 'section=download_urls', $result['actions'], 'The result should link to the approved directory settings.' );
}
+ /**
+ * @testdox Approved download directories check recommends review after synchronization when rules are enforced.
+ */
+ public function test_approved_download_directories_check_recommends_review_after_synchronization(): void {
+ wc_get_container()->get( Download_Directories::class )->set_mode( Download_Directories::MODE_ENABLED );
+ WC_Admin_Notices::add_notice( 'download_directories_sync_complete', true );
+
+ $result = $this->sut->run_test( 'woocommerce_approved_download_directories_sync' );
+
+ $this->assertSame( 'recommended', $result['status'], 'A completed synchronization should require review even when approved directory rules are enforced.' );
+ $this->assertSame( 'WooCommerce approved download directories need review', $result['label'], 'The result should prompt the merchant to review synchronized directories.' );
+ $this->assertSame( '<p>Approved product download directory synchronization has completed. Review the list to confirm downloadable product files remain protected.</p>', $result['description'], 'The result should describe synchronization completion without claiming that new directories were found.' );
+ $this->assertStringContainsString( 'section=download_urls', $result['actions'], 'The result should link to the approved directory settings.' );
+ $this->assertStringContainsString( 'wc-hide-notice=download_directories_sync_complete', $result['actions'], 'The result should let the merchant mark the synchronization as reviewed.' );
+ }
+
+ /**
+ * @testdox Approved download directories check is good when no synchronization is waiting for review.
+ */
+ public function test_approved_download_directories_check_is_good_without_completed_synchronization(): void {
+ $result = $this->sut->run_test( 'woocommerce_approved_download_directories_sync' );
+
+ $this->assertSame( 'good', $result['status'], 'No completed synchronization should require review.' );
+ $this->assertSame( 'WooCommerce approved download directories do not require review', $result['label'], 'The result should confirm that synchronized directories do not require review.' );
+ $this->assertSame( '<p>There is no completed approved download directory synchronization waiting for review.</p>', $result['description'], 'The result should confirm that no synchronization review is pending.' );
+ }
+
/**
* @testdox Upload directory protection check is inconclusive when the HTTP request fails.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductDownloads/ApprovedDirectories/SynchronizeTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductDownloads/ApprovedDirectories/SynchronizeTest.php
index 8ac2d6d0350..bd020f97522 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductDownloads/ApprovedDirectories/SynchronizeTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductDownloads/ApprovedDirectories/SynchronizeTest.php
@@ -3,8 +3,10 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\ProductDownloads\ApprovedDirectories;
+use ActionScheduler_Store;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize;
use Automattic\WooCommerce\Proxies\LegacyProxy;
+use WC_Admin_Notices;
use WC_Queue_Interface;
use WC_Unit_Test_Case;
@@ -12,6 +14,11 @@ use WC_Unit_Test_Case;
* Tests for the Product Downloads Allowed Directories synchronization utility.
*/
class SynchronizeTest extends WC_Unit_Test_Case {
+ /**
+ * Option used to mark a synchronization as cancelled.
+ */
+ private const SYNC_TASK_CANCELLED = 'wc_product_download_dir_sync_cancelled';
+
/**
* @var Synchronize
*/
@@ -23,6 +30,18 @@ class SynchronizeTest extends WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
$this->sut = wc_get_container()->get( Synchronize::class );
+ delete_option( self::SYNC_TASK_CANCELLED );
+ WC_Admin_Notices::remove_notice( 'download_directories_sync_complete', true );
+ }
+
+ /**
+ * Clean up after each test.
+ */
+ public function tearDown(): void {
+ $this->sut->cancel();
+ delete_option( self::SYNC_TASK_CANCELLED );
+ WC_Admin_Notices::remove_notice( 'download_directories_sync_complete', true );
+ parent::tearDown();
}
/**
@@ -34,9 +53,9 @@ class SynchronizeTest extends WC_Unit_Test_Case {
}
/**
- * @testdox Ensure basic controls to start and stop synchronization behave as expected.
+ * @testdox Ensure basic controls to start and cancel synchronization behave as expected.
*/
- public function test_basic_synchronization_controls() {
+ public function test_basic_synchronization_controls(): void {
$this->sut->start();
$this->assertTrue(
$this->sut->in_progress(),
@@ -48,22 +67,61 @@ class SynchronizeTest extends WC_Unit_Test_Case {
'If a download directory synchronization process is already in progress, additional concurrent sync processes cannot be created.'
);
+ $this->sut->cancel();
$this->assertFalse(
- $this->sut->start(),
- 'Synchronization process can be cancelled before it completes.'
+ $this->sut->in_progress(),
+ 'Synchronization can be cancelled before it completes.'
);
-
- $this->sut->stop();
$this->assertNull(
wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Queue_Interface::class )->get_next( Synchronize::SYNC_TASK ),
'Once synchronization has been cancelled, any related scheduled actions will also have been cleaned up.'
);
+ $this->assertFalse(
+ WC_Admin_Notices::has_notice( 'download_directories_sync_complete' ),
+ 'Cancelling synchronization should not create a completed synchronization review notice.'
+ );
+
+ $this->assertTrue( $this->sut->start(), 'A new synchronization should be able to start after cancellation.' );
+ $this->sut->run();
+ $this->assertTrue(
+ WC_Admin_Notices::has_notice( 'download_directories_sync_complete' ),
+ 'A new synchronization should complete normally after a previous synchronization was cancelled.'
+ );
+ }
+
+ /**
+ * @testdox Scheduled task detection uses scalar queue statuses supported by the queue interface.
+ */
+ public function test_scheduled_task_detection_uses_scalar_statuses(): void {
+ $searched_statuses = array();
+ $queue = $this->createMock( WC_Queue_Interface::class );
+ $queue->method( 'search' )
+ ->willReturnCallback(
+ function ( array $args, string $return_format ) use ( &$searched_statuses ): array {
+ $searched_statuses[] = $args['status'];
+ $this->assertSame( 'ids', $return_format );
+
+ return ActionScheduler_Store::STATUS_RUNNING === $args['status'] ? array( 123 ) : array();
+ }
+ );
+
+ $sut = new Synchronize();
+ $queue_property = new \ReflectionProperty( $sut, 'queue' );
+ $queue_property->setAccessible( true );
+ $queue_property->setValue( $sut, $queue );
+
+ $this->assertFalse( $sut->start(), 'A running synchronization task should prevent another synchronization from starting.' );
+ $this->assertSame(
+ array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_RUNNING ),
+ $searched_statuses,
+ 'Scheduled task detection should query each status as a scalar value.'
+ );
}
/**
* @testdox Verify expected logging and clean-up take place during and following synchronization of download directories.
*/
- public function test_sync_process() {
+ public function test_sync_process(): void {
$logged_messages = array();
$log_watcher = function ( string $logged_message ) use ( &$logged_messages ) {
@@ -87,5 +145,69 @@ class SynchronizeTest extends WC_Unit_Test_Case {
$logged_messages,
'We expect that completion of the synchronization process will have been recorded in the log.'
);
+
+ $this->assertTrue(
+ WC_Admin_Notices::has_notice( 'download_directories_sync_complete' ),
+ 'Completed synchronization should create a persistent review notice.'
+ );
+ }
+
+ /**
+ * @testdox Cancelling synchronization preserves a review notice that was already pending.
+ */
+ public function test_cancellation_preserves_pending_review_notice(): void {
+ WC_Admin_Notices::add_notice( 'download_directories_sync_complete', true );
+
+ $this->sut->start();
+ $this->sut->cancel();
+
+ $this->assertTrue(
+ WC_Admin_Notices::has_notice( 'download_directories_sync_complete' ),
+ 'Cancelling a later synchronization should not clear an existing pending review state.'
+ );
+ }
+
+ /**
+ * @testdox Cancelling an active batch does not mark synchronization as complete.
+ */
+ public function test_cancellation_during_active_batch_does_not_mark_sync_complete(): void {
+ $cancelled = false;
+ $restart_result = null;
+ $cancel_during_batch = function () use ( &$cancelled, &$restart_result ): void {
+ if ( ! $cancelled ) {
+ $cancelled = true;
+ $this->sut->cancel();
+ $restart_result = $this->sut->start();
+ }
+ };
+
+ $this->sut->start();
+ $store = ActionScheduler_Store::instance();
+ $claim = $store->stake_claim( 1, null, array( Synchronize::SYNC_TASK ) );
+ $claimed_actions = $claim->get_actions();
+ $this->assertCount( 1, $claimed_actions, 'The synchronization action should be marked as running for the test.' );
+ $store->log_execution( $claimed_actions[0] );
+ add_action( 'update_option_' . Synchronize::SYNC_TASK_PAGE, $cancel_during_batch, 10, 0 );
+
+ try {
+ $this->sut->run();
+ } finally {
+ remove_action( 'update_option_' . Synchronize::SYNC_TASK_PAGE, $cancel_during_batch, 10 );
+ $store->mark_complete( $claimed_actions[0] );
+ $store->release_claim( $claim );
+ }
+
+ $this->assertTrue( $cancelled, 'The test should cancel synchronization while a batch is active.' );
+ $this->assertFalse( $restart_result, 'A new synchronization should not start before the cancelled active batch returns.' );
+ $this->assertFalse( $this->sut->in_progress(), 'An active batch should clean up synchronization state after cancellation.' );
+ $this->assertFalse(
+ WC_Admin_Notices::has_notice( 'download_directories_sync_complete' ),
+ 'An active batch should not create a completion notice after cancellation.'
+ );
+ $this->assertNull(
+ wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Queue_Interface::class )->get_next( Synchronize::SYNC_TASK ),
+ 'An active batch should not schedule more synchronization work after cancellation.'
+ );
+ $this->assertTrue( $this->sut->start(), 'A new synchronization should start after the cancelled active batch has returned.' );
}
}