PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.2
WooCommerce v10.9.2
11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / admin / meta-boxes / class-wc-meta-box-product-data.php
woocommerce / includes / admin / meta-boxes Last commit date
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