PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / ajax / attributes.php

attributes.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/ajax/attributes.php

472 lines 15.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Ajax;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use Exception;
10 use StoreEngine\Classes\AbstractAjaxHandler;
11 use StoreEngine\Classes\Attribute;
12 use StoreEngine\Utils\Helper;
13
14 class Attributes extends AbstractAjaxHandler {
15 public function __construct() {
16 $this->actions = array(
17 'get_product_attributes' => [
18 'callback' => [ $this, 'get_product_attributes' ],
19 'fields' => [
20 'page' => 'int',
21 'per_page' => 'int',
22 'status' => 'string',
23 'search' => 'string',
24 ],
25 ],
26 'create_product_attribute' => [
27 'callback' => [ $this, 'create_product_attribute' ],
28 'fields' => [
29 'attribute_name' => 'string',
30 'attribute_label' => 'string',
31 'attribute_type' => 'string',
32 'attribute_orderby' => 'string',
33 'attribute_public' => 'boolean',
34 ],
35 ],
36 'update_product_attribute' => [
37 'callback' => [ $this, 'update_product_attribute' ],
38 'fields' => [
39 'attribute_id' => 'int',
40 'attribute_label' => 'string',
41 'attribute_name' => 'string',
42 'attribute_type' => 'string',
43 'attribute_orderby' => 'string',
44 'attribute_public' => 'int',
45 ],
46 ],
47 'delete_product_attribute' => [
48 'callback' => [ $this, 'delete_product_attribute' ],
49 'fields' => [
50 'attribute_id' => 'int',
51 ],
52 ],
53 'delete_bulk_product_attributes' => [
54 'callback' => [ $this, 'delete_bulk_product_attributes' ],
55 'fields' => [
56 'attribute_ids' => 'string',
57 ],
58 ],
59 // attribute term
60 'get_product_attribute_terms' => [
61 'callback' => [ $this, 'get_product_attribute_terms' ],
62 'fields' => [
63 'attribute_id' => 'int',
64 'search' => '',
65 ],
66 ],
67 'create_product_attribute_term' => [
68 'callback' => [ $this, 'create_product_attribute_term' ],
69 'fields' => [
70 'attribute_id' => 'int',
71 'id' => 'int',
72 'name' => 'string',
73 'slug' => 'string',
74 'description' => 'string',
75 'color' => 'string',
76 'image_id' => 'absint',
77 ],
78 ],
79 'update_product_attribute_term' => [
80 'callback' => [ $this, 'update_product_attribute_term' ],
81 'fields' => [
82 'attribute_id' => 'int',
83 'term_id' => 'int',
84 'name' => 'string',
85 'slug' => 'string',
86 'description' => 'string',
87 'color' => 'string',
88 'image_id' => 'absint',
89 ],
90 ],
91 'delete_product_attribute_term' => [
92 'callback' => [ $this, 'delete_product_attribute_term' ],
93 'fields' => [
94 'attribute_id' => 'int',
95 'term_id' => 'int',
96 ],
97 ],
98 );
99 }
100
101 public function get_product_attributes( array $payload ) {
102 $page = $payload['page'] ?? 1;
103 $per_page = $payload['per_page'] ?? 10;
104 $search = $payload['search'] ?? '';
105
106 $attributes = Helper::get_product_attributes( [
107 'per_page' => $per_page,
108 'page' => $page,
109 'search' => $search,
110 ] );
111
112 wp_send_json_success( [
113 'totalItems' => Helper::get_total_product_attributes_count( $search ),
114 'attributes' => array_map( fn( $attribute ) => [
115 'attribute_id' => $attribute->get_id(),
116 'attribute_name' => $attribute->get_name(),
117 'attribute_label' => $attribute->get_label(),
118 'attribute_orderby' => $attribute->get_orderby(),
119 'attribute_type' => $attribute->get_type(),
120 'attribute_public' => $attribute->is_public(),
121 'attribute_terms' => implode( ', ', array_map( fn( $term ) => $term->name, $attribute->get_terms() ) ),
122 ], $attributes ),
123 ] );
124 }
125
126 public function create_product_attribute( array $payload ) {
127 if ( empty( $payload['attribute_label'] ) ) {
128 wp_send_json_error( __( 'Please, provide an attribute name.', 'storeengine' ) );
129 }
130
131 // Set the attribute slug.
132 if ( empty( $payload['attribute_name'] ) ) {
133 $slug = Helper::sanitize_taxonomy_name( $payload['attribute_label'] );
134 } else {
135 $slug = Helper::strip_attribute_taxonomy_name( Helper::sanitize_taxonomy_name( $payload['attribute_name'] ) );
136 }
137
138 // Taxonomy max length is 32.
139 // se_pa_ length 6 (remain 32-6-1 = 25)
140
141 // Validate slug.
142 if ( strlen( $slug ) > 25 ) {
143 /* translators: %s: attribute slug */
144 wp_send_json_error( sprintf( __( 'Slug "%s" is too long (25 characters max). Shorten it, please.', 'storeengine' ), $slug ), array( 'status' => 400 ) );
145 } elseif ( Helper::check_if_attribute_name_is_reserved( $slug ) ) {
146 /* translators: %s: attribute slug */
147 wp_send_json_error( sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'storeengine' ), $slug ), array( 'status' => 400 ) );
148 } elseif ( taxonomy_exists( Helper::get_attribute_taxonomy_name( $slug ) ) ) {
149 /* translators: %s: attribute slug */
150 wp_send_json_error( sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'storeengine' ), $slug ) );
151 }
152
153 if ( empty( $payload['attribute_orderby'] ) || ! in_array( $payload['attribute_orderby'], [ 'name', 'custom-ordering', 'numeric-name', 'term_id' ], true ) ) {
154 $payload['attribute_orderby'] = 'custom-ordering';
155 }
156
157 $attribute = new Attribute();
158 $attribute->set_name( $slug );
159 $attribute->set_label( $payload['attribute_label'] );
160 $attribute->set_type( $this->sanitize_attribute_type( $payload['attribute_type'] ?? '' ) );
161 $attribute->set_orderby( $payload['attribute_orderby'] );
162 $attribute->set_public( $payload['attribute_public'] ?? false );
163
164 try {
165 $attribute->save();
166 } catch ( Exception $e ) {
167 wp_send_json_error( $e->getMessage() );
168 }
169
170 $attribute->get();
171
172 wp_send_json_success( [
173 'attribute_id' => $attribute->get_id(),
174 'attribute_name' => $attribute->get_name(),
175 'attribute_label' => $attribute->get_label(),
176 'attribute_orderby' => $attribute->get_orderby(),
177 'attribute_type' => $attribute->get_type(),
178 'attribute_public' => $attribute->is_public(),
179 ] );
180 }
181
182 public function update_product_attribute( array $payload ) {
183 if ( empty( $payload['attribute_id'] ) ) {
184 wp_send_json_error( __( 'Invalid request.', 'storeengine' ) );
185 }
186
187 $attribute = Helper::get_product_attribute( $payload['attribute_id'] );
188
189 if ( ! $attribute ) {
190 wp_send_json_error( __( 'Attribute not found', 'storeengine' ) );
191 }
192
193 if ( empty( $payload['attribute_label'] ) ) {
194 wp_send_json_error( __( 'Please, provide an attribute name.', 'storeengine' ) );
195 }
196
197 // Set the attribute slug.
198 if ( empty( $payload['attribute_name'] ) ) {
199 $slug = Helper::sanitize_taxonomy_name( $payload['attribute_label'] );
200 } else {
201 $slug = Helper::strip_attribute_taxonomy_name( Helper::sanitize_taxonomy_name( $payload['attribute_name'] ) );
202 }
203
204 // Taxonomy max length is 32.
205 // se_pa_ length 6 (remain 32-6-1 = 25)
206
207 // Validate slug.
208 if ( strlen( $slug ) > 25 ) {
209 /* translators: %s: attribute slug */
210 wp_send_json_error( sprintf( __( 'Slug "%s" is too long (25 characters max). Shorten it, please.', 'storeengine' ), $slug ), array( 'status' => 400 ) );
211 } elseif ( Helper::check_if_attribute_name_is_reserved( $slug ) ) {
212 /* translators: %s: attribute slug */
213 wp_send_json_error( sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'storeengine' ), $slug ), array( 'status' => 400 ) );
214 } elseif ( $slug !== $attribute->get_name() && taxonomy_exists( Helper::get_attribute_taxonomy_name( $slug ) ) ) {
215 /* translators: %s: attribute slug */
216 wp_send_json_error( sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'storeengine' ), $slug ) );
217 }
218
219 if ( empty( $payload['attribute_orderby'] ) || ! in_array( $payload['attribute_orderby'], [ 'name', 'custom-ordering', 'numeric-name', 'term_id' ], true ) ) {
220 $payload['attribute_orderby'] = 'custom-ordering';
221 }
222
223 $old_slug = $attribute->get_name();
224 $attribute->set_name( $slug );
225 $attribute->set_label( $payload['attribute_label'] );
226 $attribute->set_type( $this->sanitize_attribute_type( $payload['attribute_type'] ?? $attribute->get_type() ) );
227 $attribute->set_orderby( $payload['attribute_orderby'] );
228 $attribute->set_public( $payload['attribute_public'] );
229
230 try {
231 global $wpdb;
232
233 // Save new data.
234 $attribute->save();
235
236 $new_taxonomy = Helper::get_attribute_taxonomy_name( $slug );
237 $old_taxonomy = Helper::get_attribute_taxonomy_name( $old_slug );
238
239 // Update taxonomies in the wp term taxonomy table.
240 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
241 $wpdb->update( $wpdb->term_taxonomy, [ 'taxonomy' => $new_taxonomy ], [ 'taxonomy' => $old_taxonomy ] );
242 wp_cache_flush_group( $old_taxonomy . '_relationships' );
243 wp_cache_set_terms_last_changed();
244
245 // Update prefix in attribute order.
246 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
247 $results = $wpdb->get_results( $wpdb->prepare( "SELECT meta_id, meta_value FROM $wpdb->postmeta WHERE meta_key = '_storeengine_product_attributes_order' AND meta_value LIKE %s", '%' . $old_taxonomy . '%' ) );
248
249 foreach ( $results as $meta ) {
250 $meta_value = maybe_unserialize( $meta->meta_value );
251 if ( ! is_array( $meta_value ) ) {
252 $meta_value = json_decode( $meta->meta_value, true );
253 }
254
255 foreach ( $meta_value as $k => $v ) {
256 if ( $old_taxonomy === $v ) {
257 $meta_value[ $k ] = $new_taxonomy;
258 }
259 }
260
261 $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
262 $wpdb->postmeta,
263 [ 'meta_value' => maybe_serialize( $meta_value ) ], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
264 [ 'meta_id' => $meta->meta_id ],
265 );
266 }
267
268 wp_send_json_success( [
269 'attribute_id' => $attribute->get_id(),
270 'attribute_name' => $attribute->get_name(),
271 'attribute_label' => $attribute->get_label(),
272 'attribute_orderby' => $attribute->get_orderby(),
273 'attribute_type' => $attribute->get_type(),
274 'attribute_public' => $attribute->is_public(),
275 ] );
276 } catch ( Exception $e ) {
277 wp_send_json_error( $e->getMessage() );
278 }
279 }
280
281 public function delete_product_attribute( array $payload ) {
282 if ( ! isset( $payload['attribute_id'] ) || $payload['attribute_id'] <= 0 ) {
283 wp_send_json_error( __( 'Invalid attribute id', 'storeengine' ) );
284 }
285
286 $attribute = Helper::delete_product_attribute( $payload['attribute_id'] );
287
288 if ( ! $attribute ) {
289 wp_send_json_error( __( 'Attribute could not deleted', 'storeengine' ) );
290 }
291
292 wp_send_json_success();
293 }
294
295 public function delete_bulk_product_attributes( array $payload ) {
296 $attribute_ids = $payload['attribute_ids'] ?? '';
297 $attribute_ids = explode( ',', $attribute_ids );
298
299 if ( empty( $attribute_ids ) ) {
300 wp_send_json_error( __( 'Attribute IDs not found', 'storeengine' ) );
301 }
302
303 foreach ( $attribute_ids as $attribute_id ) {
304 $attribute = Helper::get_product_attribute( $attribute_id );
305 if ( $attribute ) {
306 $attribute->delete();
307 }
308 }
309
310 wp_send_json_success();
311 }
312
313 public function get_product_attribute_terms( array $payload ) {
314 $attribute = Helper::get_product_attribute( $payload['attribute_id'] );
315
316 if ( ! $attribute ) {
317 wp_send_json_error( __( 'Invalid Attribute.', 'storeengine' ) );
318 }
319
320 $args = [];
321 if ( isset( $payload['search'] ) ) {
322 $args['search'] = $payload['search'];
323 }
324 $terms = $attribute->get_terms( $args );
325 if ( is_wp_error( $terms ) ) {
326 wp_send_json_error( $terms->get_error_message() );
327 }
328
329 wp_send_json_success( array_map( [ $this, 'format_attribute_term' ], $terms ) );
330 }
331
332 public function create_product_attribute_term( array $payload ) {
333 $attribute = Helper::get_product_attribute( $payload['attribute_id'] );
334
335 if ( ! $attribute ) {
336 wp_send_json_error( __( 'Invalid Attribute.', 'storeengine' ) );
337 }
338
339 $term = $attribute->add_term( $payload['name'], [
340 'slug' => $payload['slug'] ?? '',
341 'description' => $payload['description'] ?? '',
342 ] );
343
344 if ( is_wp_error( $term ) ) {
345 wp_send_json_error( $term->get_error_message() );
346 }
347
348 $this->save_term_swatch_meta( (int) $term['term_id'], $payload );
349
350 wp_send_json_success( $this->format_attribute_term( get_term( (int) $term['term_id'] ) ) );
351 }
352
353 public function update_product_attribute_term( array $payload ) {
354 $attribute = Helper::get_product_attribute( $payload['attribute_id'] );
355
356 if ( ! $attribute ) {
357 wp_send_json_error( __( 'Invalid Attribute.', 'storeengine' ) );
358 }
359
360 $term = $attribute->update_term( $payload['term_id'], $payload['name'], [
361 'slug' => $payload['slug'],
362 'description' => $payload['description'],
363 ] );
364
365 if ( is_wp_error( $term ) ) {
366 wp_send_json_error( $term->get_error_message() );
367 }
368
369 $this->save_term_swatch_meta( (int) $payload['term_id'], $payload );
370
371 wp_send_json_success( $this->format_attribute_term( get_term( (int) $payload['term_id'] ) ) );
372 }
373
374 public function delete_product_attribute_term( array $payload ) {
375 $attribute = Helper::get_product_attribute( $payload['attribute_id'] );
376
377 if ( ! $attribute ) {
378 wp_send_json_error( __( 'Invalid Attribute.', 'storeengine' ) );
379 }
380
381 $result = $attribute->delete_term( $payload['term_id'] );
382
383 if ( is_wp_error( $result ) ) {
384 wp_send_json_error( $result->get_error_message() );
385 }
386
387 wp_send_json_success( __( 'Term deleted', 'storeengine' ) );
388 }
389
390 /**
391 * Meta key holding an attribute term's swatch color value.
392 */
393 const TERM_COLOR_META = '_storeengine_swatch_color';
394
395 /**
396 * Meta key holding an attribute term's swatch image attachment id.
397 */
398 const TERM_IMAGE_META = '_storeengine_swatch_image';
399
400 /**
401 * Restrict the attribute display type to the values the UI supports.
402 *
403 * @param string $type Raw type from the request.
404 *
405 * @return string One of select|color|image.
406 */
407 private function sanitize_attribute_type( string $type ): string {
408 $type = sanitize_key( $type );
409
410 return in_array( $type, [ 'select', 'color', 'image' ], true ) ? $type : 'select';
411 }
412
413 /**
414 * Persist (or clear) the color / image swatch meta for a term.
415 *
416 * Only keys present in the payload are touched, so partial updates are safe.
417 *
418 * @param int $term_id Term id.
419 * @param array $payload Request payload.
420 */
421 private function save_term_swatch_meta( int $term_id, array $payload ): void {
422 if ( ! $term_id ) {
423 return;
424 }
425
426 if ( array_key_exists( 'color', $payload ) ) {
427 $color = sanitize_text_field( $payload['color'] );
428 if ( '' === $color ) {
429 delete_term_meta( $term_id, self::TERM_COLOR_META );
430 } else {
431 update_term_meta( $term_id, self::TERM_COLOR_META, $color );
432 }
433 }
434
435 if ( array_key_exists( 'image_id', $payload ) ) {
436 $image_id = absint( $payload['image_id'] );
437 if ( ! $image_id ) {
438 delete_term_meta( $term_id, self::TERM_IMAGE_META );
439 } else {
440 update_term_meta( $term_id, self::TERM_IMAGE_META, $image_id );
441 }
442 }
443 }
444
445 /**
446 * Normalize a term into the array shape the admin app consumes, with the
447 * swatch color / image meta attached.
448 *
449 * @param \WP_Term|int $term Term object or id.
450 *
451 * @return array
452 */
453 public function format_attribute_term( $term ): array {
454 if ( is_numeric( $term ) ) {
455 $term = get_term( (int) $term );
456 }
457
458 if ( ! $term || is_wp_error( $term ) ) {
459 return [];
460 }
461
462 $data = (array) $term;
463 $image_id = (int) get_term_meta( $term->term_id, self::TERM_IMAGE_META, true );
464
465 $data['color'] = (string) get_term_meta( $term->term_id, self::TERM_COLOR_META, true );
466 $data['image_id'] = $image_id;
467 $data['image'] = $image_id ? ( wp_get_attachment_image_url( $image_id, 'thumbnail' ) ?: '' ) : '';
468
469 return $data;
470 }
471 }
472