PluginProbe
Cooked – Recipe Management / trunk
Cooked – Recipe Management vtrunk
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 1.8.5 All 36 releases
cooked / includes / class.cooked-ajax.php

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

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