first commit

This commit is contained in:
Rachit Bhargava
2023-07-21 17:12:10 -04:00
parent d0fe47dde4
commit 5d0f0734d8
14003 changed files with 2829464 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\Orders;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* When Custom Order Tables are not the default order store (ie, posts are authoritative), we should take care of
* redirecting requests for the order editor and order admin list table to the equivalent posts-table screens.
*
* If the redirect logic is problematic, it can be unhooked using code like the following example:
*
* remove_action(
* 'admin_page_access_denied',
* array( wc_get_container()->get( COTRedirectionController::class ), 'handle_hpos_admin_requests' )
* );
*/
class COTRedirectionController {
use AccessiblePrivateMethods;
/**
* Add hooks needed to perform our magic.
*/
public function setup(): void {
// Only take action in cases where access to the admin screen would otherwise be denied.
self::add_action( 'admin_page_access_denied', array( $this, 'handle_hpos_admin_requests' ) );
}
/**
* Listen for denied admin requests and, if they appear to relate to HPOS admin screens, potentially
* redirect the user to the equivalent CPT-driven screens.
*
* @param array|null $query_params The query parameters to use when determining the redirect. If not provided, the $_GET superglobal will be used.
*/
private function handle_hpos_admin_requests( $query_params = null ) {
$query_params = is_array( $query_params ) ? $query_params : $_GET;
if ( ! isset( $query_params['page'] ) || 'wc-orders' !== $query_params['page'] ) {
return;
}
$params = wp_unslash( $query_params );
$action = $params['action'] ?? '';
unset( $params['page'] );
if ( 'edit' === $action && isset( $params['id'] ) ) {
$params['post'] = $params['id'];
unset( $params['id'] );
$new_url = add_query_arg( $params, get_admin_url( null, 'post.php' ) );
} elseif ( 'new' === $action ) {
unset( $params['action'] );
$params['post_type'] = 'shop_order';
$new_url = add_query_arg( $params, get_admin_url( null, 'post-new.php' ) );
} else {
// If nonce parameters are present and valid, rebuild them for the CPT admin list table.
if ( isset( $params['_wpnonce'] ) && check_admin_referer( 'bulk-orders' ) ) {
$params['_wp_http_referer'] = get_admin_url( null, 'edit.php?post_type=shop_order' );
$params['_wpnonce'] = wp_create_nonce( 'bulk-posts' );
}
// If an `order` array parameter is present, rename as `post`.
if ( isset( $params['order'] ) && is_array( $params['order'] ) ) {
$params['post'] = $params['order'];
unset( $params['order'] );
}
$params['post_type'] = 'shop_order';
$new_url = add_query_arg( $params, get_admin_url( null, 'edit.php' ) );
}
if ( ! empty( $new_url ) && wp_safe_redirect( $new_url, 301 ) ) {
exit;
}
}
}

View File

@@ -0,0 +1,338 @@
<?php
/**
* Renders order edit page, works with both post and order object.
*/
namespace Automattic\WooCommerce\Internal\Admin\Orders;
use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox;
/**
* Class Edit.
*/
class Edit {
/**
* Screen ID for the edit order screen.
*
* @var string
*/
private $screen_id;
/**
* Instance of the CustomMetaBox class. Used to render meta box for custom meta.
*
* @var CustomMetaBox
*/
private $custom_meta_box;
/**
* Instance of WC_Order to be used in metaboxes.
*
* @var \WC_Order
*/
private $order;
/**
* Action name that the form is currently handling. Could be new_order or edit_order.
*
* @var string
*/
private $current_action;
/**
* Message to be displayed to the user. Index of message from the messages array registered when declaring shop_order post type.
*
* @var int
*/
private $message;
/**
* Hooks all meta-boxes for order edit page. This is static since this may be called by post edit form rendering.
*
* @param string $screen_id Screen ID.
* @param string $title Title of the page.
*/
public static function add_order_meta_boxes( string $screen_id, string $title ) {
/* Translators: %s order type name. */
add_meta_box( 'woocommerce-order-data', sprintf( __( '%s data', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Data::output', $screen_id, 'normal', 'high' );
add_meta_box( 'woocommerce-order-items', __( 'Items', 'woocommerce' ), 'WC_Meta_Box_Order_Items::output', $screen_id, 'normal', 'high' );
/* Translators: %s order type name. */
add_meta_box( 'woocommerce-order-notes', sprintf( __( '%s notes', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Notes::output', $screen_id, 'side', 'default' );
add_meta_box( 'woocommerce-order-downloads', __( 'Downloadable product permissions', 'woocommerce' ) . wc_help_tip( __( 'Note: Permissions for order items will automatically be granted when the order status changes to processing/completed.', 'woocommerce' ) ), 'WC_Meta_Box_Order_Downloads::output', $screen_id, 'normal', 'default' );
/* Translators: %s order type name. */
add_meta_box( 'woocommerce-order-actions', sprintf( __( '%s actions', 'woocommerce' ), $title ), 'WC_Meta_Box_Order_Actions::output', $screen_id, 'side', 'high' );
}
/**
* Hooks metabox save functions for order edit page.
*
* @return void
*/
public static function add_save_meta_boxes() {
/**
* Save Order Meta Boxes.
*
* In order:
* Save the order items.
* Save the order totals.
* Save the order downloads.
* Save order data - also updates status and sends out admin emails if needed. Last to show latest data.
* Save actions - sends out other emails. Last to show latest data.
*/
add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Items::save', 10 );
add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Downloads::save', 30, 2 );
add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Data::save', 40 );
add_action( 'woocommerce_process_shop_order_meta', 'WC_Meta_Box_Order_Actions::save', 50, 2 );
}
/**
* Enqueue necessary scripts for order edit page.
*/
private function enqueue_scripts() {
if ( wp_is_mobile() ) {
wp_enqueue_script( 'jquery-touch-punch' );
}
wp_enqueue_script( 'post' ); // Ensure existing JS libraries are still available for backward compat.
}
/**
* Setup hooks, actions and variables needed to render order edit page.
*
* @param \WC_Order $order Order object.
*/
public function setup( \WC_Order $order ) {
$this->order = $order;
$wc_screen_id = wc_get_page_screen_id( 'shop-order' );
$current_screen = get_current_screen();
$current_screen->is_block_editor( false );
$this->screen_id = $current_screen->id;
if ( ! isset( $this->custom_meta_box ) ) {
$this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class );
}
$this->add_save_meta_boxes();
$this->handle_order_update();
$this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) );
$this->add_order_specific_meta_box();
/**
* From wp-admin/includes/meta-boxes.php.
*
* Fires after all built-in meta boxes have been added. Custom metaboxes may be enqueued here.
*
* @since 3.8.0.
*/
do_action( 'add_meta_boxes', $wc_screen_id, $this->order );
/**
* Provides an opportunity to inject custom meta boxes into the order editor screen. This
* hook is an analog of `add_meta_boxes_<POST_TYPE>` as provided by WordPress core.
*
* @since 7.4.0
*
* @oaram WC_Order $order The order being edited.
*/
do_action( 'add_meta_boxes_' . $wc_screen_id, $this->order );
$this->enqueue_scripts();
}
/**
* Set the current action for the form.
*
* @param string $action Action name.
*/
public function set_current_action( string $action ) {
$this->current_action = $action;
}
/**
* Hooks meta box for order specific meta.
*/
private function add_order_specific_meta_box() {
add_meta_box(
'order_custom',
__( 'Custom Fields', 'woocommerce' ),
array( $this, 'render_custom_meta_box' ),
$this->screen_id,
'normal'
);
}
/**
* Takes care of updating order data. Fires action that metaboxes can hook to for order data updating.
*
* @return void
*/
public function handle_order_update() {
global $theorder;
if ( ! isset( $this->order ) ) {
return;
}
if ( 'edit_order' !== sanitize_text_field( wp_unslash( $_POST['action'] ?? '' ) ) ) {
return;
}
check_admin_referer( $this->get_order_edit_nonce_action() );
/**
* Save meta for shop order.
*
* @param int Order ID.
* @param \WC_Order Post object.
*
* @since 2.1.0
*/
do_action( 'woocommerce_process_shop_order_meta', $this->order->get_id(), $this->order );
// Order updated message.
$this->message = 1;
// Refresh the order from DB.
$this->order = wc_get_order( $this->order->get_id() );
$theorder = $this->order;
}
/**
* Helper method to get the name of order edit nonce.
*
* @return string Nonce action name.
*/
private function get_order_edit_nonce_action() {
return 'update-order_' . $this->order->get_id();
}
/**
* Render meta box for order specific meta.
*/
public function render_custom_meta_box() {
$this->custom_meta_box->output( $this->order );
}
/**
* Render order edit page.
*/
public function display() {
/**
* This is used by the order edit page to show messages in the notice fields.
* It should be similar to post_updated_messages filter, i.e.:
* array(
* {order_type} => array(
* 1 => 'Order updated.',
* 2 => 'Custom field updated.',
* ...
* ).
*
* The index to be displayed is computed from the $_GET['message'] variable.
*
* @since 7.4.0.
*/
$messages = apply_filters( 'woocommerce_order_updated_messages', array() );
$message = $this->message;
if ( isset( $_GET['message'] ) ) {
$message = absint( $_GET['message'] );
}
if ( isset( $message ) ) {
$message = $messages[ $this->order->get_type() ][ $message ] ?? false;
}
$this->render_wrapper_start( '', $message );
$this->render_meta_boxes();
$this->render_wrapper_end();
}
/**
* Helper function to render wrapper start.
*
* @param string $notice Notice to display, if any.
* @param string $message Message to display, if any.
*/
private function render_wrapper_start( $notice = '', $message = '' ) {
$post_type = get_post_type_object( $this->order->get_type() );
$edit_page_url = wc_get_container()->get( PageController::class )->get_edit_url( $this->order->get_id() );
$form_action = 'edit_order';
$referer = wp_get_referer();
$new_page_url = wc_get_container()->get( PageController::class )->get_new_page_url( $this->order->get_type() );
?>
<div class="wrap">
<h1 class="wp-heading-inline">
<?php
echo 'new_order' === $this->current_action ? esc_html( $post_type->labels->add_new_item ) : esc_html( $post_type->labels->edit_item );
?>
</h1>
<?php
if ( 'edit_order' === $this->current_action ) {
echo ' <a href="' . esc_url( $new_page_url ) . '" class="page-title-action">' . esc_html( $post_type->labels->add_new ) . '</a>';
}
?>
<hr class="wp-header-end">
<?php
if ( $notice ) :
?>
<div id="notice" class="notice notice-warning"><p
id="has-newer-autosave"><?php echo wp_kses_post( $notice ); ?></p></div>
<?php endif; ?>
<?php if ( $message ) : ?>
<div id="message" class="updated notice notice-success is-dismissible">
<p><?php echo wp_kses_post( $message ); ?></p></div>
<?php
endif;
?>
<form name="order" action="<?php echo esc_url( $edit_page_url ); ?>" method="post" id="order"
<?php
/**
* Fires inside the order edit form tag.
*
* @param \WC_Order $order Order object.
*
* @since 6.9.0
*/
do_action( 'order_edit_form_tag', $this->order );
?>
>
<?php wp_nonce_field( $this->get_order_edit_nonce_action() ); ?>
<input type="hidden" id="hiddenaction" name="action" value="<?php echo esc_attr( $form_action ); ?>"/>
<input type="hidden" id="original_order_status" name="original_order_status" value="<?php echo esc_attr( $this->order->get_status() ); ?>"/>
<input type="hidden" id="referredby" name="referredby" value="<?php echo $referer ? esc_url( $referer ) : ''; ?>"/>
<div id="poststuff">
<div id="post-body"
class="metabox-holder columns-<?php echo ( 1 === get_current_screen()->get_columns() ) ? '1' : '2'; ?>">
<?php
}
/**
* Helper function to render meta boxes.
*/
private function render_meta_boxes() {
?>
<div id="postbox-container-1" class="postbox-container">
<?php do_meta_boxes( $this->screen_id, 'side', $this->order ); ?>
</div>
<div id="postbox-container-2" class="postbox-container">
<?php
do_meta_boxes( $this->screen_id, 'normal', $this->order );
do_meta_boxes( $this->screen_id, 'advanced', $this->order );
?>
</div>
<?php
}
/**
* Helper function to render wrapper end.
*/
private function render_wrapper_end() {
?>
</div> <!-- /post-body -->
</div> <!-- /poststuff -->
</form>
</div> <!-- /wrap -->
<?php
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,418 @@
<?php
/**
* Meta box to edit and add custom meta values for an order.
*/
namespace Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes;
use WC_Data_Store;
use WC_Meta_Data;
use WC_Order;
use WP_Ajax_Response;
/**
* Class CustomMetaBox.
*/
class CustomMetaBox {
/**
* Update nonce shared among different meta rows.
*
* @var string
*/
private $update_nonce;
/**
* Helper method to get formatted meta data array with proper keys. This can be directly fed to `list_meta()` method.
*
* @param \WC_Order $order Order object.
*
* @return array Meta data.
*/
private function get_formatted_order_meta_data( \WC_Order $order ) {
$metadata = $order->get_meta_data();
$metadata_to_list = array();
foreach ( $metadata as $meta ) {
$data = $meta->get_data();
if ( is_protected_meta( $data['key'], 'order' ) ) {
continue;
}
$metadata_to_list[] = array(
'meta_id' => $data['id'],
'meta_key' => $data['key'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- False positive, not a meta query.
'meta_value' => maybe_serialize( $data['value'] ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- False positive, not a meta query.
);
}
return $metadata_to_list;
}
/**
* Renders the meta box to manage custom meta.
*
* @param \WP_Post|\WC_Order $order_or_post Post or order object that we are rendering for.
*/
public function output( $order_or_post ) {
if ( is_a( $order_or_post, \WP_Post::class ) ) {
$order = wc_get_order( $order_or_post );
} else {
$order = $order_or_post;
}
$this->render_custom_meta_form( $this->get_formatted_order_meta_data( $order ), $order );
}
/**
* Helper method to render layout and actual HTML
*
* @param array $metadata_to_list List of metadata to render.
* @param \WC_Order $order Order object.
*/
private function render_custom_meta_form( array $metadata_to_list, \WC_Order $order ) {
?>
<div id="postcustomstuff">
<div id="ajax-response"></div>
<?php
list_meta( $metadata_to_list );
$this->render_meta_form( $order );
?>
</div>
<p>
<?php
printf(
/* translators: 1: opening documentation tag 2: closing documentation tag. */
esc_html( __( 'Custom fields can be used to add extra metadata to an order that you can %1$suse in your theme%2$s.', 'woocommerce' ) ),
'<a href="' . esc_attr__( 'https://wordpress.org/support/article/custom-fields/', 'woocommerce' ) . '">',
'</a>'
);
?>
</p>
<?php
}
/**
* Compute keys to display in autofill when adding new meta key entry in custom meta box.
* Currently, returns empty keys, will be implemented after caching is merged.
*
* @param array|null $keys Keys to display in autofill.
* @param \WP_Post|\WC_Order $order Order object.
*
* @return array|mixed Array of keys to display in autofill.
*/
public function order_meta_keys_autofill( $keys, $order ) {
if ( is_a( $order, \WC_Order::class ) ) {
return array();
}
return $keys;
}
/**
* Reimplementation of WP core's `meta_form` function. Renders meta form box.
*
* @param \WC_Order $order WC_Order object.
*
* @return void
*/
public function render_meta_form( \WC_Order $order ) : void {
$meta_key_input_id = 'metakeyselect';
$keys = $this->order_meta_keys_autofill( null, $order );
/**
* Filters values for the meta key dropdown in the Custom Fields meta box.
*
* Compatibility filter for `postmeta_form_keys` filter.
*
* @since 6.9.0
*
* @param array|null $keys Pre-defined meta keys to be used in place of a postmeta query. Default null.
* @param \WC_Order $order The current post object.
*/
$keys = apply_filters( 'postmeta_form_keys', $keys, $order );
?>
<p><strong><?php esc_html_e( 'Add New Custom Field:', 'woocommerce' ); ?></strong></p>
<table id="newmeta">
<thead>
<tr>
<th class="left"><label for="<?php echo esc_attr( $meta_key_input_id ); ?>"><?php esc_html_e( 'Name', 'woocommerce' ); ?></label></th>
<th><label for="metavalue"><?php esc_html_e( 'Value', 'woocommerce' ); ?></label></th>
</tr>
</thead>
<tbody>
<tr>
<td id="newmetaleft" class="left">
<?php if ( $keys ) { ?>
<select id="metakeyselect" name="metakeyselect">
<option value="#NONE#"><?php esc_html_e( '&mdash; Select &mdash;', 'woocommerce' ); ?></option>
<?php
foreach ( $keys as $key ) {
if ( is_protected_meta( $key, 'post' ) || ! current_user_can( 'edit_others_shop_order', $order->get_id() ) ) {
continue;
}
echo "\n<option value='" . esc_attr( $key ) . "'>" . esc_html( $key ) . '</option>';
}
?>
</select>
<input class="hide-if-js" type="text" id="metakeyinput" name="metakeyinput" value="" />
<a href="#postcustomstuff" class="hide-if-no-js" onclick="jQuery('#metakeyinput, #metakeyselect, #enternew, #cancelnew').toggle();return false;">
<span id="enternew"><?php esc_html_e( 'Enter new', 'woocommerce' ); ?></span>
<span id="cancelnew" class="hidden"><?php esc_html_e( 'Cancel', 'woocommerce' ); ?></span></a>
<?php } else { ?>
<input type="text" id="metakeyinput" name="metakeyinput" value="" />
<?php } ?>
</td>
<td><textarea id="metavalue" name="metavalue" rows="2" cols="25"></textarea></td>
</tr>
<tr><td colspan="2">
<div class="submit">
<?php
submit_button(
__( 'Add Custom Field', 'woocommerce' ),
'',
'addmeta',
false,
array(
'id' => 'newmeta-submit',
'data-wp-lists' => 'add:the-list:newmeta',
)
);
?>
</div>
<?php wp_nonce_field( 'add-meta', '_ajax_nonce-add-meta', false ); ?>
</td></tr>
</tbody>
</table>
<?php
}
/**
* Helper method to verify order edit permissions.
*
* @param int $order_id Order ID.
*
* @return ?WC_Order WC_Order object if the user can edit the order, die otherwise.
*/
private function verify_order_edit_permission_for_ajax( int $order_id ): ?WC_Order {
if ( ! current_user_can( 'manage_woocommerce' ) || ! current_user_can( 'edit_others_shop_orders' ) ) {
wp_send_json_error( 'missing_capabilities' );
wp_die();
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
wp_send_json_error( 'invalid_order_id' );
wp_die();
}
return $order;
}
/**
* Reimplementation of WP core's `wp_ajax_add_meta` method to support order custom meta updates with custom tables.
*/
public function add_meta_ajax() {
if ( ! check_ajax_referer( 'add-meta', '_ajax_nonce-add-meta' ) ) {
wp_send_json_error( 'invalid_nonce' );
wp_die();
}
$order_id = (int) $_POST['order_id'] ?? 0;
$order = $this->verify_order_edit_permission_for_ajax( $order_id );
if ( isset( $_POST['metakeyselect'] ) && '#NONE#' === $_POST['metakeyselect'] && empty( $_POST['metakeyinput'] ) ) {
wp_die( 1 );
}
if ( isset( $_POST['metakeyinput'] ) ) { // add meta.
$meta_key = sanitize_text_field( wp_unslash( $_POST['metakeyinput'] ) );
$meta_value = sanitize_text_field( wp_unslash( $_POST['metavalue'] ?? '' ) );
$this->handle_add_meta( $order, $meta_key, $meta_value );
} else { // update.
$meta = wp_unslash( $_POST['meta'] ?? array() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitization done below in array_walk.
$this->handle_update_meta( $order, $meta );
}
}
/**
* Part of WP Core's `wp_ajax_add_meta`. This is re-implemented to support updating meta for custom tables.
*
* @param WC_Order $order Order object.
* @param string $meta_key Meta key.
* @param string $meta_value Meta value.
*
* @return void
*/
private function handle_add_meta( WC_Order $order, string $meta_key, string $meta_value ) {
$order_data_store = WC_Data_Store::load( 'order' );
$count = 0;
if ( is_protected_meta( $meta_key ) ) {
wp_send_json_error( 'protected_meta' );
wp_die();
}
$meta_id = $order_data_store->add_meta(
$order,
new WC_Meta_Data(
array(
'key' => $meta_key,
'value' => $meta_value,
)
)
);
$response = new WP_Ajax_Response(
array(
'what' => 'meta',
'id' => $meta_id,
'data' => $this->list_meta_row(
array(
'meta_id' => $meta_id,
'meta_key' => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query.
'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
),
$count
),
'position' => 1,
)
);
$response->send();
}
/**
* Handles updating metadata.
*
* @param WC_Order $order Order object.
* @param array $meta Meta object to update.
*
* @return void
*/
private function handle_update_meta( WC_Order $order, array $meta ) {
if ( ! is_array( $meta ) ) {
wp_send_json_error( 'invalid_meta' );
wp_die();
}
array_walk( $meta, 'sanitize_text_field' );
$mid = (int) key( $meta );
if ( ! $mid ) {
wp_send_json_error( 'invalid_meta_id' );
wp_die();
}
$key = $meta[ $mid ]['key'];
$value = $meta[ $mid ]['value'];
if ( is_protected_meta( $key ) ) {
wp_send_json_error( 'protected_meta' );
wp_die();
}
if ( '' === trim( $key ) ) {
wp_send_json_error( 'invalid_meta_key' );
wp_die();
}
$order_data_store = WC_Data_Store::load( 'order' );
$count = 0;
$meta_object = new WC_Meta_Data(
array(
'id' => $mid,
'key' => $key,
'value' => $value,
)
);
$order_data_store->update_meta( $order, $meta_object );
$response = new WP_Ajax_Response(
array(
'what' => 'meta',
'id' => $mid,
'old_id' => $mid,
'data' => $this->list_meta_row(
array(
'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query.
'meta_value' => $value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
'meta_id' => $mid,
),
$count
),
'position' => 0,
)
);
$response->send();
}
/**
* Outputs a single row of public meta data in the Custom Fields meta box.
*
* @since 2.5.0
*
* @param array $entry Meta entry.
* @param int $count Sequence number of meta entries.
* @return string
*/
private function list_meta_row( array $entry, int &$count ) : string {
if ( is_protected_meta( $entry['meta_key'], 'post' ) ) {
return '';
}
if ( ! $this->update_nonce ) {
$this->update_nonce = wp_create_nonce( 'add-meta' );
}
$r = '';
++ $count;
if ( is_serialized( $entry['meta_value'] ) ) {
if ( is_serialized_string( $entry['meta_value'] ) ) {
// This is a serialized string, so we should display it.
$entry['meta_value'] = maybe_unserialize( $entry['meta_value'] ); // // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
} else {
// This is a serialized array/object so we should NOT display it.
--$count;
return '';
}
}
$entry['meta_key'] = esc_attr( $entry['meta_key'] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- false positive, not a meta query.
$entry['meta_value'] = esc_textarea( $entry['meta_value'] ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- false positive, not a meta query.
$entry['meta_id'] = (int) $entry['meta_id'];
$delete_nonce = wp_create_nonce( 'delete-meta_' . $entry['meta_id'] );
$r .= "\n\t<tr id='meta-{$entry['meta_id']}'>";
$r .= "\n\t\t<td class='left'><label class='screen-reader-text' for='meta-{$entry['meta_id']}-key'>" . __( 'Key', 'woocommerce' ) . "</label><input name='meta[{$entry['meta_id']}][key]' id='meta-{$entry['meta_id']}-key' type='text' size='20' value='{$entry['meta_key']}' />";
$r .= "\n\t\t<div class='submit'>";
$r .= get_submit_button( __( 'Delete', 'woocommerce' ), 'deletemeta small', "deletemeta[{$entry['meta_id']}]", false, array( 'data-wp-lists' => "delete:the-list:meta-{$entry['meta_id']}::_ajax_nonce:$delete_nonce" ) );
$r .= "\n\t\t";
$r .= get_submit_button( __( 'Update', 'woocommerce' ), 'updatemeta small', "meta-{$entry['meta_id']}-submit", false, array( 'data-wp-lists' => "add:the-list:meta-{$entry['meta_id']}::_ajax_nonce-add-meta={$this->update_nonce}" ) );
$r .= '</div>';
$r .= wp_nonce_field( 'change-meta', '_ajax_nonce', false, false );
$r .= '</td>';
$r .= "\n\t\t<td><label class='screen-reader-text' for='meta-{$entry['meta_id']}-value'>" . __( 'Value', 'woocommerce' ) . "</label><textarea name='meta[{$entry['meta_id']}][value]' id='meta-{$entry['meta_id']}-value' rows='2' cols='30'>{$entry['meta_value']}</textarea></td>\n\t</tr>";
return $r;
}
/**
* Reimplementation of WP core's `wp_ajax_delete_meta` method to support order custom meta updates with custom tables.
*
* @return void
*/
public function delete_meta_ajax() {
$meta_id = (int) $_POST['id'] ?? 0;
$order_id = (int) $_POST['order_id'] ?? 0;
if ( ! $meta_id || ! $order_id ) {
wp_send_json_error( 'invalid_meta_id' );
wp_die();
}
check_ajax_referer( "delete-meta_$meta_id" );
$order = $this->verify_order_edit_permission_for_ajax( $order_id );
$meta_to_delete = wp_list_filter( $order->get_meta_data(), array( 'id' => $meta_id ) );
if ( empty( $meta_to_delete ) ) {
wp_send_json_error( 'invalid_meta_id' );
wp_die();
}
$order->delete_meta_data_by_mid( $meta_id );
if ( $order->save() ) {
wp_die( 1 );
}
wp_die( 0 );
}
}

View File

@@ -0,0 +1,387 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\Orders;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* Controls the different pages/screens associated to the "Orders" menu page.
*/
class PageController {
use AccessiblePrivateMethods;
/**
* The order type.
*
* @var string
*/
private $order_type = '';
/**
* Instance of the posts redirection controller.
*
* @var PostsRedirectionController
*/
private $redirection_controller;
/**
* Instance of the orders list table.
*
* @var ListTable
*/
private $orders_table;
/**
* Instance of orders edit form.
*
* @var Edit
*/
private $order_edit_form;
/**
* Current action.
*
* @var string
*/
private $current_action = '';
/**
* Order object to be used in edit/new form.
*
* @var \WC_Order
*/
private $order;
/**
* Verify that user has permission to edit orders.
*
* @return void
*/
private function verify_edit_permission() {
if ( 'edit_order' === $this->current_action && ( ! isset( $this->order ) || ! $this->order ) ) {
wp_die( esc_html__( 'You attempted to edit an order that does not exist. Perhaps it was deleted?', 'woocommerce' ) );
}
if ( $this->order->get_type() !== $this->order_type ) {
wp_die( esc_html__( 'Order type mismatch.', 'woocommerce' ) );
}
if ( ! current_user_can( get_post_type_object( $this->order_type )->cap->edit_post, $this->order->get_id() ) && ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You do not have permission to edit this order', 'woocommerce' ) );
}
if ( 'trash' === $this->order->get_status() ) {
wp_die( esc_html__( 'You cannot edit this item because it is in the Trash. Please restore it and try again.', 'woocommerce' ) );
}
}
/**
* Verify that user has permission to create order.
*
* @return void
*/
private function verify_create_permission() {
if ( ! current_user_can( get_post_type_object( $this->order_type )->cap->publish_posts ) && ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( esc_html__( 'You don\'t have permission to create a new order', 'woocommerce' ) );
}
if ( isset( $this->order ) ) {
$this->verify_edit_permission();
}
}
/**
* Sets up the page controller, including registering the menu item.
*
* @return void
*/
public function setup(): void {
$this->redirection_controller = new PostsRedirectionController( $this );
// Register menu.
if ( 'admin_menu' === current_action() ) {
$this->register_menu();
} else {
add_action( 'admin_menu', 'register_menu', 9 );
}
$this->set_order_type();
$this->set_action();
$page_suffix = ( 'shop_order' === $this->order_type ? '' : '--' . $this->order_type );
self::add_action( 'load-woocommerce_page_wc-orders' . $page_suffix, array( $this, 'handle_load_page_action' ) );
}
/**
* Perform initialization for the current action.
*/
private function handle_load_page_action() {
if ( method_exists( $this, 'setup_action_' . $this->current_action ) ) {
$this->{"setup_action_{$this->current_action}"}();
}
}
/**
* Determines the order type for the current screen.
*
* @return void
*/
private function set_order_type() {
global $plugin_page, $pagenow;
if ( 'admin.php' !== $pagenow || 0 !== strpos( $plugin_page, 'wc-orders' ) ) {
return;
}
$this->order_type = str_replace( array( 'wc-orders--', 'wc-orders' ), '', $plugin_page );
$this->order_type = empty( $this->order_type ) ? 'shop_order' : $this->order_type;
$wc_order_type = wc_get_order_type( $this->order_type );
$wp_order_type = get_post_type_object( $this->order_type );
if ( ! $wc_order_type || ! $wp_order_type || ! $wp_order_type->show_ui || ! current_user_can( $wp_order_type->cap->edit_posts ) ) {
wp_die();
}
}
/**
* Sets the current action based on querystring arguments. Defaults to 'list_orders'.
*
* @return void
*/
private function set_action(): void {
switch ( isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : '' ) {
case 'edit':
$this->current_action = 'edit_order';
break;
case 'new':
$this->current_action = 'new_order';
break;
default:
$this->current_action = 'list_orders';
break;
}
}
/**
* Registers the "Orders" menu.
*
* @return void
*/
public function register_menu(): void {
$order_types = wc_get_order_types( 'admin-menu' );
foreach ( $order_types as $order_type ) {
$post_type = get_post_type_object( $order_type );
add_submenu_page(
'woocommerce',
$post_type->labels->name,
$post_type->labels->menu_name,
$post_type->cap->edit_posts,
'wc-orders' . ( 'shop_order' === $order_type ? '' : '--' . $order_type ),
array( $this, 'output' )
);
}
// In some cases (such as if the authoritative order store was changed earlier in the current request) we
// need an extra step to remove the menu entry for the menu post type.
add_action(
'admin_init',
function() use ( $order_types ) {
foreach ( $order_types as $order_type ) {
remove_submenu_page( 'woocommerce', 'edit.php?post_type=' . $order_type );
}
}
);
}
/**
* Outputs content for the current orders screen.
*
* @return void
*/
public function output(): void {
switch ( $this->current_action ) {
case 'edit_order':
case 'new_order':
if ( ! isset( $this->order_edit_form ) ) {
$this->order_edit_form = new Edit();
$this->order_edit_form->setup( $this->order );
}
$this->order_edit_form->set_current_action( $this->current_action );
$this->order_edit_form->display();
break;
case 'list_orders':
default:
$this->orders_table->prepare_items();
$this->orders_table->display();
break;
}
}
/**
* Handles initialization of the orders list table.
*
* @return void
*/
private function setup_action_list_orders(): void {
$this->orders_table = wc_get_container()->get( ListTable::class );
$this->orders_table->setup(
array(
'order_type' => $this->order_type,
)
);
if ( $this->orders_table->current_action() ) {
$this->orders_table->handle_bulk_actions();
}
$this->strip_http_referer();
}
/**
* Perform a redirect to remove the `_wp_http_referer` and `_wpnonce` strings if present in the URL (see also
* wp-admin/edit.php where a similar process takes place), otherwise the size of this field builds to an
* unmanageable length over time.
*/
private function strip_http_referer(): void {
$current_url = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) );
$stripped_url = remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), $current_url );
if ( $stripped_url !== $current_url ) {
wp_safe_redirect( $stripped_url );
exit;
}
}
/**
* Handles initialization of the orders edit form.
*
* @return void
*/
private function setup_action_edit_order(): void {
global $theorder;
$this->order = wc_get_order( absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 ) );
$this->verify_edit_permission();
$theorder = $this->order;
}
/**
* Handles initialization of the orders edit form with a new order.
*
* @return void
*/
private function setup_action_new_order(): void {
global $theorder;
$this->verify_create_permission();
$order_class_name = wc_get_order_type( $this->order_type )['class_name'];
if ( ! $order_class_name || ! class_exists( $order_class_name ) ) {
wp_die();
}
$this->order = new $order_class_name();
$this->order->set_object_read( false );
$this->order->set_status( 'pending' );
$this->order->save();
$theorder = $this->order;
}
/**
* Returns the current order type.
*
* @return string
*/
public function get_order_type() {
return $this->order_type;
}
/**
* Helper method to generate a link to the main orders screen.
*
* @return string Orders screen URL.
*/
public function get_orders_url(): string {
return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ?
admin_url( 'admin.php?page=wc-orders' ) :
admin_url( 'edit.php?post_type=shop_order' );
}
/**
* Helper method to generate edit link for an order.
*
* @param int $order_id Order ID.
*
* @return string Edit link.
*/
public function get_edit_url( int $order_id ) : string {
if ( ! wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
return admin_url( 'post.php?post=' . absint( $order_id ) ) . '&action=edit';
}
$order = wc_get_order( $order_id );
// Confirm we could obtain the order object (since it's possible it will not exist, due to a sync issue, or may
// have been deleted in a separate concurrent request).
if ( false === $order ) {
wc_get_logger()->debug(
sprintf(
/* translators: %d order ID. */
__( 'Attempted to determine the edit URL for order %d, however the order does not exist.', 'woocommerce' ),
$order_id
)
);
$order_type = 'shop_order';
} else {
$order_type = $order->get_type();
}
return add_query_arg(
array(
'action' => 'edit',
'id' => absint( $order_id ),
),
$this->get_base_page_url( $order_type )
);
}
/**
* Helper method to generate a link for creating order.
*
* @param string $order_type The order type. Defaults to 'shop_order'.
* @return string
*/
public function get_new_page_url( $order_type = 'shop_order' ) : string {
$url = wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ?
add_query_arg( 'action', 'new', $this->get_base_page_url( $order_type ) ) :
admin_url( 'post-new.php?post_type=' . $order_type );
return $url;
}
/**
* Helper method to generate a link to the main screen for a custom order type.
*
* @param string $order_type The order type.
*
* @return string
*
* @throws \Exception When an invalid order type is passed.
*/
public function get_base_page_url( $order_type ): string {
$order_types_with_ui = wc_get_order_types( 'admin-menu' );
if ( ! in_array( $order_type, $order_types_with_ui, true ) ) {
// translators: %s is a custom order type.
throw new \Exception( sprintf( __( 'Invalid order type: %s.', 'woocommerce' ), esc_html( $order_type ) ) );
}
return admin_url( 'admin.php?page=wc-orders' . ( 'shop_order' === $order_type ? '' : '--' . $order_type ) );
}
}

View File

@@ -0,0 +1,182 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\Orders;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
/**
* When {@see OrdersTableDataStore} is in use, this class takes care of redirecting admins from CPT-based URLs
* to the new ones.
*/
class PostsRedirectionController {
/**
* Instance of the PageController class.
*
* @var PageController
*/
private $page_controller;
/**
* Constructor.
*
* @param PageController $page_controller Page controller instance. Used to generate links/URLs.
*/
public function __construct( PageController $page_controller ) {
$this->page_controller = $page_controller;
if ( ! wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) {
return;
}
add_action(
'load-edit.php',
function() {
$this->maybe_redirect_to_orders_page();
}
);
add_action(
'load-post-new.php',
function() {
$this->maybe_redirect_to_new_order_page();
}
);
add_action(
'load-post.php',
function() {
$this->maybe_redirect_to_edit_order_page();
}
);
}
/**
* If needed, performs a redirection to the main orders page.
*
* @return void
*/
private function maybe_redirect_to_orders_page(): void {
$post_type = $_GET['post_type'] ?? ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( ! $post_type || ! in_array( $post_type, wc_get_order_types( 'admin-menu' ), true ) ) {
return;
}
// Respect query args, except for 'post_type'.
$query_args = wp_unslash( $_GET );
$action = $query_args['action'] ?? '';
$posts = $query_args['post'] ?? array();
unset( $query_args['post_type'], $query_args['post'], $query_args['_wpnonce'], $query_args['_wp_http_referer'], $query_args['action'] );
// Remap 'post_status' arg.
if ( isset( $query_args['post_status'] ) ) {
$query_args['status'] = $query_args['post_status'];
unset( $query_args['post_status'] );
}
$new_url = $this->page_controller->get_base_page_url( $post_type );
$new_url = add_query_arg( $query_args, $new_url );
// Handle bulk actions.
if ( $action && in_array( $action, array( 'trash', 'untrash', 'delete', 'mark_processing', 'mark_on-hold', 'mark_completed', 'mark_cancelled' ), true ) ) {
check_admin_referer( 'bulk-posts' );
$new_url = add_query_arg(
array(
'action' => $action,
'order' => $posts,
'_wp_http_referer' => $this->page_controller->get_orders_url(),
'_wpnonce' => wp_create_nonce( 'bulk-orders' ),
),
$new_url
);
}
wp_safe_redirect( $new_url, 301 );
exit;
}
/**
* If needed, performs a redirection to the new order page.
*
* @return void
*/
private function maybe_redirect_to_new_order_page(): void {
$post_type = $_GET['post_type'] ?? ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( ! $post_type || ! in_array( $post_type, wc_get_order_types( 'admin-menu' ), true ) ) {
return;
}
// Respect query args, except for 'post_type'.
$query_args = wp_unslash( $_GET );
unset( $query_args['post_type'] );
$new_url = $this->page_controller->get_new_page_url( $post_type );
$new_url = add_query_arg( $query_args, $new_url );
wp_safe_redirect( $new_url, 301 );
exit;
}
/**
* If needed, performs a redirection to the edit order page.
*
* @return void
*/
private function maybe_redirect_to_edit_order_page(): void {
$post_id = absint( $_GET['post'] ?? 0 );
$redirect_from_types = wc_get_order_types( 'admin-menu' );
$redirect_from_types[] = 'shop_order_placehold';
if ( ! $post_id || ! in_array( get_post_type( $post_id ), $redirect_from_types, true ) || ! isset( $_GET['action'] ) ) {
return;
}
// Respect query args, except for 'post'.
$query_args = wp_unslash( $_GET );
$action = $query_args['action'];
unset( $query_args['post'], $query_args['_wpnonce'], $query_args['_wp_http_referer'], $query_args['action'] );
$new_url = '';
switch ( $action ) {
case 'edit':
$new_url = $this->page_controller->get_edit_url( $post_id );
break;
case 'trash':
case 'untrash':
case 'delete':
// Re-generate nonce if validation passes.
check_admin_referer( $action . '-post_' . $post_id );
$new_url = add_query_arg(
array(
'action' => $action,
'order' => array( $post_id ),
'_wp_http_referer' => $this->page_controller->get_orders_url(),
'_wpnonce' => wp_create_nonce( 'bulk-orders' ),
),
$this->page_controller->get_orders_url()
);
break;
default:
break;
}
if ( ! $new_url ) {
return;
}
$new_url = add_query_arg( $query_args, $new_url );
wp_safe_redirect( $new_url, 301 );
exit;
}
}