PluginProbe
Cooked – Recipe Management / 1.16.1
Cooked – Recipe Management v1.16.1
1.16.1 1.16.0 1.15.0 trunk 1.10.0 1.11.0 1.11.1 1.11.2 1.11.3 1.11.4 1.12.0 1.13.0 1.14.0 1.7.10 1.7.11 1.7.12 1.7.13 1.7.15.1 1.7.15.3 1.7.15.4 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 All 37 releases
cooked / includes / class.cooked-ajax.php

class.cooked-ajax.php in Cooked – Recipe Management 1.16.1, at includes/class.cooked-ajax.php

594 lines 22.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Cooked AJAX-Specific Functions
4 *
5 * @package Cooked
6 * @subpackage AJAX-Specific Functions
7 * @since 1.0.0
8 */
9
10 // Exit if accessed directly
11 if ( ! defined( 'ABSPATH' ) ) exit;
12
13 /**
14 * Cooked_Ajax Class
15 *
16 * This class handles the Cooked Recipe Meta Box creation.
17 *
18 * @since 1.0.0
19 */
20 class Cooked_Ajax {
21
22 function __construct() {
23 /**
24 * Back-End Ajax
25 */
26
27 // Save Default Template
28 add_action( 'wp_ajax_cooked_save_default', [&$this, 'save_default'] );
29
30 // Save Default Template in Bulk
31 add_action( 'wp_ajax_cooked_save_default_bulk', [&$this, 'save_default_bulk'] );
32
33 // Load Default Template
34 add_action( 'wp_ajax_cooked_load_default', [&$this, 'load_default'] );
35
36 // Get JSON list of Recipe IDs
37 add_action( 'wp_ajax_cooked_get_recipe_ids', [&$this, 'get_recipe_ids'] );
38
39 // Get Recipe Count
40 add_action( 'wp_ajax_cooked_get_recipe_count', [&$this, 'get_recipe_count'] );
41
42 // Get JSON list of Recipe IDs, ready for Migration
43 add_action( 'wp_ajax_cooked_get_migrate_ids', [&$this, 'get_migrate_ids'] );
44
45 // Get JSON list of Recipe IDs, ready for Import
46 add_action( 'wp_ajax_cooked_get_import_ids', [&$this, 'get_import_ids']);
47
48 // Migrate Recipes
49 add_action( 'wp_ajax_cooked_migrate_recipes', [&$this, 'migrate_recipes'] );
50
51 // Import Recipes
52 add_action( 'wp_ajax_cooked_import_recipes', [&$this, 'import_recipes']);
53
54 // CSV Import - Upload file
55 add_action( 'wp_ajax_cooked_upload_csv', [&$this, 'upload_csv']);
56
57 // CSV Import - Process file
58 add_action( 'wp_ajax_cooked_process_csv', [&$this, 'process_csv']);
59
60 // Bulk Add - Parse Ingredients
61 add_action( 'wp_ajax_cooked_parse_bulk_ingredients', [&$this, 'parse_bulk_ingredients'] );
62 add_action( 'wp_ajax_nopriv_cooked_parse_bulk_ingredients', [&$this, 'parse_bulk_ingredients'] );
63 }
64
65 private static function recipe_ids_from_json( $json ) {
66 $decoded = json_decode( $json, true );
67 if ( ! is_array( $decoded ) || empty( $decoded ) ) {
68 return [];
69 }
70
71 $ids = [];
72 foreach ( $decoded as $rid ) {
73 $safe_id = absint( $rid );
74 if ( $safe_id ) {
75 $ids[] = $safe_id;
76 }
77 }
78
79 return $ids;
80 }
81
82 private static function sanitize_import_type( $import_type ) {
83 if ( ! in_array( $import_type, [ 'delicious_recipes', 'wp_recipe_maker' ], true ) ) {
84 return '';
85 }
86 return $import_type;
87 }
88
89 private static function require_settings_ajax( $nonce_action ) {
90 check_ajax_referer( $nonce_action, 'nonce' );
91 if ( ! current_user_can( 'edit_cooked_settings' ) ) {
92 wp_die();
93 }
94 }
95
96 private static function user_can_edit_post_of_type( $post_id, $post_type ) {
97 return $post_type && get_post_type( $post_id ) === $post_type && current_user_can( 'edit_post', $post_id );
98 }
99
100 public function get_migrate_ids() {
101 self::require_settings_ajax( 'cooked_migrate_recipes' );
102
103 $old_recipes = get_transient('cooked_classic_recipes');
104 if ($old_recipes != 'complete'):
105 $total = count($old_recipes);
106
107 if ($total > 0):
108 echo wp_json_encode($old_recipes);
109 else:
110 echo 'false';
111 endif;
112 else:
113 echo 'false';
114 endif;
115
116 wp_die();
117 }
118
119 public function get_import_ids() {
120 self::require_settings_ajax( 'cooked_import_recipes' );
121
122 $import_type = isset( $_POST['import_type'] ) ? sanitize_key( wp_unslash( $_POST['import_type'] ) ) : '';
123 $import_type = self::sanitize_import_type( $import_type );
124 $recipes = [];
125
126 if ( $import_type === 'delicious_recipes' ) {
127 $args = [
128 'post_type' => 'recipe',
129 'posts_per_page' => -1,
130 'post_status' => 'any',
131 'fields' => 'ids',
132 'meta_query' => [
133 [
134 'key' => 'delicious_recipes_metadata',
135 'compare' => 'EXISTS',
136 ],
137 ],
138 ];
139 } elseif ( $import_type === 'wp_recipe_maker' ) {
140 $args = [
141 'post_type' => 'wprm_recipe',
142 'posts_per_page' => -1,
143 'post_status' => 'any',
144 'fields' => 'ids',
145 'meta_query' => [
146 [
147 'key' => 'wprm_type',
148 'value' => 'food',
149 'compare' => '=',
150 ],
151 ],
152 ];
153 } else {
154 echo 'false';
155 wp_die();
156 }
157
158 $_recipes = new WP_Query( $args );
159
160 if (!empty($_recipes->posts)) {
161 foreach ($_recipes->posts as $rid) {
162 $recipes[] = $rid;
163 }
164 }
165
166 if (!empty($recipes)) {
167 $total = count($recipes);
168
169 if ($total > 0) {
170 echo wp_json_encode($recipes);
171 } else {
172 echo 'false';
173 }
174 } else {
175 echo 'false';
176 }
177
178 wp_die();
179 }
180
181 public function migrate_recipes() {
182 $bulk_amount = 10;
183
184 self::require_settings_ajax( 'cooked_migrate_recipes' );
185
186 $recipe_ids = self::recipe_ids_from_json( isset( $_POST['recipe_ids'] ) ? wp_unslash( $_POST['recipe_ids'] ) : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded then absint in recipe_ids_from_json.
187 if ( empty( $recipe_ids ) ) {
188 wp_die();
189 }
190
191 $leftover_recipe_ids = array_slice( $recipe_ids, $bulk_amount );
192 $recipe_ids = array_slice( $recipe_ids, 0, $bulk_amount );
193
194 if ( ! empty( $recipe_ids ) ) {
195 foreach ( $recipe_ids as $rid ) {
196 if ( ! self::user_can_edit_post_of_type( $rid, 'cp_recipe' ) ) {
197 continue;
198 }
199
200 $recipe_settings = Cooked_Recipes::get_settings( $rid );
201
202 if ( ! empty( $recipe_settings ) && ! isset( $recipe_settings['cooked_version'] ) || ! empty( $recipe_settings ) && isset( $recipe_settings['cooked_version'] ) && ! $recipe_settings['cooked_version'] ) {
203 $recipe_settings['cooked_version'] = COOKED_VERSION;
204 update_post_meta( $rid, '_recipe_settings', $recipe_settings );
205 $recipe_excerpt = isset( $recipe_settings['excerpt'] ) && $recipe_settings['excerpt'] ? $recipe_settings['excerpt'] : get_the_title( $rid );
206
207 $seo_content = apply_filters( 'cooked_seo_recipe_content', '[cooked-excerpt]<h2>' . __( 'Ingredients', 'cooked' ) . '</h2>[cooked-ingredients checkboxes=false]<h2>' . __( 'Directions', 'cooked' ) . '</h2>[cooked-directions numbers=false]' );
208 $seo_content = do_shortcode( $seo_content );
209
210 wp_update_post( [
211 'ID' => $rid,
212 'post_excerpt' => $recipe_excerpt,
213 'post_content' => $seo_content,
214 ] );
215 }
216 }
217
218 if ( ! empty( $leftover_recipe_ids ) ) {
219 echo wp_json_encode( $leftover_recipe_ids );
220 wp_die();
221 }
222 }
223
224 set_transient( 'cooked_classic_recipes', 'complete', 60 * 60 * 24 * 7 );
225 echo 'false';
226 wp_die();
227 }
228
229 public function import_recipes() {
230 self::require_settings_ajax( 'cooked_import_recipes' );
231
232 require_once COOKED_DIR . 'includes/class.cooked-delicious-recipes.php';
233 require_once COOKED_DIR . 'includes/class.cooked-recipe-maker.php';
234
235 $bulk_amount = 10;
236 $recipe_ids = self::recipe_ids_from_json( isset( $_POST['recipe_ids'] ) ? wp_unslash( $_POST['recipe_ids'] ) : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON decoded then absint in recipe_ids_from_json.
237 $import_type = isset( $_POST['import_type'] ) ? sanitize_key( wp_unslash( $_POST['import_type'] ) ) : '';
238 $import_type = self::sanitize_import_type( $import_type );
239
240 if ( empty( $recipe_ids ) || ! $import_type ) {
241 wp_die();
242 }
243
244 $expected_type = $import_type === 'delicious_recipes' ? 'recipe' : 'wprm_recipe';
245 $leftover_recipe_ids = array_slice( $recipe_ids, $bulk_amount );
246 $recipe_ids = array_slice( $recipe_ids, 0, $bulk_amount );
247
248 if ( ! empty( $recipe_ids ) ) {
249 foreach ( $recipe_ids as $rid ) {
250 if ( ! self::user_can_edit_post_of_type( $rid, $expected_type ) ) {
251 continue;
252 }
253
254 if ( $import_type === 'delicious_recipes' ) {
255 Cooked_Delicious_Recipes::import_recipe( $rid );
256 } elseif ( $import_type === 'wp_recipe_maker' ) {
257 Cooked_Recipe_Maker_Recipes::import_recipe( $rid );
258 }
259 }
260
261 if ( ! empty( $leftover_recipe_ids ) ) {
262 echo wp_json_encode( $leftover_recipe_ids );
263 wp_die();
264 }
265
266 if ( $import_type === 'delicious_recipes' ) {
267 update_option( 'cooked_delicious_recipes_imported', true );
268 } elseif ( $import_type === 'wp_recipe_maker' ) {
269 update_option( 'cooked_wp_recipe_maker_imported', true );
270 }
271 }
272
273 echo 'false';
274 wp_die();
275 }
276
277 public function get_recipe_ids() {
278 $nonce = isset( $_POST['nonce'] ) ? wp_unslash( $_POST['nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verify nonce after unslash; do not sanitize_text_field a nonce.
279 if ( ! wp_verify_nonce( $nonce, 'cooked_save_default_bulk' ) || ! current_user_can( 'edit_cooked_default_template' ) ) {
280 wp_die();
281 }
282
283 $args = [
284 'post_type' => 'cp_recipe',
285 'posts_per_page' => -1,
286 'post_status' => 'any',
287 'fields' => 'ids'
288 ];
289
290 $_recipe_ids = Cooked_Recipes::get($args, false, true);
291 echo wp_json_encode($_recipe_ids);
292 wp_die();
293 }
294
295 public function get_recipe_count() {
296 $nonce = isset( $_POST['nonce'] ) ? wp_unslash( $_POST['nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verify nonce after unslash; do not sanitize_text_field a nonce.
297 if ( ! wp_verify_nonce( $nonce, 'cooked_save_default_bulk' ) || ! current_user_can( 'edit_cooked_default_template' ) ) {
298 wp_die();
299 }
300
301 $args = [
302 'post_type' => 'cp_recipe',
303 'posts_per_page' => 1,
304 'post_status' => 'any',
305 'fields' => 'ids',
306 ];
307
308 $query = new WP_Query( $args );
309 wp_send_json_success( [ 'total' => $query->found_posts ] );
310 }
311
312 public function save_default_bulk() {
313 $per_page = 20;
314
315 $nonce = isset( $_POST['nonce'] ) ? wp_unslash( $_POST['nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verify nonce after unslash; do not sanitize_text_field a nonce.
316 if ( ! wp_verify_nonce( $nonce, 'cooked_save_default_bulk' ) || ! current_user_can( 'edit_cooked_default_template' ) ) {
317 wp_die();
318 }
319
320 if ( ! isset( $_POST['default_content'] ) ) {
321 wp_send_json_error( [ 'message' => __( 'No default content provided.', 'cooked' ) ] );
322 }
323
324 $page = isset( $_POST['page'] ) ? absint( wp_unslash( $_POST['page'] ) ) : 0;
325 $content = wp_kses_post( wp_unslash( $_POST['default_content'] ) );
326
327 $args = [
328 'post_type' => 'cp_recipe',
329 'posts_per_page' => $per_page,
330 'offset' => $page * $per_page,
331 'post_status' => 'any',
332 'fields' => 'ids',
333 'orderby' => 'ID',
334 'order' => 'ASC',
335 ];
336
337 $query = new WP_Query( $args );
338 $recipe_ids = $query->posts;
339 $updated = 0;
340
341 foreach ($recipe_ids as $rid) {
342 $recipe_settings = get_post_meta($rid, '_recipe_settings', true);
343 if (!empty($recipe_settings)) {
344 $recipe_settings['content'] = $content;
345 update_post_meta($rid, '_recipe_settings', $recipe_settings);
346 $updated++;
347 }
348 }
349
350 $processed = ( $page * $per_page ) + count( $recipe_ids );
351 $has_more = $processed < $query->found_posts;
352
353 wp_send_json_success( [
354 'updated' => $updated,
355 'has_more' => $has_more,
356 ] );
357 }
358
359 public function save_default() {
360 global $_cooked_settings;
361
362 $nonce = isset( $_POST['nonce'] ) ? wp_unslash( $_POST['nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verify nonce after unslash; do not sanitize_text_field a nonce.
363 if ( ! wp_verify_nonce( $nonce, 'cooked_save_default' ) || ! current_user_can( 'edit_cooked_default_template' ) ) {
364 wp_die();
365 }
366
367 if ( isset( $_POST['default_content'] ) ) {
368 $_cooked_settings['default_content'] = wp_kses_post( wp_unslash( $_POST['default_content'] ) );
369 update_option('cooked_settings', $_cooked_settings);
370 } else {
371 echo esc_html__( 'No default content provided.', 'cooked' );
372 }
373
374 wp_die();
375 }
376
377 public function load_default() {
378 global $_cooked_settings;
379
380 if (!current_user_can('edit_cooked_recipes')) {
381 wp_die();
382 }
383
384 if (isset($_cooked_settings['default_content'])) {
385 $default_content = wp_unslash($_cooked_settings['default_content']);
386 } else {
387 $default_content = Cooked_Recipes::default_content();
388 }
389
390 echo wp_kses_post($default_content);
391
392 wp_die();
393 }
394
395 /**
396 * Handle CSV file upload
397 */
398 public function upload_csv() {
399 $nonce = isset( $_POST['nonce'] ) ? wp_unslash( $_POST['nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verify nonce after unslash; do not sanitize_text_field a nonce.
400 if ( ! wp_verify_nonce( $nonce, 'cooked_admin_import' ) || ! current_user_can( 'edit_cooked_settings' ) ) {
401 wp_send_json_error(['message' => __('You do not have permission to import recipes.', 'cooked')]);
402 }
403
404 if ( ! isset( $_FILES['csv_file'] ) || ! isset( $_FILES['csv_file']['error'] ) || UPLOAD_ERR_OK !== (int) $_FILES['csv_file']['error'] ) {
405 wp_send_json_error(['message' => __('File upload failed.', 'cooked')]);
406 }
407
408 $csv_name = isset( $_FILES['csv_file']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['csv_file']['name'] ) ) : '';
409 if ( ! $csv_name ) {
410 wp_send_json_error(['message' => __('File upload failed.', 'cooked')]);
411 }
412 $_FILES['csv_file']['name'] = $csv_name;
413
414 // Validate file type
415 $file_type = wp_check_filetype( $csv_name );
416 if ($file_type['ext'] !== 'csv') {
417 wp_send_json_error(['message' => __('Invalid file type. Please upload a CSV file.', 'cooked')]);
418 }
419
420 // Use WordPress upload handler
421 require_once ABSPATH . 'wp-admin/includes/file.php';
422 $upload = wp_handle_upload($_FILES['csv_file'], ['test_form' => false]);
423
424 if (isset($upload['error'])) {
425 wp_send_json_error(['message' => $upload['error']]);
426 }
427
428 // Store file path in transient for processing
429 $transient_key = 'cooked_csv_import_' . get_current_user_id() . '_' . time();
430 set_transient($transient_key, $upload['file'], 3600); // 1 hour
431
432 wp_send_json_success([
433 'transient_key' => $transient_key,
434 'file_path' => $upload['file']
435 ]);
436 }
437
438 /**
439 * Process CSV file and import recipes
440 */
441 public function process_csv() {
442 $nonce = isset( $_POST['nonce'] ) ? wp_unslash( $_POST['nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verify nonce after unslash; do not sanitize_text_field a nonce.
443 if ( ! wp_verify_nonce( $nonce, 'cooked_admin_import' ) || ! current_user_can( 'edit_cooked_settings' ) ) {
444 wp_send_json_error(['message' => __('You do not have permission to import recipes.', 'cooked')]);
445 }
446
447 $transient_key = isset( $_POST['transient_key'] ) ? sanitize_text_field( wp_unslash( $_POST['transient_key'] ) ) : '';
448 $file_path = get_transient($transient_key);
449
450 if (!$file_path || !file_exists($file_path)) {
451 wp_send_json_error(['message' => __('CSV file not found. Please upload again.', 'cooked')]);
452 }
453
454 // Process the CSV file
455 require_once COOKED_DIR . 'includes/class.cooked-csv-import.php';
456 $results = Cooked_CSV_Import::import_from_file($file_path);
457
458 // Clean up
459 if (file_exists($file_path)) {
460 wp_delete_file($file_path);
461 }
462 delete_transient($transient_key);
463
464 if ($results['success'] > 0) {
465 wp_send_json_success([
466 'message' => sprintf(
467 /* translators: %d: number of recipes imported */
468 __('Successfully imported %d recipe(s).', 'cooked'),
469 $results['success']
470 ),
471 'success' => $results['success'],
472 'total' => $results['total'],
473 'errors' => $results['errors']
474 ]);
475 } else {
476 wp_send_json_error([
477 'message' => __('No recipes were imported.', 'cooked'),
478 'errors' => $results['errors']
479 ]);
480 }
481 }
482
483 public function parse_bulk_ingredients() {
484 $nonce = isset( $_POST['nonce'] ) ? wp_unslash( $_POST['nonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Verify nonce after unslash; do not sanitize_text_field a nonce.
485 if ( ! wp_verify_nonce( $nonce, 'cooked_bulk_add' ) ) {
486 wp_send_json_error( [ 'message' => __( 'Security check failed.', 'cooked' ) ] );
487 }
488
489 $lines = isset( $_POST['lines'] ) ? wp_unslash( (array) $_POST['lines'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each line is sanitize_text_field'd below.
490
491 if ( empty( $lines ) ) {
492 wp_send_json_error( [ 'message' => __( 'No ingredients provided.', 'cooked' ) ] );
493 }
494
495 $measurements = Cooked_Measurements::get();
496
497 $variations_map = [];
498 foreach ( $measurements as $key => $m ) {
499 if ( ! empty( $m['variations'] ) ) {
500 foreach ( $m['variations'] as $variation ) {
501 $variations_map[ $variation ] = $key;
502 }
503 }
504 }
505
506 // Sort variations longest-first to avoid partial matches.
507 $variation_strings = array_keys( $variations_map );
508 usort( $variation_strings, function( $a, $b ) {
509 return strlen( $b ) - strlen( $a );
510 });
511
512 $escaped = array_map( function( $v ) {
513 return preg_quote( $v, '/' );
514 }, $variation_strings );
515
516 $units_pattern = '/^(' . implode( '|', $escaped ) . ')\.?\s+/iu';
517
518 $parsed = [];
519
520 foreach ( $lines as $index => $line ) {
521 // Do not use Cooked_Functions::sanitize_text_field() here — it runs htmlentities() and turns
522 // Unicode like en dash or ½ into &ndash; / &frac12;, which breaks parsing and leaks into output.
523 $line = is_string( $line ) ? $line : '';
524 $line = html_entity_decode( $line, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
525 $line = trim( sanitize_text_field( $line ) );
526
527 if ( '' === $line ) {
528 $parsed[ $index ] = [ 'amount' => '', 'measurement' => '', 'name' => '' ];
529 continue;
530 }
531
532 $raw = $line;
533 $amount = '';
534 $measurement = '';
535
536 $raw = str_replace( "\xE2\x81\x84", '/', $raw );
537
538 $fraction_map = [
539 "\xC2\xBC" => '1/4', "\xC2\xBD" => '1/2', "\xC2\xBE" => '3/4',
540 "\xE2\x85\x93" => '1/3', "\xE2\x85\x94" => '2/3',
541 "\xE2\x85\x95" => '1/5', "\xE2\x85\x96" => '2/5',
542 "\xE2\x85\x97" => '3/5', "\xE2\x85\x98" => '4/5',
543 "\xE2\x85\x99" => '1/6', "\xE2\x85\x9A" => '5/6',
544 "\xE2\x85\x9B" => '1/8', "\xE2\x85\x9C" => '3/8',
545 "\xE2\x85\x9D" => '5/8', "\xE2\x85\x9E" => '7/8',
546 ];
547 // "1½" must become "1 1/2", not "11/2".
548 foreach ( $fraction_map as $symbol => $replacement ) {
549 $raw = preg_replace( '/(\d)' . preg_quote( $symbol, '/' ) . '/u', '$1 ' . $replacement, $raw );
550 }
551 foreach ( $fraction_map as $symbol => $replacement ) {
552 $raw = str_replace( $symbol, $replacement, $raw );
553 }
554
555 // Allow en dash (U+2013) and em dash (U+2014) in amounts like "2–3".
556 $amount_regex = '/^\s*([\d][\s\/\-\d.,\x{2013}\x{2014}]*)\s*/u';
557 if ( preg_match( $amount_regex, $raw, $match ) ) {
558 $amount = trim( $match[1] );
559 $raw = trim( substr( $raw, strlen( $match[0] ) ) );
560 }
561
562 if ( preg_match( $units_pattern, $raw, $match ) ) {
563 $matched_variation = trim( rtrim( $match[1], '.' ) );
564 $matched_lower = strtolower( $matched_variation );
565
566 foreach ( $variations_map as $variation => $key ) {
567 if ( strtolower( $variation ) === $matched_lower ) {
568 $measurement = $key;
569 break;
570 }
571 }
572
573 $raw = trim( substr( $raw, strlen( $match[0] ) ) );
574 }
575
576 $name = trim( $raw );
577
578 if ( ! $name ) {
579 $amount = '';
580 $measurement = '';
581 $name = trim( $line );
582 }
583
584 $parsed[ $index ] = [
585 'amount' => $amount,
586 'measurement' => $measurement,
587 'name' => $name,
588 ];
589 }
590
591 wp_send_json_success( [ 'parsed' => $parsed ] );
592 }
593 }
594