views
1 month ago
class-wc-meta-box-coupon-data.php
1 year ago
class-wc-meta-box-order-actions.php
7 months ago
class-wc-meta-box-order-data.php
11 months ago
class-wc-meta-box-order-downloads.php
3 years ago
class-wc-meta-box-order-items.php
3 years ago
class-wc-meta-box-order-notes.php
1 month ago
class-wc-meta-box-product-categories.php
2 years ago
class-wc-meta-box-product-data.php
1 month ago
class-wc-meta-box-product-images.php
2 months ago
class-wc-meta-box-product-reviews.php
5 years ago
class-wc-meta-box-product-short-description.php
3 years ago
class-wc-meta-box-product-data.php
604 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Product Data |
| 4 | * |
| 5 | * Displays the product data box, tabbed, with several panels covering price, stock etc. |
| 6 | * |
| 7 | * @package WooCommerce\Admin\Meta Boxes |
| 8 | * @version 3.0.0 |
| 9 | */ |
| 10 | |
| 11 | use Automattic\WooCommerce\Enums\ProductStatus; |
| 12 | use Automattic\WooCommerce\Enums\ProductType; |
| 13 | use Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController; |
| 14 | use Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog\POSProductVisibilitySync; |
| 15 | use Automattic\WooCommerce\Utilities\FeaturesUtil; |
| 16 | |
| 17 | if ( ! defined( 'ABSPATH' ) ) { |
| 18 | exit; |
| 19 | } |
| 20 | |
| 21 | /** |
| 22 | * WC_Meta_Box_Product_Data Class. |
| 23 | */ |
| 24 | class WC_Meta_Box_Product_Data { |
| 25 | |
| 26 | /** |
| 27 | * Output the metabox. |
| 28 | * |
| 29 | * @param WP_Post $post Post object. |
| 30 | */ |
| 31 | public static function output( $post ) { |
| 32 | global $thepostid, $product_object; |
| 33 | |
| 34 | $thepostid = $post->ID; |
| 35 | $product_object = $thepostid ? wc_get_product( $thepostid ) : new WC_Product(); |
| 36 | |
| 37 | wp_nonce_field( 'woocommerce_save_data', 'woocommerce_meta_nonce' ); |
| 38 | |
| 39 | include __DIR__ . '/views/html-product-data-panel.php'; |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * Show tab content/settings. |
| 44 | */ |
| 45 | private static function output_tabs() { |
| 46 | global $post, $thepostid, $product_object; |
| 47 | |
| 48 | include __DIR__ . '/views/html-product-data-general.php'; |
| 49 | include __DIR__ . '/views/html-product-data-inventory.php'; |
| 50 | include __DIR__ . '/views/html-product-data-shipping.php'; |
| 51 | include __DIR__ . '/views/html-product-data-linked-products.php'; |
| 52 | include __DIR__ . '/views/html-product-data-attributes.php'; |
| 53 | include __DIR__ . '/views/html-product-data-advanced.php'; |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * Return array of product type options. |
| 58 | * |
| 59 | * @return array |
| 60 | */ |
| 61 | private static function get_product_type_options() { |
| 62 | /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ |
| 63 | return apply_filters( |
| 64 | 'product_type_options', |
| 65 | wc_get_default_product_type_options(), |
| 66 | ); |
| 67 | /* phpcs: enable */ |
| 68 | } |
| 69 | |
| 70 | /** |
| 71 | * Return array of tabs to show. |
| 72 | * |
| 73 | * @return array |
| 74 | */ |
| 75 | private static function get_product_data_tabs() { |
| 76 | /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ |
| 77 | $tabs = apply_filters( |
| 78 | 'woocommerce_product_data_tabs', |
| 79 | array( |
| 80 | 'general' => array( |
| 81 | 'label' => __( 'General', 'woocommerce' ), |
| 82 | 'target' => 'general_product_data', |
| 83 | 'class' => array( 'hide_if_grouped' ), |
| 84 | 'priority' => 10, |
| 85 | ), |
| 86 | 'inventory' => array( |
| 87 | 'label' => __( 'Inventory', 'woocommerce' ), |
| 88 | 'target' => 'inventory_product_data', |
| 89 | 'class' => array( 'show_if_simple', 'show_if_variable', 'show_if_grouped', 'show_if_external' ), |
| 90 | 'priority' => 20, |
| 91 | ), |
| 92 | 'shipping' => array( |
| 93 | 'label' => __( 'Shipping', 'woocommerce' ), |
| 94 | 'target' => 'shipping_product_data', |
| 95 | 'class' => array( 'hide_if_virtual', 'hide_if_grouped', 'hide_if_external' ), |
| 96 | 'priority' => 30, |
| 97 | ), |
| 98 | 'linked_product' => array( |
| 99 | 'label' => __( 'Linked Products', 'woocommerce' ), |
| 100 | 'target' => 'linked_product_data', |
| 101 | 'class' => array(), |
| 102 | 'priority' => 40, |
| 103 | ), |
| 104 | 'attribute' => array( |
| 105 | 'label' => __( 'Attributes', 'woocommerce' ), |
| 106 | 'target' => 'product_attributes', |
| 107 | 'class' => array(), |
| 108 | 'priority' => 50, |
| 109 | ), |
| 110 | 'variations' => array( |
| 111 | 'label' => __( 'Variations', 'woocommerce' ), |
| 112 | 'target' => 'variable_product_options', |
| 113 | 'class' => array( 'show_if_variable' ), |
| 114 | 'priority' => 60, |
| 115 | ), |
| 116 | 'advanced' => array( |
| 117 | 'label' => __( 'Advanced', 'woocommerce' ), |
| 118 | 'target' => 'advanced_product_data', |
| 119 | 'class' => array(), |
| 120 | 'priority' => 70, |
| 121 | ), |
| 122 | ) |
| 123 | ); |
| 124 | /* phpcs: enable */ |
| 125 | |
| 126 | // Sort tabs based on priority. |
| 127 | uasort( $tabs, array( __CLASS__, 'product_data_tabs_sort' ) ); |
| 128 | |
| 129 | return $tabs; |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Callback to sort product data tabs on priority. |
| 134 | * |
| 135 | * @since 3.1.0 |
| 136 | * @param int $a First item. |
| 137 | * @param int $b Second item. |
| 138 | * |
| 139 | * @return bool |
| 140 | */ |
| 141 | private static function product_data_tabs_sort( $a, $b ) { |
| 142 | if ( ! isset( $a['priority'], $b['priority'] ) ) { |
| 143 | return -1; |
| 144 | } |
| 145 | |
| 146 | if ( $a['priority'] === $b['priority'] ) { |
| 147 | return 0; |
| 148 | } |
| 149 | |
| 150 | return $a['priority'] < $b['priority'] ? -1 : 1; |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * Filter callback for finding variation attributes. |
| 155 | * |
| 156 | * @param WC_Product_Attribute $attribute Product attribute. |
| 157 | * @return bool |
| 158 | */ |
| 159 | private static function filter_variation_attributes( $attribute ) { |
| 160 | return true === $attribute->get_variation(); |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Filter callback for finding non-variation attributes. |
| 165 | * |
| 166 | * @param WC_Product_Attribute $attribute Product attribute. |
| 167 | * @return bool |
| 168 | */ |
| 169 | private static function filter_non_variation_attributes( $attribute ) { |
| 170 | return false === $attribute->get_variation(); |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Show options for the variable product type. |
| 175 | */ |
| 176 | public static function output_variations() { |
| 177 | global $post, $wpdb, $product_object; |
| 178 | |
| 179 | /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ |
| 180 | $variation_attributes = array_filter( $product_object->get_attributes(), array( __CLASS__, 'filter_variation_attributes' ) ); |
| 181 | $default_attributes = $product_object->get_default_attributes(); |
| 182 | $variations_count = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_count', $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(ID) FROM $wpdb->posts WHERE post_parent = %d AND post_type = 'product_variation' AND post_status IN ('publish', 'private')", $post->ID ) ), $post->ID ) ); |
| 183 | $variations_per_page = absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ); |
| 184 | $variations_total_pages = ceil( $variations_count / $variations_per_page ); |
| 185 | /* phpcs: enable */ |
| 186 | |
| 187 | include __DIR__ . '/views/html-product-data-variations.php'; |
| 188 | } |
| 189 | |
| 190 | /** |
| 191 | * Prepare downloads for save. |
| 192 | * |
| 193 | * @param array $file_names File names. |
| 194 | * @param array $file_urls File urls. |
| 195 | * @param array $file_hashes File hashes. |
| 196 | * |
| 197 | * @return array |
| 198 | */ |
| 199 | private static function prepare_downloads( $file_names, $file_urls, $file_hashes ) { |
| 200 | $downloads = array(); |
| 201 | |
| 202 | if ( ! empty( $file_urls ) ) { |
| 203 | $file_url_size = count( $file_urls ); |
| 204 | |
| 205 | for ( $i = 0; $i < $file_url_size; $i++ ) { |
| 206 | if ( ! empty( $file_urls[ $i ] ) ) { |
| 207 | $downloads[] = array( |
| 208 | 'name' => wc_clean( $file_names[ $i ] ), |
| 209 | 'file' => wp_unslash( trim( $file_urls[ $i ] ) ), |
| 210 | 'download_id' => wc_clean( $file_hashes[ $i ] ), |
| 211 | ); |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | return $downloads; |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Prepare children for save. |
| 220 | * |
| 221 | * @return array |
| 222 | */ |
| 223 | private static function prepare_children() { |
| 224 | return isset( $_POST['grouped_products'] ) ? array_filter( array_map( 'intval', (array) $_POST['grouped_products'] ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 225 | } |
| 226 | |
| 227 | /** |
| 228 | * Prepare attributes for save. |
| 229 | * |
| 230 | * @param array $data Attribute data. |
| 231 | * |
| 232 | * @return array |
| 233 | */ |
| 234 | public static function prepare_attributes( $data = false ) { |
| 235 | $attributes = array(); |
| 236 | |
| 237 | if ( ! $data ) { |
| 238 | $data = stripslashes_deep( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 239 | } |
| 240 | |
| 241 | if ( isset( $data['attribute_names'], $data['attribute_values'] ) ) { |
| 242 | $attribute_names = $data['attribute_names']; |
| 243 | $attribute_values = $data['attribute_values']; |
| 244 | $attribute_visibility = isset( $data['attribute_visibility'] ) ? $data['attribute_visibility'] : array(); |
| 245 | $attribute_variation = isset( $data['attribute_variation'] ) ? $data['attribute_variation'] : array(); |
| 246 | $attribute_position = $data['attribute_position']; |
| 247 | $attribute_names_max_key = max( array_keys( $attribute_names ) ); |
| 248 | |
| 249 | for ( $i = 0; $i <= $attribute_names_max_key; $i++ ) { |
| 250 | if ( empty( $attribute_names[ $i ] ) || ! isset( $attribute_values[ $i ] ) ) { |
| 251 | continue; |
| 252 | } |
| 253 | $attribute_id = 0; |
| 254 | $attribute_name = wc_clean( esc_html( $attribute_names[ $i ] ) ); |
| 255 | |
| 256 | if ( 'pa_' === substr( $attribute_name, 0, 3 ) ) { |
| 257 | $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name ); |
| 258 | } |
| 259 | |
| 260 | $options = isset( $attribute_values[ $i ] ) ? $attribute_values[ $i ] : ''; |
| 261 | |
| 262 | if ( is_array( $options ) ) { |
| 263 | // Term ids sent as array. |
| 264 | $options = wp_parse_id_list( $options ); |
| 265 | } else { |
| 266 | // Terms or text sent in textarea. |
| 267 | $options = 0 < $attribute_id ? wc_sanitize_textarea( esc_html( wc_sanitize_term_text_based( $options ) ) ) : wc_sanitize_textarea( esc_html( $options ) ); |
| 268 | $options = wc_get_text_attributes( $options ); |
| 269 | } |
| 270 | |
| 271 | if ( empty( $options ) ) { |
| 272 | continue; |
| 273 | } |
| 274 | |
| 275 | $attribute = new WC_Product_Attribute(); |
| 276 | $attribute->set_id( $attribute_id ); |
| 277 | $attribute->set_name( $attribute_name ); |
| 278 | $attribute->set_options( $options ); |
| 279 | $attribute->set_position( $attribute_position[ $i ] ); |
| 280 | $attribute->set_visible( isset( $attribute_visibility[ $i ] ) ); |
| 281 | $attribute->set_variation( isset( $attribute_variation[ $i ] ) ); |
| 282 | /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ |
| 283 | $attributes[] = apply_filters( 'woocommerce_admin_meta_boxes_prepare_attribute', $attribute, $data, $i ); |
| 284 | /* phpcs: enable */ |
| 285 | } |
| 286 | } |
| 287 | return $attributes; |
| 288 | } |
| 289 | |
| 290 | /** |
| 291 | * Prepare attributes for a specific variation or defaults. |
| 292 | * |
| 293 | * @param array $all_attributes List of attribute keys. |
| 294 | * @param string $key_prefix Attribute key prefix. |
| 295 | * @param int $index Attribute array index. |
| 296 | * @return array |
| 297 | */ |
| 298 | private static function prepare_set_attributes( $all_attributes, $key_prefix = 'attribute_', $index = null ) { |
| 299 | $attributes = array(); |
| 300 | |
| 301 | if ( $all_attributes ) { |
| 302 | foreach ( $all_attributes as $attribute ) { |
| 303 | if ( $attribute->get_variation() ) { |
| 304 | $attribute_key = sanitize_title( $attribute->get_name() ); |
| 305 | |
| 306 | if ( ! is_null( $index ) ) { |
| 307 | $value = isset( $_POST[ $key_prefix . $attribute_key ][ $index ] ) ? wp_unslash( $_POST[ $key_prefix . $attribute_key ][ $index ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 308 | } else { |
| 309 | $value = isset( $_POST[ $key_prefix . $attribute_key ] ) ? wp_unslash( $_POST[ $key_prefix . $attribute_key ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 310 | } |
| 311 | |
| 312 | if ( $attribute->is_taxonomy() ) { |
| 313 | // Don't use wc_clean as it destroys sanitized characters. |
| 314 | $value = sanitize_title( $value ); |
| 315 | } else { |
| 316 | $value = html_entity_decode( wc_clean( $value ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // WPCS: sanitization ok. |
| 317 | } |
| 318 | |
| 319 | $attributes[ $attribute_key ] = $value; |
| 320 | } |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | return $attributes; |
| 325 | } |
| 326 | |
| 327 | /** |
| 328 | * Save meta box data. |
| 329 | * |
| 330 | * @param int $post_id WP post id. |
| 331 | * @param WP_Post $post Post object. |
| 332 | */ |
| 333 | public static function save( $post_id, $post ) { |
| 334 | // phpcs:disable WordPress.Security.NonceVerification.Missing |
| 335 | // Process product type first so we have the correct class to run setters. |
| 336 | $product_type = empty( $_POST['product-type'] ) ? WC_Product_Factory::get_product_type( $post_id ) : sanitize_title( wp_unslash( $_POST['product-type'] ) ); |
| 337 | $classname = WC_Product_Factory::get_product_classname( $post_id, $product_type ? $product_type : ProductType::SIMPLE ); |
| 338 | $product = new $classname( $post_id ); |
| 339 | $attributes = self::prepare_attributes(); |
| 340 | $stock = null; |
| 341 | |
| 342 | // Handle stock changes. |
| 343 | if ( isset( $_POST['_stock'] ) ) { |
| 344 | if ( isset( $_POST['_original_stock'] ) && wc_stock_amount( $product->get_stock_quantity( 'edit' ) ) !== wc_stock_amount( wp_unslash( $_POST['_original_stock'] ) ) ) { |
| 345 | /* translators: 1: product ID 2: quantity in stock */ |
| 346 | WC_Admin_Meta_Boxes::add_error( sprintf( __( 'The stock has not been updated because the value has changed since editing. Product %1$d has %2$d units in stock.', 'woocommerce' ), $product->get_id(), $product->get_stock_quantity( 'edit' ) ) ); |
| 347 | } else { |
| 348 | $stock = wc_stock_amount( wp_unslash( $_POST['_stock'] ) ); |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | // Handle dates. |
| 353 | $date_on_sale_from = ''; |
| 354 | $date_on_sale_to = ''; |
| 355 | |
| 356 | // Force date from to beginning of day. |
| 357 | if ( isset( $_POST['_sale_price_dates_from'] ) ) { |
| 358 | $date_on_sale_from = wc_clean( wp_unslash( $_POST['_sale_price_dates_from'] ) ); |
| 359 | |
| 360 | if ( ! empty( $date_on_sale_from ) ) { |
| 361 | $date_on_sale_from = date( 'Y-m-d 00:00:00', strtotime( $date_on_sale_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | // Force date to to the end of the day. |
| 366 | if ( isset( $_POST['_sale_price_dates_to'] ) ) { |
| 367 | $date_on_sale_to = wc_clean( wp_unslash( $_POST['_sale_price_dates_to'] ) ); |
| 368 | |
| 369 | if ( ! empty( $date_on_sale_to ) ) { |
| 370 | $date_on_sale_to = date( 'Y-m-d 23:59:59', strtotime( $date_on_sale_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | $errors = $product->set_props( |
| 375 | array( |
| 376 | 'sku' => isset( $_POST['_sku'] ) ? wc_clean( wp_unslash( $_POST['_sku'] ) ) : null, |
| 377 | 'global_unique_id' => isset( $_POST['_global_unique_id'] ) ? wc_clean( wp_unslash( $_POST['_global_unique_id'] ) ) : null, |
| 378 | 'purchase_note' => isset( $_POST['_purchase_note'] ) ? wp_kses_post( wp_unslash( $_POST['_purchase_note'] ) ) : '', |
| 379 | 'downloadable' => isset( $_POST['_downloadable'] ), |
| 380 | 'virtual' => isset( $_POST['_virtual'] ), |
| 381 | 'featured' => isset( $_POST['_featured'] ), |
| 382 | 'catalog_visibility' => isset( $_POST['_visibility'] ) ? wc_clean( wp_unslash( $_POST['_visibility'] ) ) : null, |
| 383 | 'tax_status' => isset( $_POST['_tax_status'] ) ? wc_clean( wp_unslash( $_POST['_tax_status'] ) ) : null, |
| 384 | 'tax_class' => isset( $_POST['_tax_class'] ) ? sanitize_title( wp_unslash( $_POST['_tax_class'] ) ) : null, |
| 385 | 'weight' => isset( $_POST['_weight'] ) ? wc_clean( wp_unslash( $_POST['_weight'] ) ) : null, |
| 386 | 'length' => isset( $_POST['_length'] ) ? wc_clean( wp_unslash( $_POST['_length'] ) ) : null, |
| 387 | 'width' => isset( $_POST['_width'] ) ? wc_clean( wp_unslash( $_POST['_width'] ) ) : null, |
| 388 | 'height' => isset( $_POST['_height'] ) ? wc_clean( wp_unslash( $_POST['_height'] ) ) : null, |
| 389 | 'shipping_class_id' => isset( $_POST['product_shipping_class'] ) ? absint( wp_unslash( $_POST['product_shipping_class'] ) ) : null, |
| 390 | 'sold_individually' => ! empty( $_POST['_sold_individually'] ), |
| 391 | 'upsell_ids' => isset( $_POST['upsell_ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['upsell_ids'] ) ) : array(), |
| 392 | 'cross_sell_ids' => isset( $_POST['crosssell_ids'] ) ? array_map( 'intval', (array) wp_unslash( $_POST['crosssell_ids'] ) ) : array(), |
| 393 | 'regular_price' => isset( $_POST['_regular_price'] ) ? wc_clean( wp_unslash( $_POST['_regular_price'] ) ) : null, |
| 394 | 'sale_price' => isset( $_POST['_sale_price'] ) ? wc_clean( wp_unslash( $_POST['_sale_price'] ) ) : null, |
| 395 | 'date_on_sale_from' => $date_on_sale_from, |
| 396 | 'date_on_sale_to' => $date_on_sale_to, |
| 397 | 'manage_stock' => ! empty( $_POST['_manage_stock'] ), |
| 398 | 'backorders' => isset( $_POST['_backorders'] ) ? wc_clean( wp_unslash( $_POST['_backorders'] ) ) : null, |
| 399 | 'stock_status' => isset( $_POST['_stock_status'] ) ? wc_clean( wp_unslash( $_POST['_stock_status'] ) ) : null, |
| 400 | 'stock_quantity' => $stock, |
| 401 | 'low_stock_amount' => isset( $_POST['_low_stock_amount'] ) && '' !== $_POST['_low_stock_amount'] ? wc_stock_amount( wp_unslash( $_POST['_low_stock_amount'] ) ) : '', |
| 402 | 'download_limit' => isset( $_POST['_download_limit'] ) && '' !== $_POST['_download_limit'] ? absint( wp_unslash( $_POST['_download_limit'] ) ) : '', |
| 403 | 'download_expiry' => isset( $_POST['_download_expiry'] ) && '' !== $_POST['_download_expiry'] ? absint( wp_unslash( $_POST['_download_expiry'] ) ) : '', |
| 404 | // Those are sanitized inside prepare_downloads. |
| 405 | 'downloads' => self::prepare_downloads( |
| 406 | isset( $_POST['_wc_file_names'] ) ? wp_unslash( $_POST['_wc_file_names'] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 407 | isset( $_POST['_wc_file_urls'] ) ? wp_unslash( $_POST['_wc_file_urls'] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 408 | isset( $_POST['_wc_file_hashes'] ) ? wp_unslash( $_POST['_wc_file_hashes'] ) : array() // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 409 | ), |
| 410 | 'product_url' => isset( $_POST['_product_url'] ) ? esc_url_raw( wp_unslash( $_POST['_product_url'] ) ) : '', |
| 411 | 'button_text' => isset( $_POST['_button_text'] ) ? wc_clean( wp_unslash( $_POST['_button_text'] ) ) : '', |
| 412 | 'children' => ProductType::GROUPED === $product_type ? self::prepare_children() : null, |
| 413 | 'reviews_allowed' => ! empty( $_POST['comment_status'] ) && 'open' === $_POST['comment_status'], |
| 414 | 'attributes' => $attributes, |
| 415 | 'default_attributes' => self::prepare_set_attributes( $attributes, 'default_attribute_' ), |
| 416 | ) |
| 417 | ); |
| 418 | |
| 419 | if ( wc_get_container()->get( CostOfGoodsSoldController::class )->feature_is_enabled() ) { |
| 420 | $cogs_value = wc_clean( wp_unslash( $_POST['_cogs_value'] ?? null ) ); |
| 421 | $product->set_cogs_value( is_null( $cogs_value ) ? null : (float) wc_format_decimal( $cogs_value ) ); |
| 422 | } |
| 423 | |
| 424 | if ( is_wp_error( $errors ) ) { |
| 425 | WC_Admin_Meta_Boxes::add_error( $errors->get_error_message() ); |
| 426 | } |
| 427 | |
| 428 | // Remove _product_template_id for products that were created with the new product editor. |
| 429 | $product->delete_meta_data( '_product_template_id' ); |
| 430 | |
| 431 | if ( FeaturesUtil::feature_is_enabled( 'point_of_sale' ) ) { |
| 432 | $visible_in_pos = isset( $_POST['_visible_in_pos'] ) && 'yes' === wc_clean( wp_unslash( $_POST['_visible_in_pos'] ) ); |
| 433 | wc_get_container()->get( POSProductVisibilitySync::class )->set_product_pos_visibility( $post_id, $visible_in_pos ); |
| 434 | } |
| 435 | |
| 436 | /** |
| 437 | * Set props before save. |
| 438 | * |
| 439 | * @since 3.0.0 |
| 440 | */ |
| 441 | do_action( 'woocommerce_admin_process_product_object', $product ); |
| 442 | |
| 443 | $product->save(); |
| 444 | |
| 445 | if ( $product->is_type( ProductType::VARIABLE ) ) { |
| 446 | $original_post_title = isset( $_POST['original_post_title'] ) ? wc_clean( wp_unslash( $_POST['original_post_title'] ) ) : ''; |
| 447 | $post_title = isset( $_POST['post_title'] ) ? wc_clean( wp_unslash( $_POST['post_title'] ) ) : ''; |
| 448 | |
| 449 | $product->get_data_store()->sync_variation_names( $product, $original_post_title, $post_title ); |
| 450 | } |
| 451 | /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ |
| 452 | do_action( 'woocommerce_process_product_meta_' . $product_type, $post_id ); |
| 453 | /* phpcs:enable WordPress.Security.NonceVerification.Missing and WooCommerce.Commenting.CommentHooks.MissingHookComment */ |
| 454 | } |
| 455 | |
| 456 | /** |
| 457 | * Save variation meta box data. |
| 458 | * |
| 459 | * @param int $post_id WP post id. |
| 460 | * @param WP_Post $post Post object. |
| 461 | */ |
| 462 | public static function save_variations( $post_id, $post ) { |
| 463 | global $wpdb; |
| 464 | |
| 465 | // phpcs:disable WordPress.Security.NonceVerification.Missing |
| 466 | if ( isset( $_POST['variable_post_id'] ) ) { |
| 467 | $parent = wc_get_product( $post_id ); |
| 468 | $parent->set_default_attributes( self::prepare_set_attributes( $parent->get_attributes(), 'default_attribute_' ) ); |
| 469 | $parent->save(); |
| 470 | |
| 471 | $max_loop = max( array_keys( wp_unslash( $_POST['variable_post_id'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 472 | $data_store = $parent->get_data_store(); |
| 473 | $data_store->sort_all_product_variations( $parent->get_id() ); |
| 474 | $new_variation_menu_order_id = ! empty( $_POST['new_variation_menu_order_id'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_id'] ) ) : false; |
| 475 | $new_variation_menu_order_value = ! empty( $_POST['new_variation_menu_order_value'] ) ? wc_clean( wp_unslash( $_POST['new_variation_menu_order_value'] ) ) : false; |
| 476 | |
| 477 | // Only perform this operation if setting menu order via the prompt. |
| 478 | if ( $new_variation_menu_order_id && $new_variation_menu_order_value ) { |
| 479 | /* |
| 480 | * We need to gather all the variations with menu order that is |
| 481 | * equal or greater than the menu order that is newly set and |
| 482 | * increment them by one so that we can correctly insert the updated |
| 483 | * variation menu order. |
| 484 | */ |
| 485 | $wpdb->query( |
| 486 | $wpdb->prepare( |
| 487 | "UPDATE {$wpdb->posts} SET menu_order = menu_order + 1 WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' AND menu_order >= %d AND ID != %d", |
| 488 | $post_id, |
| 489 | $new_variation_menu_order_value, |
| 490 | $new_variation_menu_order_id |
| 491 | ) |
| 492 | ); |
| 493 | } |
| 494 | |
| 495 | $cogs_is_enabled = wc_get_container()->get( CostOfGoodsSoldController::class )->feature_is_enabled(); |
| 496 | for ( $i = 0; $i <= $max_loop; $i++ ) { |
| 497 | |
| 498 | if ( ! isset( $_POST['variable_post_id'][ $i ] ) ) { |
| 499 | continue; |
| 500 | } |
| 501 | $variation_id = absint( $_POST['variable_post_id'][ $i ] ); |
| 502 | $variation = wc_get_product_object( ProductType::VARIATION, $variation_id ); |
| 503 | $stock = null; |
| 504 | |
| 505 | // Handle stock changes. |
| 506 | if ( isset( $_POST['variable_stock'], $_POST['variable_stock'][ $i ] ) ) { |
| 507 | if ( isset( $_POST['variable_original_stock'], $_POST['variable_original_stock'][ $i ] ) && wc_stock_amount( $variation->get_stock_quantity( 'edit' ) ) !== wc_stock_amount( wp_unslash( $_POST['variable_original_stock'][ $i ] ) ) ) { |
| 508 | /* translators: 1: product ID 2: quantity in stock */ |
| 509 | WC_Admin_Meta_Boxes::add_error( sprintf( __( 'The stock has not been updated because the value has changed since editing. Product %1$d has %2$d units in stock.', 'woocommerce' ), $variation->get_id(), $variation->get_stock_quantity( 'edit' ) ) ); |
| 510 | } else { |
| 511 | $stock = wc_stock_amount( wp_unslash( $_POST['variable_stock'][ $i ] ) ); |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | // Handle dates. |
| 516 | $date_on_sale_from = ''; |
| 517 | $date_on_sale_to = ''; |
| 518 | |
| 519 | // Force date from to beginning of day. |
| 520 | if ( isset( $_POST['variable_sale_price_dates_from'][ $i ] ) ) { |
| 521 | $date_on_sale_from = wc_clean( wp_unslash( $_POST['variable_sale_price_dates_from'][ $i ] ) ); |
| 522 | |
| 523 | if ( ! empty( $date_on_sale_from ) ) { |
| 524 | $date_on_sale_from = date( 'Y-m-d 00:00:00', strtotime( $date_on_sale_from ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 525 | } |
| 526 | } |
| 527 | |
| 528 | // Force date to to the end of the day. |
| 529 | if ( isset( $_POST['variable_sale_price_dates_to'][ $i ] ) ) { |
| 530 | $date_on_sale_to = wc_clean( wp_unslash( $_POST['variable_sale_price_dates_to'][ $i ] ) ); |
| 531 | |
| 532 | if ( ! empty( $date_on_sale_to ) ) { |
| 533 | $date_on_sale_to = date( 'Y-m-d 23:59:59', strtotime( $date_on_sale_to ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 534 | } |
| 535 | } |
| 536 | |
| 537 | $errors = $variation->set_props( |
| 538 | array( |
| 539 | 'status' => isset( $_POST['variable_enabled'][ $i ] ) ? ProductStatus::PUBLISH : ProductStatus::PRIVATE, |
| 540 | 'menu_order' => isset( $_POST['variation_menu_order'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variation_menu_order'][ $i ] ) ) : null, |
| 541 | 'regular_price' => isset( $_POST['variable_regular_price'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_regular_price'][ $i ] ) ) : null, |
| 542 | 'sale_price' => isset( $_POST['variable_sale_price'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_sale_price'][ $i ] ) ) : null, |
| 543 | 'virtual' => isset( $_POST['variable_is_virtual'][ $i ] ), |
| 544 | 'downloadable' => isset( $_POST['variable_is_downloadable'][ $i ] ), |
| 545 | 'date_on_sale_from' => $date_on_sale_from, |
| 546 | 'date_on_sale_to' => $date_on_sale_to, |
| 547 | 'description' => isset( $_POST['variable_description'][ $i ] ) ? wp_kses_post( wp_unslash( $_POST['variable_description'][ $i ] ) ) : null, |
| 548 | 'download_limit' => isset( $_POST['variable_download_limit'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_download_limit'][ $i ] ) ) : null, |
| 549 | 'download_expiry' => isset( $_POST['variable_download_expiry'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_download_expiry'][ $i ] ) ) : null, |
| 550 | // Those are sanitized inside prepare_downloads. |
| 551 | 'downloads' => self::prepare_downloads( |
| 552 | isset( $_POST['_wc_variation_file_names'][ $variation_id ] ) ? wp_unslash( $_POST['_wc_variation_file_names'][ $variation_id ] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 553 | isset( $_POST['_wc_variation_file_urls'][ $variation_id ] ) ? wp_unslash( $_POST['_wc_variation_file_urls'][ $variation_id ] ) : array(), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 554 | isset( $_POST['_wc_variation_file_hashes'][ $variation_id ] ) ? wp_unslash( $_POST['_wc_variation_file_hashes'][ $variation_id ] ) : array() // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized |
| 555 | ), |
| 556 | 'manage_stock' => isset( $_POST['variable_manage_stock'][ $i ] ), |
| 557 | 'stock_quantity' => $stock, |
| 558 | 'low_stock_amount' => isset( $_POST['variable_low_stock_amount'][ $i ] ) && '' !== $_POST['variable_low_stock_amount'][ $i ] ? wc_stock_amount( wp_unslash( $_POST['variable_low_stock_amount'][ $i ] ) ) : '', |
| 559 | 'backorders' => isset( $_POST['variable_backorders'], $_POST['variable_backorders'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_backorders'][ $i ] ) ) : null, |
| 560 | 'stock_status' => isset( $_POST['variable_stock_status'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_stock_status'][ $i ] ) ) : null, |
| 561 | 'image_id' => isset( $_POST['upload_image_id'][ $i ] ) ? wc_clean( wp_unslash( $_POST['upload_image_id'][ $i ] ) ) : null, |
| 562 | 'attributes' => self::prepare_set_attributes( $parent->get_attributes(), 'attribute_', $i ), |
| 563 | 'sku' => isset( $_POST['variable_sku'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_sku'][ $i ] ) ) : '', |
| 564 | 'global_unique_id' => isset( $_POST['variable_global_unique_id'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_global_unique_id'][ $i ] ) ) : '', |
| 565 | 'weight' => isset( $_POST['variable_weight'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_weight'][ $i ] ) ) : '', |
| 566 | 'length' => isset( $_POST['variable_length'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_length'][ $i ] ) ) : '', |
| 567 | 'width' => isset( $_POST['variable_width'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_width'][ $i ] ) ) : '', |
| 568 | 'height' => isset( $_POST['variable_height'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_height'][ $i ] ) ) : '', |
| 569 | 'shipping_class_id' => isset( $_POST['variable_shipping_class'][ $i ] ) ? wc_clean( wp_unslash( $_POST['variable_shipping_class'][ $i ] ) ) : null, |
| 570 | 'tax_class' => isset( $_POST['variable_tax_class'][ $i ] ) ? sanitize_title( wp_unslash( $_POST['variable_tax_class'][ $i ] ) ) : null, |
| 571 | ) |
| 572 | ); |
| 573 | |
| 574 | if ( is_wp_error( $errors ) ) { |
| 575 | WC_Admin_Meta_Boxes::add_error( $errors->get_error_message() ); |
| 576 | } |
| 577 | |
| 578 | if ( $cogs_is_enabled ) { |
| 579 | $cogs_value = wc_clean( wp_unslash( $_POST['variable_cost_value'][ $i ] ?? '' ) ); |
| 580 | if ( '' === $cogs_value ) { |
| 581 | $cogs_value = null; |
| 582 | } |
| 583 | $variation->set_cogs_value( is_null( $cogs_value ) ? null : (float) wc_format_decimal( $cogs_value ) ); |
| 584 | } |
| 585 | |
| 586 | /** |
| 587 | * Set variation props before save. |
| 588 | * |
| 589 | * @param object $variation WC_Product_Variation object. |
| 590 | * @param int $i |
| 591 | * @since 3.8.0 |
| 592 | */ |
| 593 | do_action( 'woocommerce_admin_process_variation_object', $variation, $i ); |
| 594 | |
| 595 | $variation->save(); |
| 596 | /* phpcs:disable WooCommerce.Commenting.CommentHooks.MissingHookComment */ |
| 597 | do_action( 'woocommerce_save_product_variation', $variation_id, $i ); |
| 598 | /* phpcs: enable */ |
| 599 | } |
| 600 | } |
| 601 | // phpcs:enable WordPress.Security.NonceVerification.Missing |
| 602 | } |
| 603 | } |
| 604 |