PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.1
WooCommerce v10.9.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 / ProductAttributesLookup / DataRegenerator.php
woocommerce / src / Internal / ProductAttributesLookup Last commit date
CLIRunner.php 1 year ago DataRegenerator.php 1 month ago Filterer.php 1 month ago LookupDataStore.php 4 months ago TaxQuery.php 1 month ago
DataRegenerator.php
589 lines
1 <?php
2 /**
3 * DataRegenerator class file.
4 */
5
6 namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
7
8 use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
9
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * This class handles the (re)generation of the product attributes lookup table.
14 * It schedules the regeneration in small product batches by itself, so it can be used outside the
15 * regular WooCommerce data regenerations mechanism.
16 *
17 * After the regeneration is completed a wp_wc_product_attributes_lookup table will exist with entries for
18 * all the products that existed when initiate_regeneration was invoked; entries for products created after that
19 * are supposed to be created/updated by the appropriate data store classes (or by the code that uses
20 * the data store classes) whenever a product is created/updated.
21 *
22 * Additionally, after the regeneration is completed a 'woocommerce_attribute_lookup_enabled' option
23 * with a value of 'yes' will have been created, thus effectively enabling the table usage
24 * (with an exception: if the regeneration was manually aborted via deleting the
25 * 'woocommerce_attribute_lookup_regeneration_in_progress' option) the option will be set to 'no'.
26 *
27 * This class also adds two entries to the Status - Tools menu: one for manually regenerating the table contents,
28 * and another one for enabling or disabling the actual lookup table usage.
29 */
30 class DataRegenerator {
31
32 public const PRODUCTS_PER_GENERATION_STEP = 100;
33
34 /**
35 * The data store to use.
36 *
37 * @var LookupDataStore
38 */
39 private $data_store;
40
41 /**
42 * The lookup table name.
43 *
44 * @var string
45 */
46 private $lookup_table_name;
47
48 /**
49 * Flag indicating if the last regeneration step failed.
50 *
51 * @var bool
52 */
53 private $last_regeneration_step_failed;
54
55 /**
56 * DataRegenerator constructor.
57 */
58 public function __construct() {
59 global $wpdb;
60
61 $this->lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup';
62
63 add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 1, 999 );
64 add_action( 'woocommerce_run_product_attribute_lookup_regeneration_callback', array( $this, 'run_regeneration_step_callback' ) );
65 add_action( 'woocommerce_installed', array( $this, 'run_woocommerce_installed_callback' ) );
66 }
67
68 /**
69 * Class initialization, invoked by the DI container.
70 *
71 * @internal
72 * @param LookupDataStore $data_store The data store to use.
73 */
74 final public function init( LookupDataStore $data_store ) {
75 $this->data_store = $data_store;
76 }
77
78 /**
79 * Check if the last regeneration step failed.
80 *
81 * @return bool True if the last regeneration step failed.
82 */
83 public function get_last_regeneration_step_failed() {
84 return $this->last_regeneration_step_failed;
85 }
86
87 /**
88 * Initialize the regeneration procedure:
89 * deletes the lookup table and related options if they exist,
90 * then it creates the table and runs the first step of the regeneration process.
91 *
92 * If $in_background is true, regeneration will continue in the background using scheduled actions.
93 * If $in_background is false, do_regeneration_step and finalize_regeneration must be invoked explicitly.
94 *
95 * This method is intended to be used as a callback for a db update in wc-update-functions
96 * and in the CLI commands, regeneration triggered from the tools page will use
97 * initiate_regeneration_from_tools_page instead.
98 *
99 * @param bool $in_background True if regeneration will continue in the background using scheduled actions.
100 * @return int Highest product id that will be processed.
101 */
102 public function initiate_regeneration( bool $in_background = true ): int {
103 $this->check_can_do_lookup_table_regeneration();
104
105 $this->enable_or_disable_lookup_table_usage( false );
106
107 $this->delete_all_attributes_lookup_data( true );
108 $last_product_id = $this->initialize_table_and_data();
109 if ( $last_product_id > 0 ) {
110 $this->data_store->set_regeneration_in_progress_flag();
111 if ( $in_background ) {
112 $this->enqueue_regeneration_step_run();
113 }
114 } else {
115 $this->finalize_regeneration( true );
116 }
117 return $last_product_id;
118 }
119
120 /**
121 * Delete all the existing data related to the lookup table, optionally including the table itself.
122 *
123 * @param bool $truncate_table True to truncate the lookup table too.
124 */
125 private function delete_all_attributes_lookup_data( bool $truncate_table ) {
126 global $wpdb;
127
128 delete_option( 'woocommerce_attribute_lookup_enabled' );
129 delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' );
130 delete_option( 'woocommerce_attribute_lookup_processed_count' );
131 $this->data_store->unset_regeneration_in_progress_flag();
132 $this->data_store->unset_regeneration_aborted_flag();
133
134 if ( $truncate_table && $this->data_store->check_lookup_table_exists() ) {
135 $this->truncate_lookup_table();
136 }
137 }
138
139 /**
140 * Delete all the data from the lookup table.
141 */
142 public function truncate_lookup_table() {
143 global $wpdb;
144
145 $wpdb->query( "TRUNCATE TABLE {$this->lookup_table_name}" ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
146 }
147
148 /**
149 * Create the lookup table and initialize the options that will be temporarily used
150 * while the regeneration is in progress.
151 *
152 * @return int Id of the last product id that will be processed.
153 */
154 private function initialize_table_and_data(): int {
155 $database_util = wc_get_container()->get( DatabaseUtil::class );
156 $database_util->dbdelta( $this->get_table_creation_sql() );
157
158 $last_existing_product_id = $this->get_last_existing_product_id();
159 if ( ! $last_existing_product_id ) {
160 // No products exist, nothing to (re)generate.
161 return 0;
162 }
163
164 update_option( 'woocommerce_attribute_lookup_last_product_id_to_process', $last_existing_product_id );
165 update_option( 'woocommerce_attribute_lookup_processed_count', 0 );
166
167 return $last_existing_product_id;
168 }
169
170 /**
171 * Get the highest existing product id.
172 *
173 * @return int|null Highest existing product id, or null if no products exist at all.
174 */
175 private function get_last_existing_product_id(): ?int {
176 $last_existing_product_id_array =
177 WC()->call_function(
178 'wc_get_products',
179 array(
180 'return' => 'ids',
181 'limit' => 1,
182 'orderby' => array(
183 'ID' => 'DESC',
184 ),
185 )
186 );
187
188 return empty( $last_existing_product_id_array ) ? null : current( $last_existing_product_id_array );
189 }
190
191 /**
192 * Action scheduler callback, performs one regeneration step and then
193 * schedules the next step if necessary.
194 *
195 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
196 */
197 public function run_regeneration_step_callback() {
198 if ( ! $this->data_store->regeneration_is_in_progress() ) {
199 // No regeneration in progress at this point means that the regeneration process
200 // was manually aborted via deleting the 'woocommerce_attribute_lookup_regeneration_in_progress' option.
201 $this->data_store->set_regeneration_aborted_flag();
202 $this->finalize_regeneration( false );
203 return;
204 }
205
206 $result = $this->do_regeneration_step( null, $this->data_store->optimized_data_access_is_enabled() );
207 if ( $result ) {
208 $this->enqueue_regeneration_step_run();
209 } else {
210 $this->finalize_regeneration( true );
211 }
212 }
213
214 /**
215 * Enqueue one regeneration step in action scheduler.
216 */
217 private function enqueue_regeneration_step_run() {
218 $queue = WC()->get_instance_of( \WC_Queue::class );
219 $queue->schedule_single(
220 WC()->call_function( 'time' ) + 1,
221 'woocommerce_run_product_attribute_lookup_regeneration_callback',
222 array(),
223 'woocommerce-db-updates'
224 );
225 }
226
227 /**
228 * Perform one regeneration step: grabs a chunk of products and creates
229 * the appropriate entries for them in the lookup table.
230 *
231 * @param int|null $step_size How many products to process, by default PRODUCTS_PER_GENERATION_STEP will be used.
232 * @param bool $use_optimized_db_access Use direct database access for data retrieval if possible.
233 * @return bool True if more steps need to be run, false otherwise.
234 */
235 public function do_regeneration_step( ?int $step_size = null, bool $use_optimized_db_access = false ) {
236 /**
237 * Filter to alter the count of products that will be processed in each step of the product attributes lookup table regeneration process.
238 *
239 * @since 6.3
240 * @param int $count Default processing step size.
241 */
242 $products_per_generation_step = apply_filters( 'woocommerce_attribute_lookup_regeneration_step_size', $step_size ?? self::PRODUCTS_PER_GENERATION_STEP );
243
244 $products_already_processed = get_option( 'woocommerce_attribute_lookup_processed_count', 0 );
245
246 $product_ids = WC()->call_function(
247 'wc_get_products',
248 array(
249 'limit' => $products_per_generation_step,
250 'offset' => $products_already_processed,
251 'orderby' => array(
252 'ID' => 'ASC',
253 ),
254 'return' => 'ids',
255 )
256 );
257
258 if ( ! is_array( $product_ids ) || empty( $product_ids ) ) {
259 return false;
260 }
261
262 $this->last_regeneration_step_failed = false;
263 foreach ( $product_ids as $id ) {
264 $this->data_store->create_data_for_product( $id, $use_optimized_db_access );
265 $this->last_regeneration_step_failed = $this->last_regeneration_step_failed || $this->data_store->get_last_create_operation_failed();
266 }
267
268 $products_already_processed += count( $product_ids );
269 update_option( 'woocommerce_attribute_lookup_processed_count', $products_already_processed );
270
271 $last_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process', PHP_INT_MAX );
272 return end( $product_ids ) < $last_product_id_to_process;
273 }
274
275 /**
276 * Cleanup/final option setup after the regeneration has been completed.
277 *
278 * @param bool $enable_usage Whether the table usage should be enabled or not.
279 */
280 public function finalize_regeneration( bool $enable_usage ) {
281 $this->cancel_regeneration_scheduled_action();
282 $this->delete_all_attributes_lookup_data( false );
283 update_option( 'woocommerce_attribute_lookup_enabled', $enable_usage ? 'yes' : 'no' );
284 }
285
286 /**
287 * Add a 'Regenerate product attributes lookup table' entry to the Status - Tools page.
288 *
289 * @param array $tools_array The tool definitions array that is passed ro the woocommerce_debug_tools filter.
290 * @return array The tools array with the entry added.
291 *
292 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
293 */
294 public function add_initiate_regeneration_entry_to_tools_array( array $tools_array ) {
295 if ( ! $this->data_store->check_lookup_table_exists() ) {
296 return $tools_array;
297 }
298
299 $generation_is_in_progress = $this->data_store->regeneration_is_in_progress();
300 $generation_was_aborted = $this->data_store->regeneration_was_aborted();
301
302 $entry = array(
303 'name' => __( 'Regenerate the product attributes lookup table', 'woocommerce' ),
304 'desc' => __( 'This tool will regenerate the product attributes lookup table data from existing product(s) data. This process may take a while.', 'woocommerce' ),
305 'requires_refresh' => true,
306 'callback' => function () {
307 $this->initiate_regeneration_from_tools_page();
308 return __( 'Product attributes lookup table data is regenerating', 'woocommerce' );
309 },
310 'selector' => array(
311 'description' => __( 'Select a product to regenerate the data for, or leave empty for a full table regeneration:', 'woocommerce' ),
312 'class' => 'wc-product-search',
313 'search_action' => 'woocommerce_json_search_products',
314 'name' => 'regenerate_product_attribute_lookup_data_product_id',
315 'placeholder' => esc_attr__( 'Search for a product&hellip;', 'woocommerce' ),
316 ),
317 );
318
319 if ( $generation_is_in_progress ) {
320 $entry['button'] = sprintf(
321 /* translators: %d: How many products have been processed so far. */
322 __( 'Filling in progress (%d)', 'woocommerce' ),
323 get_option( 'woocommerce_attribute_lookup_processed_count', 0 )
324 );
325 $entry['disabled'] = true;
326 } else {
327 $entry['button'] = __( 'Regenerate', 'woocommerce' );
328 }
329
330 $tools_array['regenerate_product_attributes_lookup_table'] = $entry;
331
332 if ( $generation_is_in_progress ) {
333 $entry = array(
334 'name' => __( 'Abort the product attributes lookup table regeneration', 'woocommerce' ),
335 'desc' => __( 'This tool will abort the regenerate product attributes lookup table regeneration. After this is done the process can be either started over, or resumed to continue where it stopped.', 'woocommerce' ),
336 'requires_refresh' => true,
337 'callback' => function () {
338 $this->abort_regeneration( true );
339 return __( 'Product attributes lookup table regeneration process has been aborted.', 'woocommerce' );
340 },
341 'button' => __( 'Abort', 'woocommerce' ),
342 );
343 $tools_array['abort_product_attributes_lookup_table_regeneration'] = $entry;
344 } elseif ( $generation_was_aborted ) {
345 $processed_count = get_option( 'woocommerce_attribute_lookup_processed_count', 0 );
346 $entry = array(
347 'name' => __( 'Resume the product attributes lookup table regeneration', 'woocommerce' ),
348 'desc' =>
349 sprintf(
350 /* translators: %1$s = count of products already processed. */
351 __( 'This tool will resume the product attributes lookup table regeneration at the point in which it was aborted (%1$s products were already processed).', 'woocommerce' ),
352 $processed_count
353 ),
354 'requires_refresh' => true,
355 'callback' => function () {
356 $this->resume_regeneration( true );
357 return __( 'Product attributes lookup table regeneration process has been resumed.', 'woocommerce' );
358 },
359 'button' => __( 'Resume', 'woocommerce' ),
360 );
361 $tools_array['resume_product_attributes_lookup_table_regeneration'] = $entry;
362 }
363
364 return $tools_array;
365 }
366
367 /**
368 * Callback to initiate the regeneration process from the Status - Tools page.
369 *
370 * @throws \Exception The regeneration is already in progress.
371 */
372 private function initiate_regeneration_from_tools_page() {
373 $this->verify_tool_execution_nonce();
374
375 //phpcs:disable WordPress.Security.NonceVerification.Recommended
376 if ( isset( $_REQUEST['regenerate_product_attribute_lookup_data_product_id'] ) ) {
377 $product_id = (int) $_REQUEST['regenerate_product_attribute_lookup_data_product_id'];
378 $this->check_can_do_lookup_table_regeneration( $product_id );
379 $this->data_store->create_data_for_product( $product_id, $this->data_store->optimized_data_access_is_enabled() );
380 } else {
381 $this->initiate_regeneration();
382 }
383 //phpcs:enable WordPress.Security.NonceVerification.Recommended
384 }
385
386 /**
387 * Enable or disable the actual lookup table usage.
388 *
389 * @param bool $enable True to enable, false to disable.
390 * @throws \Exception A lookup table regeneration is currently in progress.
391 */
392 private function enable_or_disable_lookup_table_usage( $enable ) {
393 if ( $this->data_store->regeneration_is_in_progress() ) {
394 throw new \Exception( "Can't enable or disable the attributes lookup table usage while it's regenerating." );
395 }
396
397 update_option( 'woocommerce_attribute_lookup_enabled', $enable ? 'yes' : 'no' );
398 }
399
400 /**
401 * Check if everything is good to go to perform a complete or per product lookup table data regeneration
402 * and throw an exception if not.
403 *
404 * @param mixed $product_id The product id to check the regeneration viability for, or null to check if a complete regeneration is possible.
405 * @throws \Exception Something prevents the regeneration from starting.
406 */
407 public function check_can_do_lookup_table_regeneration( $product_id = null ) {
408 if ( $product_id && ! $this->data_store->check_lookup_table_exists() ) {
409 throw new \Exception( "Can't do product attribute lookup data regeneration: lookup table doesn't exist" );
410 }
411 if ( $this->data_store->regeneration_is_in_progress() ) {
412 throw new \Exception( "Can't do product attribute lookup data regeneration: regeneration is already in progress" );
413 }
414 if ( $product_id && ! wc_get_product( $product_id ) ) {
415 throw new \Exception( "Can't do product attribute lookup data regeneration: product doesn't exist" );
416 }
417 }
418
419 /**
420 * Callback to abort the regeneration process from the Status - Tools page or from CLI.
421 *
422 * @param bool $verify_nonce True to perform nonce verification (needed when running the tool from the tools page).
423 * @throws \Exception The lookup table doesn't exist, or there's no regeneration process in progress to abort.
424 */
425 public function abort_regeneration( bool $verify_nonce ) {
426 if ( $verify_nonce ) {
427 $this->verify_tool_execution_nonce();
428 }
429
430 if ( ! $this->data_store->check_lookup_table_exists() ) {
431 throw new \Exception( "Can't abort the product attribute lookup data regeneration process: lookup table doesn't exist" );
432 }
433 if ( ! $this->data_store->regeneration_is_in_progress() ) {
434 throw new \Exception( "Can't abort the product attribute lookup data regeneration process since it's not currently in progress" );
435 }
436
437 $this->cancel_regeneration_scheduled_action();
438 $this->data_store->unset_regeneration_in_progress_flag();
439 $this->data_store->set_regeneration_aborted_flag();
440 $this->enable_or_disable_lookup_table_usage( false );
441
442 // Note that we are NOT deleting the options that track the regeneration progress (processed count, last product id to process).
443 // This is on purpose so that the regeneration can be resumed where it stopped.
444 }
445
446 /**
447 * Cancel any existing regeneration step scheduled action.
448 */
449 public function cancel_regeneration_scheduled_action() {
450 $queue = WC()->get_instance_of( \WC_Queue::class );
451 $queue->cancel_all( 'woocommerce_run_product_attribute_lookup_regeneration_callback' );
452 }
453
454 /**
455 * Check if any pending regeneration step scheduled action exists.
456 *
457 * @return bool True if any pending regeneration step scheduled action exists.
458 */
459 public function has_scheduled_action_for_regeneration_step(): bool {
460 $queue = WC()->get_instance_of( \WC_Queue::class );
461 $actions = $queue->search(
462 array(
463 'hook' => 'woocommerce_run_product_attribute_lookup_regeneration_callback',
464 'status' => \ActionScheduler_Store::STATUS_PENDING,
465 ),
466 'ids'
467 );
468 return ! empty( $actions );
469 }
470
471 /**
472 * Callback to resume the regeneration process from the Status - Tools page or from CLI.
473 *
474 * @param bool $verify_nonce True to perform nonce verification (needed when running the tool from the tools page).
475 * @throws \Exception The lookup table doesn't exist, or a regeneration process is already in place or hasn't been aborted.
476 */
477 public function resume_regeneration( bool $verify_nonce ) {
478 if ( $verify_nonce ) {
479 $this->verify_tool_execution_nonce();
480 }
481
482 if ( ! $this->data_store->check_lookup_table_exists() ) {
483 throw new \Exception( "Can't resume the product attribute lookup data regeneration process: lookup table doesn't exist" );
484 }
485 if ( $this->data_store->regeneration_is_in_progress() ) {
486 throw new \Exception( "Can't resume the product attribute lookup data regeneration process: regeneration is already in progress" );
487 }
488 if ( ! $this->data_store->regeneration_was_aborted() ) {
489 throw new \Exception( "Can't resume the product attribute lookup data regeneration process: no aborted regeneration process exists" );
490 }
491
492 $this->data_store->unset_regeneration_aborted_flag();
493 $this->data_store->set_regeneration_in_progress_flag();
494 $this->enqueue_regeneration_step_run();
495 }
496
497 /**
498 * Verify the validity of the nonce received when executing a tool from the Status - Tools page.
499 *
500 * @throws \Exception Missing or invalid nonce received.
501 */
502 private function verify_tool_execution_nonce() {
503 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
504 if ( ! isset( $_REQUEST['_wpnonce'] ) || wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) === false ) {
505 throw new \Exception( 'Invalid nonce' );
506 }
507 }
508
509 /**
510 * Get the name of the product attributes lookup table.
511 *
512 * @return string
513 */
514 public function get_lookup_table_name() {
515 return $this->lookup_table_name;
516 }
517
518 /**
519 * Get the SQL statement that creates the product attributes lookup table, including the indices.
520 *
521 * @return string
522 */
523 public function get_table_creation_sql() {
524 global $wpdb;
525
526 $collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : '';
527
528 return "CREATE TABLE {$this->lookup_table_name} (
529 product_id bigint(20) NOT NULL,
530 product_or_parent_id bigint(20) NOT NULL,
531 taxonomy varchar(32) NOT NULL,
532 term_id bigint(20) NOT NULL,
533 is_variation_attribute tinyint(1) NOT NULL,
534 in_stock tinyint(1) NOT NULL,
535 INDEX is_variation_attribute_term_id (is_variation_attribute, term_id),
536 INDEX taxonomy_term_id_in_stock_product_or_parent_id (taxonomy, term_id, in_stock, product_or_parent_id),
537 PRIMARY KEY ( `product_or_parent_id`, `term_id`, `product_id`, `taxonomy` ),
538 KEY product_id (product_id)
539 ) $collate;";
540 }
541
542 /**
543 * Create the primary key for the table if it doesn't exist already.
544 * It also deletes the product_or_parent_id_term_id index if it exists, since it's now redundant.
545 *
546 * @return void
547 */
548 public function create_table_primary_index() {
549 $database_util = wc_get_container()->get( DatabaseUtil::class );
550 $database_util->create_primary_key( $this->lookup_table_name, array( 'product_or_parent_id', 'term_id', 'product_id', 'taxonomy' ) );
551 $database_util->drop_table_index( $this->lookup_table_name, 'product_or_parent_id_term_id' );
552
553 if ( empty( $database_util->get_index_columns( $this->lookup_table_name ) ) ) {
554 wc_get_logger()->error( "The creation of the primary key for the {$this->lookup_table_name} table failed" );
555 }
556
557 if ( ! empty( $database_util->get_index_columns( $this->lookup_table_name, 'product_or_parent_id_term_id' ) ) ) {
558 wc_get_logger()->error( "Dropping the product_or_parent_id_term_id index from the {$this->lookup_table_name} table failed" );
559 }
560 }
561
562 /**
563 * Run additional setup needed after a WooCommerce install or update finishes.
564 *
565 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
566 */
567 public function run_woocommerce_installed_callback() {
568 // The table must exist at this point (created via dbDelta), but we check just in case.
569 if ( ! $this->data_store->check_lookup_table_exists() ) {
570 return;
571 }
572
573 // If a table regeneration is in progress, leave it alone.
574 if ( $this->data_store->regeneration_is_in_progress() ) {
575 return;
576 }
577
578 // If the lookup table has data, or if it's empty because there are no products yet, we're good.
579 // Otherwise (lookup table is empty but products exist) we need to initiate a regeneration if one isn't already in progress.
580 if ( $this->data_store->lookup_table_has_data() || ! $this->get_last_existing_product_id() ) {
581 $must_enable = get_option( 'woocommerce_attribute_lookup_enabled' ) !== 'no';
582 $this->delete_all_attributes_lookup_data( false );
583 update_option( 'woocommerce_attribute_lookup_enabled', $must_enable ? 'yes' : 'no' );
584 } else {
585 $this->initiate_regeneration();
586 }
587 }
588 }
589