> */ public function data() { return array_merge( parent::data(), array( Cleanup::record( 'addresses', __( 'Addresses saved to customer profiles', 'admin-powerups-for-woocommerce' ), __( 'They are the customers’ own addresses now, so they stay.', 'admin-powerups-for-woocommerce' ) ) ) ); } /** * {@inheritDoc} * * @return array> */ public function fields() { $statuses = array( '' => __( 'Never', 'admin-powerups-for-woocommerce' ) ); foreach ( wc_get_order_statuses() as $key => $label ) { $statuses[ substr( $key, 3 ) ] = $label; } return array( array( 'id' => 'show_buttons', 'type' => 'toggle', 'label' => __( 'Show save buttons on orders', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Adds a button under each address.', 'admin-powerups-for-woocommerce' ), 'default' => true, ), array( 'id' => 'auto_status', 'type' => 'select', 'label' => __( 'Also save automatically when an order is', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Handy for orders you create for customers.', 'admin-powerups-for-woocommerce' ), 'default' => '', 'options' => $statuses, ), array( 'id' => 'auto_types', 'type' => 'checkboxes', 'label' => __( 'Addresses to save', 'admin-powerups-for-woocommerce' ), 'default' => array( 'billing', 'shipping' ), 'options' => array( 'billing' => __( 'Billing', 'admin-powerups-for-woocommerce' ), 'shipping' => __( 'Shipping', 'admin-powerups-for-woocommerce' ), ), ), array( 'id' => 'skip_empty', 'type' => 'toggle', 'label' => __( 'Skip empty fields', 'admin-powerups-for-woocommerce' ), 'description' => __( 'Keeps what the profile already has instead of clearing it.', 'admin-powerups-for-woocommerce' ), 'default' => false, ), ); } /** * {@inheritDoc} */ public function boot() { if ( $this->setting( 'show_buttons' ) ) { add_action( 'woocommerce_admin_order_data_after_billing_address', array( $this, 'render_billing_button' ) ); add_action( 'woocommerce_admin_order_data_after_shipping_address', array( $this, 'render_shipping_button' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'ajax_sync' ) ); } $status = (string) $this->setting( 'auto_status' ); if ( '' !== $status ) { add_action( 'woocommerce_order_status_' . $status, array( $this, 'auto_sync' ), 20, 2 ); } } /** * Billing button. * * @since 1.0.0 * @param WC_Order $order Order. * @return void */ public function render_billing_button( $order ) { $this->render_button( $order, 'billing' ); } /** * Shipping button. * * @since 1.0.0 * @param WC_Order $order Order. * @return void */ public function render_shipping_button( $order ) { $this->render_button( $order, 'shipping' ); } /** * Render a sync button when the order has a linked customer the current user may edit. * * On multisite, site admins and shop managers usually can't edit customer accounts * (`edit_user`), so they get no button rather than one that always fails. * * @since 1.0.0 * @param WC_Order $order Order. * @param string $type billing|shipping. * @return void */ private function render_button( $order, $type ) { if ( ! $order instanceof WC_Order || ! $order->get_customer_id() || ! current_user_can( 'edit_user', $order->get_customer_id() ) ) { return; } ?>
admin_url( 'admin-ajax.php' ), 'action' => self::AJAX_ACTION, 'nonce' => wp_create_nonce( self::AJAX_ACTION ), 'i18n' => array( 'working' => __( 'Saving…', 'admin-powerups-for-woocommerce' ), 'done' => __( 'Saved', 'admin-powerups-for-woocommerce' ), 'error' => __( 'Couldn’t save. Try again.', 'admin-powerups-for-woocommerce' ), 'unsaved' => __( 'Update the order first, then save.', 'admin-powerups-for-woocommerce' ), ), ) ); } /** * AJAX handler. * * @since 1.0.0 * @return void */ public function ajax_sync() { check_ajax_referer( self::AJAX_ACTION, 'nonce' ); $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0; $type = isset( $_POST['type'] ) ? sanitize_key( wp_unslash( $_POST['type'] ) ) : ''; if ( ! $order_id || ! current_user_can( 'edit_shop_orders' ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to edit this order.', 'admin-powerups-for-woocommerce' ) ), 403 ); } if ( ! in_array( $type, array( 'billing', 'shipping' ), true ) ) { wp_send_json_error( array( 'message' => __( 'Unknown address type.', 'admin-powerups-for-woocommerce' ) ), 400 ); } $order = wc_get_order( $order_id ); if ( ! $order instanceof WC_Order ) { wp_send_json_error( array( 'message' => __( 'Order not found.', 'admin-powerups-for-woocommerce' ) ), 404 ); } $user_id = $order->get_customer_id(); if ( ! $user_id ) { wp_send_json_error( array( 'message' => __( 'This order has no customer account.', 'admin-powerups-for-woocommerce' ) ), 400 ); } if ( ! current_user_can( 'edit_user', $user_id ) ) { wp_send_json_error( array( 'message' => __( 'You are not allowed to edit this customer.', 'admin-powerups-for-woocommerce' ) ), 403 ); } try { $written = CustomerAddress::copy_from_order( $order, $user_id, $type, (bool) $this->setting( 'skip_empty' ) ); } catch ( \Exception $e ) { wp_send_json_error( array( 'message' => $e->getMessage() ), 500 ); } $order->add_order_note( sprintf( /* translators: 1: address type, 2: user ID. */ __( '%1$s address saved to the customer profile (user #%2$d).', 'admin-powerups-for-woocommerce' ), 'billing' === $type ? __( 'Billing', 'admin-powerups-for-woocommerce' ) : __( 'Shipping', 'admin-powerups-for-woocommerce' ), $user_id ), 0, true ); wp_send_json_success( array( 'fields' => $written ) ); } /** * Sync automatically on the configured status. * * Runs on the status change itself (checkout, payment webhooks, cron, staff), so the target * account is checked with may_auto_sync() instead of a capability alone. * * @since 1.0.0 * @param int $order_id Order ID. * @param WC_Order $order Order. * @return void */ public function auto_sync( $order_id, $order = null ) { $order = $order instanceof WC_Order ? $order : wc_get_order( $order_id ); if ( ! $order instanceof WC_Order || ! $order->get_customer_id() || ! $this->may_auto_sync( $order->get_customer_id() ) ) { return; } foreach ( (array) $this->setting( 'auto_types', array() ) as $type ) { try { CustomerAddress::copy_from_order( $order, $order->get_customer_id(), $type, (bool) $this->setting( 'skip_empty' ) ); } catch ( \Exception $e ) { wc_get_logger()->warning( $e->getMessage(), array( 'source' => 'apwc-address-sync' ) ); } } } /** * Whether the automatic sync may write onto this account. * * Staff can link an order to any account and then change its status, so: * - staff accounts (order editors, store managers, admins, super admins) are never updated, * on multisite also when they are staff on another site of the network; * - with someone logged in, they must be allowed to edit that user (a shopper may edit * themselves), or edit orders while the account only has customer roles on this site * (`apwc_order_address_sync_auto_roles`; on multisite store managers can't edit users); * - with nobody logged in (payment webhook, cron), only accounts with only customer roles on * this site are updated. * * @since 1.0.0 * @param int $user_id Customer linked to the order. * @return bool */ private function may_auto_sync( $user_id ) { $user = get_userdata( (int) $user_id ); if ( ! $user instanceof \WP_User || ! $user->exists() ) { return false; } if ( is_super_admin( $user->ID ) || $this->is_staff( $user->ID ) ) { return false; } // Multisite: billing/shipping meta is shared by every site, so staff on any site are off limits. if ( is_multisite() ) { $here = get_current_blog_id(); foreach ( array_keys( get_blogs_of_user( $user->ID ) ) as $blog_id ) { if ( (int) $blog_id === $here ) { continue; } switch_to_blog( (int) $blog_id ); $staff = $this->is_staff( $user->ID ); restore_current_blog(); if ( $staff ) { return false; } } } /** * Filter the roles whose accounts the automatic sync may update without `edit_user`: when * nobody is logged in (payment webhooks, scheduled jobs), or when an order editor changes * the status (on multisite they can't edit customers). Every role the account has on this * site must be in the list. Staff accounts are never updated, whatever this returns. * * @since 1.0.0 * @param string[] $roles Role slugs. Default `customer`. * @param int $user_id Customer linked to the order. */ $roles = array_map( 'strval', (array) apply_filters( 'apwc_order_address_sync_auto_roles', array( 'customer' ), $user->ID ) ); $has = array_map( 'strval', (array) $user->roles ); $role_ok = $has && ! array_diff( $has, $roles ); if ( get_current_user_id() ) { return current_user_can( 'edit_user', $user->ID ) || ( $role_ok && current_user_can( 'edit_shop_orders' ) ); } return $role_ok; } /** * Whether a user has staff capabilities on the current site. * * Depends on the site switched to, so its result may differ between calls. * * @phpstan-impure * @since 1.0.0 * @param int $user_id User ID. * @return bool */ private function is_staff( $user_id ) { $user = new \WP_User( (int) $user_id ); // Fresh object: capabilities of the site switched to. foreach ( array( 'edit_shop_orders', 'manage_woocommerce', 'manage_options', 'edit_users' ) as $staff_cap ) { if ( $user->has_cap( $staff_cap ) ) { return true; } } return false; } }