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