PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.1.0
WooCommerce v11.1.0
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Internal / ProductDownloads / ApprovedDirectories / Synchronize.php
woocommerce / src / Internal / ProductDownloads / ApprovedDirectories Last commit date
Admin 2 weeks ago ApprovedDirectoriesException.php 4 years ago Register.php 7 months ago StoredUrl.php 4 years ago Synchronize.php 2 weeks ago
Synchronize.php
340 lines
1 <?php
2
3 namespace Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories;
4
5 use ActionScheduler_Store;
6 use Exception;
7 use Automattic\WooCommerce\Internal\Utilities\URL;
8 use WC_Admin_Notices;
9 use WC_Product;
10 use WC_Queue_Interface;
11
12 /**
13 * Ensures that any downloadable files have a corresponding entry in the Approved Product
14 * Download Directories list.
15 */
16 class Synchronize {
17 /**
18 * Scheduled action hook used to facilitate scanning the product catalog for downloadable products.
19 */
20 public const SYNC_TASK = 'woocommerce_download_dir_sync';
21
22 /**
23 * The group under which synchronization tasks run (our standard 'woocommerce-db-updates' group).
24 */
25 public const SYNC_TASK_GROUP = 'woocommerce-db-updates';
26
27 /**
28 * Used to track progress throughout the sync process.
29 */
30 public const SYNC_TASK_PAGE = 'wc_product_download_dir_sync_page';
31
32 /**
33 * Used to record an estimation of progress on the current synchronization process. 0 means 0%,
34 * 100 means 100%.
35 *
36 * @param int
37 */
38 public const SYNC_TASK_PROGRESS = 'wc_product_download_dir_sync_progress';
39
40 /**
41 * Used to prevent an active synchronization task from continuing after cancellation.
42 */
43 private const SYNC_TASK_CANCELLED = 'wc_product_download_dir_sync_cancelled';
44
45 /**
46 * Number of downloadable products to be processed in each atomic sync task.
47 */
48 public const SYNC_TASK_BATCH_SIZE = 20;
49
50 /**
51 * WC Queue.
52 *
53 * @var WC_Queue_Interface
54 */
55 private $queue;
56
57 /**
58 * Register of approved directories.
59 *
60 * @var Register
61 */
62 private $register;
63
64 /**
65 * Sets up our checks and controls for downloadable asset URLs, as appropriate for
66 * the current approved download directory mode.
67 *
68 * @internal
69 * @throws Exception If the WC_Queue instance cannot be obtained.
70 *
71 * @param Register $register The active approved download directories instance in use.
72 */
73 final public function init( Register $register ) {
74 $this->queue = WC()->get_instance_of( WC_Queue_Interface::class );
75 $this->register = $register;
76
77 }
78
79 /**
80 * Performs any work needed to add hooks and otherwise integrate with the wider system.
81 */
82 final public function init_hooks() {
83 add_action( self::SYNC_TASK, array( $this, 'run' ) );
84 }
85
86 /**
87 * Initializes the Approved Download Directories feature, typically following an update or
88 * during initial installation.
89 *
90 * @param bool $synchronize Synchronize with existing product downloads. Not needed in a fresh installation.
91 * @param bool $enable_feature Enable (default) or disable the feature.
92 */
93 public function init_feature( bool $synchronize = true, bool $enable_feature = true ) {
94 try {
95 $this->add_default_directories();
96
97 if ( $synchronize ) {
98 $this->start();
99 }
100 } catch ( Exception $e ) {
101 wc_get_logger()->log( 'warning', __( 'It was not possible to synchronize download directories following the most recent update.', 'woocommerce' ) );
102 }
103
104 $this->register->set_mode(
105 $enable_feature ? Register::MODE_ENABLED : Register::MODE_DISABLED
106 );
107 }
108
109 /**
110 * By default we add the woocommerce_uploads directory (file path plus web URL) to the list
111 * of approved download directories.
112 *
113 * @throws Exception If the default directories cannot be added to the Approved List.
114 */
115 public function add_default_directories() {
116 $upload_dir = wp_get_upload_dir();
117 $this->register->add_approved_directory( $upload_dir['basedir'] . '/woocommerce_uploads' );
118 $this->register->add_approved_directory( $upload_dir['baseurl'] . '/woocommerce_uploads' );
119 }
120
121 /**
122 * Starts the synchronization process.
123 *
124 * @return bool
125 */
126 public function start(): bool {
127 if ( $this->has_scheduled_task() ) {
128 wc_get_logger()->log( 'warning', __( 'Synchronization of approved product download directories is already in progress.', 'woocommerce' ) );
129 return false;
130 }
131
132 delete_option( self::SYNC_TASK_CANCELLED );
133 update_option( self::SYNC_TASK_PAGE, 1 );
134 $this->queue->schedule_single( time(), self::SYNC_TASK, array(), self::SYNC_TASK_GROUP );
135 wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: new scan scheduled.', 'woocommerce' ) );
136 return true;
137 }
138
139 /**
140 * Indicates whether a synchronization task is pending or running.
141 */
142 private function has_scheduled_task(): bool {
143 foreach ( array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_RUNNING ) as $status ) {
144 $scheduled_tasks = $this->queue->search(
145 array(
146 'hook' => self::SYNC_TASK,
147 'status' => $status,
148 'per_page' => 1,
149 ),
150 'ids'
151 );
152
153 if ( ! empty( $scheduled_tasks ) ) {
154 return true;
155 }
156 }
157
158 return false;
159 }
160
161 /**
162 * Runs the synchronization task.
163 */
164 public function run() {
165 if ( $this->is_cancelled() ) {
166 $this->clean_up();
167 return;
168 }
169
170 $products = $this->get_next_set_of_downloadable_products();
171
172 foreach ( $products as $product ) {
173 $this->process_product( $product );
174 }
175
176 if ( $this->is_cancelled() ) {
177 $this->clean_up();
178 return;
179 }
180
181 // Detect if we have reached the end of the task.
182 if ( count( $products ) < self::SYNC_TASK_BATCH_SIZE ) {
183 wc_get_logger()->log( 'info', __( 'Approved Download Directories sync: scan is complete!', 'woocommerce' ) );
184 $this->stop();
185 } else {
186 wc_get_logger()->log(
187 'info',
188 sprintf(
189 /* translators: %1$d is the current batch in the synchronization task, %2$d is the percent complete. */
190 __( 'Approved Download Directories sync: completed batch %1$d (%2$d%% complete).', 'woocommerce' ),
191 (int) get_option( self::SYNC_TASK_PAGE, 2 ) - 1,
192 $this->get_progress()
193 )
194 );
195 $this->queue->schedule_single( time() + 1, self::SYNC_TASK, array(), self::SYNC_TASK_GROUP );
196 }
197 }
198
199 /**
200 * Completes the current synchronization task and marks its results for review.
201 *
202 * The persistent notice is also the acknowledgement state used by Site Health. Existing
203 * sites can therefore keep a pending review across requests and WooCommerce updates.
204 */
205 public function stop() {
206 if ( $this->is_cancelled() ) {
207 $this->clean_up();
208 return;
209 }
210
211 WC_Admin_Notices::add_notice( 'download_directories_sync_complete', true );
212 $this->clean_up();
213 }
214
215 /**
216 * Cancels the current synchronization task without marking its results for review.
217 *
218 * @since 11.1.0
219 */
220 public function cancel(): void {
221 update_option( self::SYNC_TASK_CANCELLED, true );
222 $this->clean_up();
223 }
224
225 /**
226 * Indicates whether the current synchronization was cancelled.
227 *
228 * @phpstan-impure
229 */
230 private function is_cancelled(): bool {
231 return (bool) get_option( self::SYNC_TASK_CANCELLED, false );
232 }
233
234 /**
235 * Cleans up synchronization state and scheduled actions.
236 */
237 private function clean_up(): void {
238 delete_option( self::SYNC_TASK_PAGE );
239 delete_option( self::SYNC_TASK_PROGRESS );
240 $this->queue->cancel( self::SYNC_TASK );
241 }
242
243 /**
244 * Queries for the next batch of downloadable products, applying logic to ensure we only fetch those that actually
245 * have downloadable files (a downloadable product can be created that does not have downloadable files and/or
246 * downloadable files can be removed from existing downloadable products).
247 *
248 * @return array
249 */
250 private function get_next_set_of_downloadable_products(): array {
251 $query_filter = function ( array $query ): array {
252 $query['meta_query'][] = array(
253 'key' => '_downloadable_files',
254 'compare' => 'EXISTS',
255 );
256
257 return $query;
258 };
259
260 $page = (int) get_option( self::SYNC_TASK_PAGE, 1 );
261 add_filter( 'woocommerce_product_data_store_cpt_get_products_query', $query_filter );
262
263 $products = wc_get_products(
264 array(
265 'limit' => self::SYNC_TASK_BATCH_SIZE,
266 'page' => $page,
267 'paginate' => true,
268 )
269 );
270
271 remove_filter( 'woocommerce_product_data_store_cpt_get_products_query', $query_filter );
272 $progress = $products->max_num_pages > 0 ? (int) ( ( $page / $products->max_num_pages ) * 100 ) : 1;
273 update_option( self::SYNC_TASK_PAGE, $page + 1 );
274 update_option( self::SYNC_TASK_PROGRESS, $progress );
275
276 return $products->products;
277 }
278
279 /**
280 * Processes an individual downloadable product, adding the parent paths for any downloadable files to the
281 * Approved Download Directories list.
282 *
283 * Any such paths will be added with the disabled flag set, because we want a site administrator to review
284 * and approve first.
285 *
286 * @param WC_Product $product The product we wish to examine for downloadable file paths.
287 */
288 private function process_product( WC_Product $product ) {
289 $downloads = $product->get_downloads();
290
291 foreach ( $downloads as $downloadable ) {
292 $parent_url = _x( 'invalid URL', 'Approved product download URLs migration', 'woocommerce' );
293
294 try {
295 $download_file = $downloadable->get_file();
296
297 /**
298 * Controls whether shortcodes should be resolved and validated using the Approved Download Directory feature.
299 *
300 * @param bool $should_validate
301 */
302 if ( apply_filters( 'woocommerce_product_downloads_approved_directory_validation_for_shortcodes', true ) && 'shortcode' === $downloadable->get_type_of_file_path() ) {
303 $download_file = do_shortcode( $download_file );
304 }
305
306 $parent_url = ( new URL( $download_file ) )->get_parent_url();
307 $this->register->add_approved_directory( $parent_url, false );
308 } catch ( Exception $e ) {
309 wc_get_logger()->log(
310 'error',
311 sprintf(
312 /* translators: %s is a URL, %d is a product ID. */
313 __( 'Product download migration: %1$s (for product %1$d) could not be added to the list of approved download directories.', 'woocommerce' ),
314 $parent_url,
315 $product->get_id()
316 )
317 );
318 }
319 }
320 }
321
322 /**
323 * Indicates if a synchronization of product download directories is in progress.
324 *
325 * @return bool
326 */
327 public function in_progress(): bool {
328 return (bool) get_option( self::SYNC_TASK_PAGE, false );
329 }
330
331 /**
332 * Returns a value between 0 and 100 representing the percentage complete of the current sync.
333 *
334 * @return int
335 */
336 public function get_progress(): int {
337 return min( 100, max( 0, (int) get_option( self::SYNC_TASK_PROGRESS, 0 ) ) );
338 }
339 }
340