CustomOrdersTableController.php
| 1 | <?php |
| 2 | /** |
| 3 | * CustomOrdersTableController class file. |
| 4 | */ |
| 5 | |
| 6 | namespace Automattic\WooCommerce\Internal\DataStores\Orders; |
| 7 | |
| 8 | use Automattic\WooCommerce\Caches\OrderCache; |
| 9 | use Automattic\WooCommerce\Caches\OrderCacheController; |
| 10 | use Automattic\WooCommerce\Enums\FeaturePluginCompatibility; |
| 11 | use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController; |
| 12 | use Automattic\WooCommerce\Internal\Features\FeaturesController; |
| 13 | use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil; |
| 14 | use Automattic\WooCommerce\Utilities\OrderUtil; |
| 15 | use Automattic\WooCommerce\Utilities\PluginUtil; |
| 16 | use WC_Admin_Settings; |
| 17 | |
| 18 | defined( 'ABSPATH' ) || exit; |
| 19 | |
| 20 | /** |
| 21 | * This is the main class that controls the custom orders tables feature. Its responsibilities are: |
| 22 | * |
| 23 | * - Displaying UI components (entries in the tools page and in settings) |
| 24 | * - Providing the proper data store for orders via 'woocommerce_order_data_store' hook |
| 25 | * |
| 26 | * ...and in general, any functionality that doesn't imply database access. |
| 27 | */ |
| 28 | class CustomOrdersTableController { |
| 29 | |
| 30 | private const SYNC_QUERY_ARG = 'wc_hpos_sync_now'; |
| 31 | |
| 32 | private const STOP_SYNC_QUERY_ARG = 'wc_hpos_stop_sync'; |
| 33 | |
| 34 | /** |
| 35 | * The name of the option for enabling the usage of the custom orders tables |
| 36 | */ |
| 37 | public const CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION = 'woocommerce_custom_orders_table_enabled'; |
| 38 | |
| 39 | /** |
| 40 | * The name of the option that tells whether database transactions are to be used or not for data synchronization. |
| 41 | */ |
| 42 | public const USE_DB_TRANSACTIONS_OPTION = 'woocommerce_use_db_transactions_for_custom_orders_table_data_sync'; |
| 43 | |
| 44 | /** |
| 45 | * The name of the option to store the transaction isolation level to use when database transactions are enabled. |
| 46 | */ |
| 47 | public const DB_TRANSACTIONS_ISOLATION_LEVEL_OPTION = 'woocommerce_db_transactions_isolation_level_for_custom_orders_table_data_sync'; |
| 48 | |
| 49 | public const DEFAULT_DB_TRANSACTIONS_ISOLATION_LEVEL = 'READ UNCOMMITTED'; |
| 50 | |
| 51 | public const HPOS_FTS_INDEX_OPTION = 'woocommerce_hpos_fts_index_enabled'; |
| 52 | |
| 53 | public const HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION = 'woocommerce_hpos_address_fts_index_created'; |
| 54 | |
| 55 | public const HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION = 'woocommerce_hpos_order_item_fts_index_created'; |
| 56 | |
| 57 | public const HPOS_DATASTORE_CACHING_ENABLED_OPTION = 'woocommerce_hpos_datastore_caching_enabled'; |
| 58 | |
| 59 | /** |
| 60 | * The data store object to use. |
| 61 | * |
| 62 | * @var OrdersTableDataStore |
| 63 | */ |
| 64 | private $data_store; |
| 65 | |
| 66 | /** |
| 67 | * Refunds data store object to use. |
| 68 | * |
| 69 | * @var OrdersTableRefundDataStore |
| 70 | */ |
| 71 | private $refund_data_store; |
| 72 | |
| 73 | /** |
| 74 | * The data synchronizer object to use. |
| 75 | * |
| 76 | * @var DataSynchronizer |
| 77 | */ |
| 78 | private $data_synchronizer; |
| 79 | |
| 80 | /** |
| 81 | * The data cleanup instance to use. |
| 82 | * |
| 83 | * @var LegacyDataCleanup |
| 84 | */ |
| 85 | private $data_cleanup; |
| 86 | |
| 87 | /** |
| 88 | * The batch processing controller to use. |
| 89 | * |
| 90 | * @var BatchProcessingController |
| 91 | */ |
| 92 | private $batch_processing_controller; |
| 93 | |
| 94 | /** |
| 95 | * The features controller to use. |
| 96 | * |
| 97 | * @var FeaturesController |
| 98 | */ |
| 99 | private $features_controller; |
| 100 | |
| 101 | /** |
| 102 | * The orders cache object to use. |
| 103 | * |
| 104 | * @var OrderCache |
| 105 | */ |
| 106 | private $order_cache; |
| 107 | |
| 108 | /** |
| 109 | * The orders cache controller object to use. |
| 110 | * |
| 111 | * @var OrderCacheController |
| 112 | */ |
| 113 | private $order_cache_controller; |
| 114 | |
| 115 | /** |
| 116 | * The plugin util object to use. |
| 117 | * |
| 118 | * @var PluginUtil |
| 119 | */ |
| 120 | private $plugin_util; |
| 121 | |
| 122 | /** |
| 123 | * The db util object to use. |
| 124 | * |
| 125 | * @var DatabaseUtil; |
| 126 | */ |
| 127 | private $db_util; |
| 128 | |
| 129 | /** |
| 130 | * Class constructor. |
| 131 | */ |
| 132 | public function __construct() { |
| 133 | $this->init_hooks(); |
| 134 | } |
| 135 | |
| 136 | /** |
| 137 | * Initialize the hooks used by the class. |
| 138 | */ |
| 139 | private function init_hooks() { |
| 140 | add_filter( 'woocommerce_order_data_store', array( $this, 'get_orders_data_store' ), 999, 1 ); |
| 141 | add_filter( 'woocommerce_order-refund_data_store', array( $this, 'get_refunds_data_store' ), 999, 1 ); |
| 142 | add_filter( 'woocommerce_debug_tools', array( $this, 'add_hpos_tools' ), 999 ); |
| 143 | add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 ); |
| 144 | add_filter( 'updated_option', array( $this, 'process_updated_option_fts_index' ), 999, 3 ); |
| 145 | add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 ); |
| 146 | add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 ); |
| 147 | add_action( 'woocommerce_sections_advanced', array( $this, 'sync_now' ) ); |
| 148 | add_filter( 'removable_query_args', array( $this, 'register_removable_query_arg' ) ); |
| 149 | add_filter( 'get_edit_post_link', array( $this, 'maybe_rewrite_order_edit_link' ), 10, 2 ); |
| 150 | add_action( 'before_woocommerce_init', array( $this, 'maybe_set_order_cache_group_as_non_persistent' ) ); |
| 151 | add_filter( 'map_meta_cap', array( $this, 'maybe_translate_order_caps' ), 0, 4 ); |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * Class initialization, invoked by the DI container. |
| 156 | * |
| 157 | * @internal |
| 158 | * @param OrdersTableDataStore $data_store The data store to use. |
| 159 | * @param DataSynchronizer $data_synchronizer The data synchronizer to use. |
| 160 | * @param LegacyDataCleanup $data_cleanup The legacy data cleanup instance to use. |
| 161 | * @param OrdersTableRefundDataStore $refund_data_store The refund data store to use. |
| 162 | * @param BatchProcessingController $batch_processing_controller The batch processing controller to use. |
| 163 | * @param FeaturesController $features_controller The features controller instance to use. |
| 164 | * @param OrderCache $order_cache The order cache engine to use. |
| 165 | * @param OrderCacheController $order_cache_controller The order cache controller to use. |
| 166 | * @param PluginUtil $plugin_util The plugin util to use. |
| 167 | * @param DatabaseUtil $db_util The database util to use. |
| 168 | */ |
| 169 | final public function init( |
| 170 | OrdersTableDataStore $data_store, |
| 171 | DataSynchronizer $data_synchronizer, |
| 172 | LegacyDataCleanup $data_cleanup, |
| 173 | OrdersTableRefundDataStore $refund_data_store, |
| 174 | BatchProcessingController $batch_processing_controller, |
| 175 | FeaturesController $features_controller, |
| 176 | OrderCache $order_cache, |
| 177 | OrderCacheController $order_cache_controller, |
| 178 | PluginUtil $plugin_util, |
| 179 | DatabaseUtil $db_util |
| 180 | ) { |
| 181 | $this->data_store = $data_store; |
| 182 | $this->data_synchronizer = $data_synchronizer; |
| 183 | $this->data_cleanup = $data_cleanup; |
| 184 | $this->batch_processing_controller = $batch_processing_controller; |
| 185 | $this->refund_data_store = $refund_data_store; |
| 186 | $this->features_controller = $features_controller; |
| 187 | $this->order_cache = $order_cache; |
| 188 | $this->order_cache_controller = $order_cache_controller; |
| 189 | $this->plugin_util = $plugin_util; |
| 190 | $this->db_util = $db_util; |
| 191 | } |
| 192 | |
| 193 | /** |
| 194 | * Translate capabilities for HPOS orders when sync is not active. |
| 195 | * |
| 196 | * Only activates when HPOS is the authoritative source and sync is off, |
| 197 | * then lazily delegates to HposOrderCapabilityHelper for the actual |
| 198 | * capability translation. |
| 199 | * |
| 200 | * @since 10.7.0 |
| 201 | * |
| 202 | * @param string[] $caps The resolved primitive capabilities. |
| 203 | * @param string $cap The meta capability being checked. |
| 204 | * @param int $user_id The user ID. |
| 205 | * @param array $args Additional arguments (object ID). |
| 206 | * @return string[] Translated capabilities. |
| 207 | */ |
| 208 | public function maybe_translate_order_caps( $caps, $cap, $user_id, $args ) { |
| 209 | if ( ! $this->custom_orders_table_usage_is_enabled() ) { |
| 210 | return $caps; |
| 211 | } |
| 212 | |
| 213 | if ( ! $this->data_synchronizer instanceof DataSynchronizer ) { |
| 214 | return $caps; |
| 215 | } |
| 216 | |
| 217 | if ( $this->data_synchronizer->data_sync_is_enabled() ) { |
| 218 | return $caps; |
| 219 | } |
| 220 | |
| 221 | return wc_get_container()->get( HposOrderCapabilityHelper::class )->translate_order_caps( $caps, $cap, $user_id, $args ); |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Is the custom orders table usage enabled via settings? |
| 226 | * This can be true only if the feature is enabled and a table regeneration has been completed. |
| 227 | * |
| 228 | * @return bool True if the custom orders table usage is enabled |
| 229 | */ |
| 230 | public function custom_orders_table_usage_is_enabled(): bool { |
| 231 | return get_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ) === 'yes'; |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * Is caching of data within the CustomerOrdersTable datastores enabled? |
| 236 | * |
| 237 | * @return bool True if the caching is enabled within the CustomeOrderTable Datastores. |
| 238 | */ |
| 239 | public function hpos_data_caching_is_enabled(): bool { |
| 240 | return get_option( self::HPOS_DATASTORE_CACHING_ENABLED_OPTION ) === 'yes' && |
| 241 | $this->custom_orders_table_usage_is_enabled(); |
| 242 | } |
| 243 | |
| 244 | /** |
| 245 | * Gets the instance of the orders data store to use. |
| 246 | * |
| 247 | * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order_data_store hook). |
| 248 | * |
| 249 | * @return \WC_Object_Data_Store_Interface|string The actual data store to use. |
| 250 | * |
| 251 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 252 | */ |
| 253 | public function get_orders_data_store( $default_data_store ) { |
| 254 | return $this->get_data_store_instance( $default_data_store, 'order' ); |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Gets the instance of the refunds data store to use. |
| 259 | * |
| 260 | * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the woocommerce_order-refund_data_store hook). |
| 261 | * |
| 262 | * @return \WC_Object_Data_Store_Interface|string The actual data store to use. |
| 263 | * |
| 264 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 265 | */ |
| 266 | public function get_refunds_data_store( $default_data_store ) { |
| 267 | return $this->get_data_store_instance( $default_data_store, 'order_refund' ); |
| 268 | } |
| 269 | |
| 270 | /** |
| 271 | * Gets the instance of a given data store. |
| 272 | * |
| 273 | * @param \WC_Object_Data_Store_Interface|string $default_data_store The default data store (as received via the appropriate hooks). |
| 274 | * @param string $type The type of the data store to get. |
| 275 | * |
| 276 | * @return \WC_Object_Data_Store_Interface|string The actual data store to use. |
| 277 | */ |
| 278 | private function get_data_store_instance( $default_data_store, string $type ) { |
| 279 | if ( $this->custom_orders_table_usage_is_enabled() ) { |
| 280 | switch ( $type ) { |
| 281 | case 'order_refund': |
| 282 | return $this->refund_data_store; |
| 283 | default: |
| 284 | return $this->data_store; |
| 285 | } |
| 286 | } else { |
| 287 | return $default_data_store; |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | /** |
| 292 | * Add an entry to Status - Tools to create or regenerate the custom orders table, |
| 293 | * and also an entry to delete the table as appropriate. |
| 294 | * |
| 295 | * @param array $tools_array The array of tools to add the tool to. |
| 296 | * @return array The updated array of tools. |
| 297 | * |
| 298 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 299 | */ |
| 300 | public function add_hpos_tools( array $tools_array ): array { |
| 301 | if ( ! $this->data_synchronizer->check_orders_table_exists() ) { |
| 302 | return $tools_array; |
| 303 | } |
| 304 | |
| 305 | // Cleanup tool. |
| 306 | $tools_array = array_merge( $tools_array, $this->data_cleanup->get_tools_entries() ); |
| 307 | |
| 308 | // Delete HPOS tables tool. |
| 309 | if ( $this->custom_orders_table_usage_is_enabled() || $this->data_synchronizer->data_sync_is_enabled() || $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) ) ) { |
| 310 | $disabled = true; |
| 311 | $message = __( 'This will delete the custom orders tables. The tables can be deleted only if the "High-Performance order storage" is not authoritative and sync is disabled (via Settings > Advanced > Features).', 'woocommerce' ); |
| 312 | } else { |
| 313 | $disabled = false; |
| 314 | $message = __( 'This will delete the custom orders tables. To create them again enable the "High-Performance order storage" feature (via Settings > Advanced > Features).', 'woocommerce' ); |
| 315 | } |
| 316 | |
| 317 | $tools_array['delete_custom_orders_table'] = array( |
| 318 | 'name' => __( 'Delete the custom orders tables', 'woocommerce' ), |
| 319 | 'desc' => sprintf( |
| 320 | '<strong class="red">%1$s</strong> %2$s', |
| 321 | __( 'Note:', 'woocommerce' ), |
| 322 | $message |
| 323 | ), |
| 324 | 'requires_refresh' => true, |
| 325 | 'callback' => function () use ( $disabled ) { |
| 326 | if ( $disabled ) { |
| 327 | return; |
| 328 | } |
| 329 | |
| 330 | $this->features_controller->change_feature_enable( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, false ); |
| 331 | $this->delete_custom_orders_tables(); |
| 332 | return __( 'Custom orders tables have been deleted.', 'woocommerce' ); |
| 333 | }, |
| 334 | 'button' => __( 'Delete', 'woocommerce' ), |
| 335 | 'disabled' => $disabled, |
| 336 | ); |
| 337 | |
| 338 | return $tools_array; |
| 339 | } |
| 340 | |
| 341 | /** |
| 342 | * Delete the custom orders tables and any related options and data in response to the user pressing the tool button. |
| 343 | * |
| 344 | * @throws \Exception Can't delete the tables. |
| 345 | */ |
| 346 | private function delete_custom_orders_tables() { |
| 347 | if ( $this->custom_orders_table_usage_is_enabled() ) { |
| 348 | throw new \Exception( "Can't delete the custom orders tables: they are currently in use (via Settings > Advanced > Features)." ); |
| 349 | } |
| 350 | |
| 351 | delete_option( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION ); |
| 352 | $this->data_synchronizer->delete_database_tables(); |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Handler for the individual setting updated hook. |
| 357 | * |
| 358 | * @param string $option Setting name. |
| 359 | * @param mixed $old_value Old value of the setting. |
| 360 | * @param mixed $value New value of the setting. |
| 361 | * |
| 362 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 363 | */ |
| 364 | public function process_updated_option( $option, $old_value, $value ) { |
| 365 | if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && 'no' === $value ) { |
| 366 | $this->data_synchronizer->cleanup_synchronization_state(); |
| 367 | } |
| 368 | if ( self::HPOS_DATASTORE_CACHING_ENABLED_OPTION === $option && $old_value !== $value && 'yes' === $value ) { |
| 369 | $this->data_store->clear_all_cached_data(); |
| 370 | } |
| 371 | } |
| 372 | |
| 373 | /** |
| 374 | * Process option that enables FTS index on orders table. Tries to create an FTS index when option is enabled. |
| 375 | * |
| 376 | * @param string $option Option name. |
| 377 | * @param string $old_value Old value of the option. |
| 378 | * @param string $value New value of the option. |
| 379 | * |
| 380 | * @return void |
| 381 | * |
| 382 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 383 | */ |
| 384 | public function process_updated_option_fts_index( $option, $old_value, $value ) { |
| 385 | if ( self::HPOS_FTS_INDEX_OPTION !== $option ) { |
| 386 | return; |
| 387 | } |
| 388 | |
| 389 | if ( 'yes' !== $value ) { |
| 390 | return; |
| 391 | } |
| 392 | |
| 393 | if ( ! $this->custom_orders_table_usage_is_enabled() ) { |
| 394 | update_option( self::HPOS_FTS_INDEX_OPTION, 'no', true ); |
| 395 | if ( class_exists( 'WC_Admin_Settings' ) ) { |
| 396 | WC_Admin_Settings::add_error( __( 'Failed to create FTS index on orders table. This feature is only available when High-performance order storage is enabled.', 'woocommerce' ) ); |
| 397 | } |
| 398 | return; |
| 399 | } |
| 400 | |
| 401 | if ( ! $this->db_util->fts_index_on_order_address_table_exists() ) { |
| 402 | $this->db_util->create_fts_index_order_address_table(); |
| 403 | } |
| 404 | |
| 405 | // Check again to see if index was actually created. |
| 406 | if ( $this->db_util->fts_index_on_order_address_table_exists() ) { |
| 407 | update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'yes', false ); |
| 408 | } else { |
| 409 | update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'no', false ); |
| 410 | if ( class_exists( 'WC_Admin_Settings ' ) ) { |
| 411 | WC_Admin_Settings::add_error( __( 'Failed to create FTS index on address table', 'woocommerce' ) ); |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | if ( ! $this->db_util->fts_index_on_order_item_table_exists() ) { |
| 416 | $this->db_util->create_fts_index_order_item_table(); |
| 417 | } |
| 418 | |
| 419 | // Check again to see if index was actually created. |
| 420 | if ( $this->db_util->fts_index_on_order_item_table_exists() ) { |
| 421 | update_option( self::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION, 'yes', false ); |
| 422 | } else { |
| 423 | update_option( self::HPOS_FTS_ORDER_ITEM_INDEX_CREATED_OPTION, 'no', false ); |
| 424 | if ( class_exists( 'WC_Admin_Settings ' ) ) { |
| 425 | WC_Admin_Settings::add_error( __( 'Failed to create FTS index on order item table', 'woocommerce' ) ); |
| 426 | } |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | /** |
| 431 | * Recreate order addresses FTS index. Useful when updating to 9.4 when phone number was added to index, or when other recreating index is needed. |
| 432 | * |
| 433 | * @since 9.4.0. |
| 434 | * |
| 435 | * @return array Array with keys status (bool) and message (string). |
| 436 | */ |
| 437 | public function recreate_order_address_fts_index(): array { |
| 438 | $this->db_util->drop_fts_index_order_address_table(); |
| 439 | if ( $this->db_util->fts_index_on_order_address_table_exists() ) { |
| 440 | return array( |
| 441 | 'status' => false, |
| 442 | 'message' => __( 'Failed to modify existing FTS index. Please go to WooCommerce > Status > Tools and run the "Re-create Order Address FTS index" tool.', 'woocommerce' ), |
| 443 | ); |
| 444 | } else { |
| 445 | update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'no', false ); |
| 446 | } |
| 447 | |
| 448 | $this->db_util->create_fts_index_order_address_table(); |
| 449 | if ( ! $this->db_util->fts_index_on_order_address_table_exists() ) { |
| 450 | return array( |
| 451 | 'status' => false, |
| 452 | 'message' => __( 'Failed to create FTS index on order address table. Please go to WooCommerce > Status > Tools and run the "Re-create Order Address FTS index" tool.', 'woocommerce' ), |
| 453 | ); |
| 454 | } else { |
| 455 | update_option( self::HPOS_FTS_ADDRESS_INDEX_CREATED_OPTION, 'yes', false ); |
| 456 | return array( |
| 457 | 'status' => true, |
| 458 | 'message' => __( 'FTS index recreated.', 'woocommerce' ), |
| 459 | ); |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | /** |
| 464 | * Handler for the setting pre-update hook. |
| 465 | * We use it to verify that authoritative orders table switch doesn't happen while sync is pending. |
| 466 | * |
| 467 | * @param mixed $value New value of the setting. |
| 468 | * @param string $option Setting name. |
| 469 | * @param mixed $old_value Old value of the setting. |
| 470 | * |
| 471 | * @throws \Exception Attempt to change the authoritative orders table while orders sync is pending. |
| 472 | * |
| 473 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 474 | */ |
| 475 | public function process_pre_update_option( $value, $option, $old_value ) { |
| 476 | if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && $value !== $old_value ) { |
| 477 | $this->order_cache->flush(); |
| 478 | return $value; |
| 479 | } |
| 480 | |
| 481 | if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option ) { |
| 482 | return $value; |
| 483 | } |
| 484 | |
| 485 | if ( $old_value === $value ) { |
| 486 | return $value; |
| 487 | } |
| 488 | |
| 489 | $this->order_cache->flush(); |
| 490 | if ( ! $this->data_synchronizer->check_orders_table_exists() ) { |
| 491 | $this->data_synchronizer->create_database_tables(); |
| 492 | } |
| 493 | |
| 494 | $tables_created = get_option( DataSynchronizer::ORDERS_TABLE_CREATED ) === 'yes'; |
| 495 | if ( ! $tables_created ) { |
| 496 | return 'no'; |
| 497 | } |
| 498 | |
| 499 | if ( ! $this->changing_data_source_with_sync_pending_is_allowed() && $this->data_synchronizer->has_orders_pending_sync() ) { |
| 500 | throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" ); |
| 501 | } |
| 502 | |
| 503 | return $value; |
| 504 | } |
| 505 | |
| 506 | /** |
| 507 | * Callback to trigger a sync immediately by clicking a button on the Features screen. |
| 508 | * |
| 509 | * @return void |
| 510 | * |
| 511 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 512 | */ |
| 513 | public function sync_now() { |
| 514 | $section = filter_input( INPUT_GET, 'section' ); |
| 515 | if ( 'features' !== $section ) { |
| 516 | return; |
| 517 | } |
| 518 | |
| 519 | if ( filter_input( INPUT_GET, self::SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) { |
| 520 | $action = 'sync-now'; |
| 521 | } elseif ( filter_input( INPUT_GET, self::STOP_SYNC_QUERY_ARG, FILTER_VALIDATE_BOOLEAN ) ) { |
| 522 | $action = 'stop-sync'; |
| 523 | } else { |
| 524 | return; |
| 525 | } |
| 526 | |
| 527 | if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), "hpos-{$action}" ) ) { |
| 528 | WC_Admin_Settings::add_error( |
| 529 | 'sync-now' === $action ? |
| 530 | esc_html__( 'Unable to start synchronization. The link you followed may have expired.', 'woocommerce' ) |
| 531 | : esc_html__( 'Unable to stop synchronization. The link you followed may have expired.', 'woocommerce' ) |
| 532 | ); |
| 533 | return; |
| 534 | } |
| 535 | |
| 536 | $this->data_cleanup->toggle_flag( false ); |
| 537 | |
| 538 | if ( 'sync-now' === $action ) { |
| 539 | if ( ! $this->data_synchronizer->check_orders_table_exists() && ! $this->data_synchronizer->create_database_tables() ) { |
| 540 | WC_Admin_Settings::add_error( |
| 541 | __( 'Unable to create HPOS tables for synchronization.', 'woocommerce' ) |
| 542 | ); |
| 543 | return; |
| 544 | } |
| 545 | |
| 546 | $this->batch_processing_controller->enqueue_processor( DataSynchronizer::class ); |
| 547 | } else { |
| 548 | $this->batch_processing_controller->remove_processor( DataSynchronizer::class ); |
| 549 | } |
| 550 | } |
| 551 | |
| 552 | /** |
| 553 | * Tell WP Admin to remove the sync query arg from the URL. |
| 554 | * |
| 555 | * @param array $query_args The query args that are removable. |
| 556 | * |
| 557 | * @return array |
| 558 | * |
| 559 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 560 | */ |
| 561 | public function register_removable_query_arg( $query_args ) { |
| 562 | $query_args[] = self::SYNC_QUERY_ARG; |
| 563 | $query_args[] = self::STOP_SYNC_QUERY_ARG; |
| 564 | |
| 565 | return $query_args; |
| 566 | } |
| 567 | |
| 568 | /** |
| 569 | * Handler for the woocommerce_after_register_post_type post, |
| 570 | * registers the post type for placeholder orders. |
| 571 | * |
| 572 | * @return void |
| 573 | * |
| 574 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 575 | */ |
| 576 | public function register_post_type_for_order_placeholders(): void { |
| 577 | wc_register_order_type( |
| 578 | DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE, |
| 579 | array( |
| 580 | 'public' => false, |
| 581 | 'exclude_from_search' => true, |
| 582 | 'publicly_queryable' => false, |
| 583 | 'show_ui' => false, |
| 584 | 'show_in_menu' => false, |
| 585 | 'show_in_nav_menus' => false, |
| 586 | 'show_in_admin_bar' => false, |
| 587 | 'show_in_rest' => false, |
| 588 | 'rewrite' => false, |
| 589 | 'query_var' => false, |
| 590 | 'can_export' => false, |
| 591 | 'supports' => array(), |
| 592 | 'capabilities' => array(), |
| 593 | 'exclude_from_order_count' => true, |
| 594 | 'exclude_from_order_views' => true, |
| 595 | 'exclude_from_order_reports' => true, |
| 596 | 'exclude_from_order_sales_reports' => true, |
| 597 | ) |
| 598 | ); |
| 599 | } |
| 600 | |
| 601 | /** |
| 602 | * Add the definition for the HPOS feature. |
| 603 | * |
| 604 | * @param FeaturesController $features_controller The instance of FeaturesController. |
| 605 | * |
| 606 | * @return void |
| 607 | * |
| 608 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 609 | */ |
| 610 | public function add_feature_definition( $features_controller ) { |
| 611 | $definition = array( |
| 612 | 'option_key' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, |
| 613 | 'is_experimental' => false, |
| 614 | 'enabled_by_default' => false, |
| 615 | 'order' => 50, |
| 616 | 'setting' => $this->get_hpos_setting_for_feature(), |
| 617 | 'default_plugin_compatibility' => FeaturePluginCompatibility::INCOMPATIBLE, |
| 618 | 'additional_settings' => array( |
| 619 | $this->get_hpos_setting_for_sync(), |
| 620 | ), |
| 621 | ); |
| 622 | |
| 623 | $features_controller->add_feature_definition( |
| 624 | 'custom_order_tables', |
| 625 | __( 'High-Performance order storage', 'woocommerce' ), |
| 626 | $definition |
| 627 | ); |
| 628 | } |
| 629 | |
| 630 | /** |
| 631 | * Returns the HPOS setting for rendering HPOS vs Post setting block in Features section of the settings page. |
| 632 | * |
| 633 | * @return array Feature setting object. |
| 634 | */ |
| 635 | private function get_hpos_setting_for_feature() { |
| 636 | if ( 'yes' === get_transient( 'wc_installing' ) ) { |
| 637 | return array(); |
| 638 | } |
| 639 | |
| 640 | $get_value = function () { |
| 641 | return $this->custom_orders_table_usage_is_enabled() ? 'yes' : 'no'; |
| 642 | }; |
| 643 | |
| 644 | /** |
| 645 | * ⚠️The FeaturesController instance must only be accessed from within the callback functions. Otherwise it |
| 646 | * gets called while it's still being instantiated and creates and endless loop. |
| 647 | */ |
| 648 | |
| 649 | $get_desc = function () { |
| 650 | $plugin_compatibility = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true ); |
| 651 | |
| 652 | return $this->plugin_util->generate_incompatible_plugin_feature_warning( 'custom_order_tables', $plugin_compatibility ); |
| 653 | }; |
| 654 | |
| 655 | $get_disabled = function () { |
| 656 | $compatibility_info = $this->features_controller->get_compatible_plugins_for_feature( 'custom_order_tables', true ); |
| 657 | $sync_complete = ! $this->data_synchronizer->has_orders_pending_sync(); |
| 658 | $disabled = array(); |
| 659 | // Changing something here? You might also want to look at `enable|disable` functions in Automattic\WooCommerce\Database\Migrations\CustomOrderTable\CLIRunner. |
| 660 | $incompatible_plugins = $this->plugin_util->get_items_considered_incompatible( 'custom_order_tables', $compatibility_info ); |
| 661 | $incompatible_plugins = array_diff( $incompatible_plugins, $this->plugin_util->get_plugins_excluded_from_compatibility_ui() ); |
| 662 | if ( count( $incompatible_plugins ) > 0 ) { |
| 663 | $disabled = array( 'yes' ); |
| 664 | } |
| 665 | if ( ! $sync_complete && ! $this->changing_data_source_with_sync_pending_is_allowed() ) { |
| 666 | $disabled = array( 'yes', 'no' ); |
| 667 | } |
| 668 | |
| 669 | return $disabled; |
| 670 | }; |
| 671 | |
| 672 | return array( |
| 673 | 'id' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, |
| 674 | 'title' => __( 'Order data storage', 'woocommerce' ), |
| 675 | 'type' => 'radio', |
| 676 | 'options' => array( |
| 677 | 'no' => __( 'WordPress posts storage (legacy)', 'woocommerce' ), |
| 678 | 'yes' => __( 'High-performance order storage (recommended)', 'woocommerce' ), |
| 679 | ), |
| 680 | 'value' => $get_value, |
| 681 | 'disabled' => $get_disabled, |
| 682 | 'desc' => $get_desc, |
| 683 | 'desc_at_end' => true, |
| 684 | 'row_class' => self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION, |
| 685 | ); |
| 686 | } |
| 687 | |
| 688 | /** |
| 689 | * Returns the setting for rendering sync enabling setting block in Features section of the settings page. |
| 690 | * |
| 691 | * @return array Feature setting object. |
| 692 | */ |
| 693 | private function get_hpos_setting_for_sync() { |
| 694 | if ( 'yes' === get_transient( 'wc_installing' ) ) { |
| 695 | return array(); |
| 696 | } |
| 697 | |
| 698 | $get_value = function () { |
| 699 | return get_option( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION ); |
| 700 | }; |
| 701 | |
| 702 | $get_sync_message = function () { |
| 703 | $sync_in_progress = $this->batch_processing_controller->is_enqueued( get_class( $this->data_synchronizer ) ); |
| 704 | $sync_enabled = $this->data_synchronizer->data_sync_is_enabled(); |
| 705 | $sync_is_pending = $this->data_synchronizer->has_orders_pending_sync( true ); |
| 706 | $sync_message = array(); |
| 707 | $is_dangerous = $sync_is_pending && $this->changing_data_source_with_sync_pending_is_allowed(); |
| 708 | |
| 709 | if ( $is_dangerous ) { |
| 710 | $sync_message[] = wp_kses_data( |
| 711 | __( "There are orders pending sync.", 'woocommerce' ) |
| 712 | . '<strong>' |
| 713 | . __( 'Switching data storage while sync is incomplete is dangerous and can lead to order data corruption or loss!', 'woocommerce' ) |
| 714 | . '</strong>' |
| 715 | ); |
| 716 | } |
| 717 | |
| 718 | if ( ! $sync_enabled && $this->data_synchronizer->background_sync_is_enabled() ) { |
| 719 | $sync_message[] = __( 'Background sync is enabled.', 'woocommerce' ); |
| 720 | } |
| 721 | |
| 722 | if ( $sync_in_progress && $sync_is_pending ) { |
| 723 | $orders_pending_sync_count = $this->data_synchronizer->get_current_orders_pending_sync_count( true ); |
| 724 | |
| 725 | $sync_message[] = sprintf( |
| 726 | // translators: %s: number of pending orders. |
| 727 | __( 'Currently syncing orders... %s pending', 'woocommerce' ), |
| 728 | number_format_i18n( $orders_pending_sync_count ) |
| 729 | ); |
| 730 | |
| 731 | if ( ! $sync_enabled ) { |
| 732 | $stop_sync_url = wp_nonce_url( |
| 733 | add_query_arg( |
| 734 | array( |
| 735 | self::STOP_SYNC_QUERY_ARG => true, |
| 736 | ), |
| 737 | wc_get_container()->get( FeaturesController::class )->get_features_page_url() |
| 738 | ), |
| 739 | 'hpos-stop-sync' |
| 740 | ); |
| 741 | |
| 742 | $sync_message[] = sprintf( |
| 743 | '<a href="%1$s" class="button-link">%2$s</a>', |
| 744 | esc_url( $stop_sync_url ), |
| 745 | __( 'Stop sync', 'woocommerce' ) |
| 746 | ); |
| 747 | } |
| 748 | } elseif ( $sync_is_pending ) { |
| 749 | $sync_now_url = wp_nonce_url( |
| 750 | add_query_arg( |
| 751 | array( |
| 752 | self::SYNC_QUERY_ARG => true, |
| 753 | ), |
| 754 | wc_get_container()->get( FeaturesController::class )->get_features_page_url() |
| 755 | ), |
| 756 | 'hpos-sync-now' |
| 757 | ); |
| 758 | |
| 759 | if ( ! $is_dangerous ) { |
| 760 | $sync_message[] = wp_kses_data( |
| 761 | __( "You can switch order data storage <strong>only when the posts and orders tables are in sync</strong>. There are currently orders out of sync.", 'woocommerce' ), |
| 762 | ); |
| 763 | } |
| 764 | |
| 765 | $sync_message[] = sprintf( |
| 766 | '<a href="%1$s" class="button-link">%2$s</a>', |
| 767 | esc_url( $sync_now_url ), |
| 768 | __( 'Sync orders now', 'woocommerce' ) |
| 769 | ); |
| 770 | } |
| 771 | |
| 772 | return implode( '<br />', $sync_message ); |
| 773 | }; |
| 774 | |
| 775 | $get_description_is_error = function () { |
| 776 | $sync_is_pending = $this->data_synchronizer->has_orders_pending_sync(); |
| 777 | |
| 778 | return $sync_is_pending && $this->changing_data_source_with_sync_pending_is_allowed(); |
| 779 | }; |
| 780 | |
| 781 | return array( |
| 782 | 'id' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, |
| 783 | 'title' => '', |
| 784 | 'type' => 'checkbox', |
| 785 | 'desc' => __( 'Enable compatibility mode (Synchronize orders between High-performance order storage and WordPress posts storage).', 'woocommerce' ), |
| 786 | 'value' => $get_value, |
| 787 | 'desc_tip' => $get_sync_message, |
| 788 | 'description_is_error' => $get_description_is_error, |
| 789 | 'row_class' => DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION, |
| 790 | ); |
| 791 | } |
| 792 | |
| 793 | /** |
| 794 | * Returns a value indicating if changing the authoritative data source for orders while there are orders pending synchronization is allowed. |
| 795 | * |
| 796 | * @return bool |
| 797 | */ |
| 798 | private function changing_data_source_with_sync_pending_is_allowed(): bool { |
| 799 | /** |
| 800 | * Filter to allow changing where order data is stored, even when there are orders pending synchronization. |
| 801 | * |
| 802 | * DANGER! This filter is intended for usage when doing manual and automated testing in development environments only, |
| 803 | * it should NEVER be used in production environments. Order data corruption or loss can happen! |
| 804 | * |
| 805 | * @param bool $allow True to allow changing order storage when there are orders pending synchronization, false to disallow. |
| 806 | * @returns bool |
| 807 | * |
| 808 | * @since 8.3.0 |
| 809 | */ |
| 810 | return apply_filters( 'wc_allow_changing_orders_storage_while_sync_is_pending', false ); |
| 811 | } |
| 812 | |
| 813 | /** |
| 814 | * Rewrites post edit links for HPOS placeholder posts so that they go to the HPOS order itself. |
| 815 | * Hooked onto `get_edit_post_link`. |
| 816 | * |
| 817 | * @since 9.0.0 |
| 818 | * |
| 819 | * @param string $link The edit link. |
| 820 | * @param int $post_id Post ID. |
| 821 | * @return string |
| 822 | * |
| 823 | * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed. |
| 824 | */ |
| 825 | public function maybe_rewrite_order_edit_link( $link, $post_id ) { |
| 826 | if ( DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE === get_post_type( $post_id ) ) { |
| 827 | $link = OrderUtil::get_order_admin_edit_url( $post_id ); |
| 828 | } |
| 829 | |
| 830 | return $link; |
| 831 | } |
| 832 | |
| 833 | /** |
| 834 | * Set the `order_objects` cache group as non-persistent if Custom Order data caching is enabled. |
| 835 | * |
| 836 | * With order datastore cache enabled, caching of raw data is now handled by the datastore, rather than full object |
| 837 | * being stored in persistent cache. |
| 838 | * |
| 839 | * @return void |
| 840 | */ |
| 841 | public function maybe_set_order_cache_group_as_non_persistent() { |
| 842 | if ( OrderUtil::custom_orders_table_datastore_cache_enabled() ) { |
| 843 | // If we're using datastore cache, we don't want to persist the order objects in cache. It should be in-memory only. |
| 844 | wp_cache_add_non_persistent_groups( array( $this->order_cache->get_object_type() ) ); |
| 845 | } |
| 846 | } |
| 847 | } |
| 848 |