/** * CJ's Brew & BBQ - Form Validation Rules * * Handles all validation logic for recipe submission form * Includes both server-side and AJAX-based validations */ if (!defined('ABSPATH')) { exit; } /** * Main validation function for the entire recipe * Runs before database insertion */ function cjbb_validate_recipe($recipe_data) { $errors = array(); // Validate Basic Info $errors = array_merge($errors, cjbb_validate_basic_info($recipe_data)); // Validate Ingredients $errors = array_merge($errors, cjbb_validate_ingredients($recipe_data)); // Validate Instructions $errors = array_merge($errors, cjbb_validate_instructions($recipe_data)); // Validate Categories $errors = array_merge($errors, cjbb_validate_categories($recipe_data)); return $errors; } /** * Validate basic recipe information (Step 1) */ function cjbb_validate_basic_info($data) { $errors = array(); // Title validation if (empty($data['recipe_title'])) { $errors['title'] = 'Give your recipe a title so folks know what they\'re getting into.'; } elseif (strlen($data['recipe_title']) > 200) { $errors['title'] = 'Keep that title under 200 characters.'; } elseif (strlen($data['recipe_title']) < 3) { $errors['title'] = 'Title needs at least 3 characters.'; } // Type validation if (empty($data['recipe_type'])) { $errors['type'] = 'Let us know if this is a beer or BBQ recipe.'; } elseif (!in_array($data['recipe_type'], array('beer', 'bbq'))) { $errors['type'] = 'Recipe type must be either beer or BBQ.'; } // Difficulty validation if (empty($data['recipe_difficulty'])) { $errors['difficulty'] = 'Tell us how hard this recipe is to make.'; } elseif (!in_array($data['recipe_difficulty'], array('beginner', 'intermediate', 'advanced'))) { $errors['difficulty'] = 'Invalid difficulty level.'; } // Description validation if (empty($data['recipe_description'])) { $errors['description'] = 'Share what makes this recipe special. Other enthusiasts will want to know!'; } elseif (strlen($data['recipe_description']) > 1000) { $errors['description'] = 'Keep your description under 1000 characters.'; } elseif (strlen($data['recipe_description']) < 10) { $errors['description'] = 'Give us at least 10 characters to understand your recipe.'; } // Time validations (optional but if provided must be valid) if (!empty($data['recipe_prep_time'])) { if (!is_numeric($data['recipe_prep_time']) || $data['recipe_prep_time'] < 0 || $data['recipe_prep_time'] > 1440) { $errors['prep_time'] = 'Prep time should be between 0 and 1440 minutes.'; } } if (!empty($data['recipe_cook_time'])) { if (!is_numeric($data['recipe_cook_time']) || $data['recipe_cook_time'] < 0 || $data['recipe_cook_time'] > 10080) { $errors['cook_time'] = 'Cook time should be between 0 and 10080 minutes (7 days).'; } } if (!empty($data['recipe_yield'])) { if (!is_numeric($data['recipe_yield']) || $data['recipe_yield'] < 1 || $data['recipe_yield'] > 1000) { $errors['yield'] = 'Yield should be between 1 and 1000.'; } } return $errors; } /** * Validate ingredients list (Step 2) */ function cjbb_validate_ingredients($data) { $errors = array(); // Check if ingredients array exists and has at least one item if (empty($data['ingredients']) || !is_array($data['ingredients'])) { $errors['ingredients'] = 'Add at least one ingredient to your recipe.'; return $errors; } $ingredient_count = count($data['ingredients']); if ($ingredient_count < 1) { $errors['ingredients'] = 'Add at least one ingredient.'; } elseif ($ingredient_count > 100) { $errors['ingredients'] = 'Wow, that\'s a lot of ingredients! Keep it to 100 or fewer.'; } // Validate each ingredient foreach ($data['ingredients'] as $index => $ingredient) { $ingredient_index = 'ingredients_' . $index; // Item validation if (empty($ingredient['item'])) { $errors[$ingredient_index . '_item'] = 'Don\'t forget to name this ingredient.'; } elseif (strlen($ingredient['item']) > 200) { $errors[$ingredient_index . '_item'] = 'Keep ingredient names under 200 characters.'; } // Quantity validation if (empty($ingredient['quantity'])) { $errors[$ingredient_index . '_quantity'] = 'How much of this ingredient?'; } elseif (!is_numeric(str_replace(array('/', '.', ' '), '', $ingredient['quantity']))) { $errors[$ingredient_index . '_quantity'] = 'Use numbers or fractions (e.g., 2, 0.5, 1/2).'; } // Unit validation if (empty($ingredient['unit'])) { $errors[$ingredient_index . '_unit'] = 'What\'s the unit? (cups, grams, etc.)'; } elseif (strlen($ingredient['unit']) > 50) { $errors[$ingredient_index . '_unit'] = 'Keep unit names short.'; } } return $errors; } /** * Validate instructions/steps (Step 3) */ function cjbb_validate_instructions($data) { $errors = array(); // Check if instructions exist if (empty($data['instructions']) || !is_array($data['instructions'])) { $errors['instructions'] = 'Give us step-by-step instructions on how to make this.'; return $errors; } $instruction_count = count($data['instructions']); if ($instruction_count < 1) { $errors['instructions'] = 'Add at least one instruction step.'; } elseif ($instruction_count > 50) { $errors['instructions'] = 'That\'s a lot of steps! Try to simplify to 50 or fewer.'; } // Validate each instruction foreach ($data['instructions'] as $index => $instruction) { $instruction_index = 'instruction_' . $index; if (empty($instruction['step'])) { $errors[$instruction_index . '_step'] = 'What\'s step ' . ($index + 1) . '?'; } elseif (strlen($instruction['step']) > 1000) { $errors[$instruction_index . '_step'] = 'Keep each step under 1000 characters.'; } elseif (strlen($instruction['step']) < 5) { $errors[$instruction_index . '_step'] = 'Give us a bit more detail for this step.'; } // Temperature is optional, but if provided must be valid if (!empty($instruction['temperature'])) { if (!is_numeric($instruction['temperature'])) { $errors[$instruction_index . '_temp'] = 'Temperature should be a number (in Fahrenheit).'; } } // Time is optional, but if provided must be valid if (!empty($instruction['time'])) { if (!is_numeric($instruction['time']) || $instruction['time'] < 0) { $errors[$instruction_index . '_time'] = 'Time should be a positive number.'; } } } return $errors; } /** * Validate recipe categories and tags (Step 4) */ function cjbb_validate_categories($data) { $errors = array(); // At least one category should be selected if (empty($data['categories']) || !is_array($data['categories'])) { $errors['categories'] = 'Pick at least one category for your recipe.'; } elseif (count($data['categories']) > 10) { $errors['categories'] = 'Pick up to 10 categories, no more.'; } // Tags are optional but validate if provided if (!empty($data['tags'])) { if (is_array($data['tags'])) { if (count($data['tags']) > 20) { $errors['tags'] = 'Keep it to 20 tags or fewer.'; } // Validate individual tags foreach ($data['tags'] as $tag) { if (strlen($tag) > 50) { $errors['tags'] = 'Keep individual tags under 50 characters.'; break; } } } } return $errors; } /** * AJAX validation for individual fields (real-time) */ function cjbb_validate_field() { // Verify nonce if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'cjbb_recipe_nonce')) { wp_send_json_error(array('message' => 'Security check failed.')); } $field = isset($_POST['field']) ? sanitize_text_field($_POST['field']) : ''; $value = isset($_POST['value']) ? $_POST['value'] : ''; $error = ''; // Validate based on field type switch ($field) { case 'recipe_title': $value = sanitize_text_field($value); if (empty($value)) { $error = 'Recipe title is required.'; } elseif (strlen($value) < 3) { $error = 'Title needs at least 3 characters.'; } elseif (strlen($value) > 200) { $error = 'Keep title under 200 characters.'; } break; case 'recipe_description': $value = sanitize_textarea_field($value); if (empty($value)) { $error = 'Description is required.'; } elseif (strlen($value) < 10) { $error = 'Give us at least 10 characters.'; } elseif (strlen($value) > 1000) { $error = 'Keep it under 1000 characters.'; } break; case 'ingredient_item': $value = sanitize_text_field($value); if (empty($value)) { $error = 'Ingredient name is required.'; } elseif (strlen($value) > 200) { $error = 'Keep ingredient name under 200 characters.'; } break; case 'ingredient_quantity': if (empty($value)) { $error = 'Quantity is required.'; } break; case 'ingredient_unit': $value = sanitize_text_field($value); if (empty($value)) { $error = 'Unit is required.'; } break; case 'instruction_step': $value = sanitize_textarea_field($value); if (empty($value)) { $error = 'Step description is required.'; } elseif (strlen($value) < 5) { $error = 'Give more detail in this step.'; } elseif (strlen($value) > 1000) { $error = 'Keep it under 1000 characters.'; } break; } if ($error) { wp_send_json_error(array('message' => $error)); } else { wp_send_json_success(array('message' => 'Looks good!')); } } /** * Check for duplicate recipes * Prevents accidental duplicate submissions */ function cjbb_check_duplicate_recipe($user_id, $title, $type) { global $wpdb; $query = $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}recipes WHERE user_id = %d AND title = %s AND type = %s AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)", $user_id, $title, $type ); $result = $wpdb->get_var($query); return $result > 0 ? true : false; } /** * Sanitize recipe data before database insertion */ function cjbb_sanitize_recipe_data($recipe_data) { $sanitized = array(); // Text fields $text_fields = array('recipe_title', 'recipe_type', 'recipe_difficulty'); foreach ($text_fields as $field) { $sanitized[$field] = isset($recipe_data[$field]) ? sanitize_text_field($recipe_data[$field]) : ''; } // Textarea fields $sanitized['recipe_description'] = isset($recipe_data['recipe_description']) ? sanitize_textarea_field($recipe_data['recipe_description']) : ''; // Numeric fields $numeric_fields = array('recipe_prep_time', 'recipe_cook_time', 'recipe_yield'); foreach ($numeric_fields as $field) { $sanitized[$field] = isset($recipe_data[$field]) ? absint($recipe_data[$field]) : 0; } // Arrays (ingredients, instructions, categories, tags) if (isset($recipe_data['ingredients']) && is_array($recipe_data['ingredients'])) { $sanitized['ingredients'] = array(); foreach ($recipe_data['ingredients'] as $ingredient) { $sanitized['ingredients'][] = array( 'item' => sanitize_text_field($ingredient['item'] ?? ''), 'quantity' => sanitize_text_field($ingredient['quantity'] ?? ''), 'unit' => sanitize_text_field($ingredient['unit'] ?? ''), ); } } if (isset($recipe_data['instructions']) && is_array($recipe_data['instructions'])) { $sanitized['instructions'] = array(); foreach ($recipe_data['instructions'] as $instruction) { $sanitized['instructions'][] = array( 'step' => sanitize_textarea_field($instruction['step'] ?? ''), 'temperature' => absint($instruction['temperature'] ?? 0), 'time' => absint($instruction['time'] ?? 0), ); } } if (isset($recipe_data['categories']) && is_array($recipe_data['categories'])) { $sanitized['categories'] = array_map('absint', $recipe_data['categories']); } if (isset($recipe_data['tags']) && is_array($recipe_data['tags'])) { $sanitized['tags'] = array_map('sanitize_text_field', $recipe_data['tags']); } return $sanitized; } /** * CJ's Brew & BBQ - Recipe Form Handler * Processes recipe form submissions and manages database inserts */ if (!defined('ABSPATH')) { exit; } // Handle recipe submission function cjbb_handle_recipe_submission() { if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'cjbb_recipe_form_nonce')) { wp_send_json_error(array('message' => 'Security check failed.')); } if (!is_user_logged_in()) { wp_send_json_error(array('message' => 'You must be logged in.')); } $user_id = get_current_user_id(); $recipe_data = isset($_POST['recipe_data']) ? $_POST['recipe_data'] : array(); // Sanitize $recipe_data = cjbb_sanitize_recipe_data($recipe_data); // Validate $errors = cjbb_validate_recipe($recipe_data); if (!empty($errors)) { wp_send_json_error(array('message' => 'Fix errors:', 'errors' => $errors)); } // Check duplicates if (cjbb_check_duplicate_recipe($user_id, $recipe_data['recipe_title'], $recipe_data['recipe_type'])) { wp_send_json_error(array('message' => 'You just submitted this recipe.')); } $result = cjbb_insert_recipe($user_id, $recipe_data); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message())); } wp_send_json_success(array( 'message' => 'Recipe submitted!', 'recipe_id' => $result )); } // Insert recipe into database function cjbb_insert_recipe($user_id, $recipe_data) { global $wpdb; $wpdb->query('START TRANSACTION'); try { // Insert main recipe $inserted = $wpdb->insert( $wpdb->prefix . 'recipes', array( 'user_id' => $user_id, 'title' => $recipe_data['recipe_title'], 'type' => $recipe_data['recipe_type'], 'difficulty' => $recipe_data['recipe_difficulty'], 'description' => $recipe_data['recipe_description'], 'prep_time' => $recipe_data['recipe_prep_time'], 'cook_time' => $recipe_data['recipe_cook_time'], 'yield' => $recipe_data['recipe_yield'], 'status' => 'published', 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql'), ) ); if (!$inserted) { throw new Exception('Failed to insert recipe.'); } $recipe_id = $wpdb->insert_id; // Insert ingredients if (!empty($recipe_data['ingredients'])) { foreach ($recipe_data['ingredients'] as $index => $ing) { $wpdb->insert($wpdb->prefix . 'recipe_ingredients', array( 'recipe_id' => $recipe_id, 'item' => $ing['item'], 'quantity' => $ing['quantity'], 'unit' => $ing['unit'], 'sort_order' => $index + 1, )); } } // Insert instructions if (!empty($recipe_data['instructions'])) { foreach ($recipe_data['instructions'] as $index => $instr) { $wpdb->insert($wpdb->prefix . 'recipe_instructions', array( 'recipe_id' => $recipe_id, 'step_number' => $index + 1, 'description' => $instr['step'], 'temperature' => $instr['temperature'], 'time_minutes' => $instr['time'], )); } } // Insert categories if (!empty($recipe_data['categories'])) { foreach ($recipe_data['categories'] as $cat_id) { $wpdb->insert($wpdb->prefix . 'recipe_category_map', array( 'recipe_id' => $recipe_id, 'category_id' => $cat_id, )); } } // Insert tags if (!empty($recipe_data['tags'])) { foreach ($recipe_data['tags'] as $tag_name) { $tag_id = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$wpdb->prefix}recipe_tags WHERE name = %s", $tag_name )); if (!$tag_id) { $wpdb->insert($wpdb->prefix . 'recipe_tags', array('name' => $tag_name)); $tag_id = $wpdb->insert_id; } $wpdb->insert($wpdb->prefix . 'recipe_tag_map', array( 'recipe_id' => $recipe_id, 'tag_id' => $tag_id, )); } } $wpdb->query('COMMIT'); return $recipe_id; } catch (Exception $e) { $wpdb->query('ROLLBACK'); return new WP_Error('insert_failed', $e->getMessage()); } } // Save recipe to profile function cjbb_save_recipe_to_profile() { if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'cjbb_recipe_nonce')) { wp_send_json_error(array('message' => 'Security failed.')); } if (!is_user_logged_in()) { wp_send_json_error(array('message' => 'Must be logged in.')); } $user_id = get_current_user_id(); $recipe_id = absint($_POST['recipe_id'] ?? 0); if (!$recipe_id) { wp_send_json_error(array('message' => 'Invalid recipe.')); } global $wpdb; $exists = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$wpdb->prefix}user_saved_recipes WHERE user_id = %d AND recipe_id = %d", $user_id, $recipe_id )); if ($exists) { wp_send_json_success(array('message' => 'Already saved!')); } $inserted = $wpdb->insert($wpdb->prefix . 'user_saved_recipes', array( 'user_id' => $user_id, 'recipe_id' => $recipe_id, 'saved_at' => current_time('mysql'), )); if ($inserted) { wp_send_json_success(array('message' => 'Saved to profile!')); } else { wp_send_json_error(array('message' => 'Could not save.')); } } // Create tables function cjbb_create_recipe_tables() { global $wpdb; $c = $wpdb->get_charset_collate(); $wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}recipes ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) UNSIGNED NOT NULL, title VARCHAR(255) NOT NULL, type VARCHAR(50) NOT NULL, difficulty VARCHAR(50) NOT NULL, description LONGTEXT NOT NULL, prep_time INT(11) DEFAULT 0, cook_time INT(11) DEFAULT 0, yield INT(11) DEFAULT 1, status VARCHAR(20) DEFAULT 'draft', created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, KEY user_id (user_id), KEY type (type), KEY status (status) ) $c;"); $wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}recipe_ingredients ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, recipe_id BIGINT(20) UNSIGNED NOT NULL, item VARCHAR(255) NOT NULL, quantity VARCHAR(100) NOT NULL, unit VARCHAR(50) NOT NULL, sort_order INT(11) DEFAULT 0, KEY recipe_id (recipe_id) ) $c;"); $wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}recipe_instructions ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, recipe_id BIGINT(20) UNSIGNED NOT NULL, step_number INT(11) NOT NULL, description LONGTEXT NOT NULL, temperature INT(11) DEFAULT NULL, time_minutes INT(11) DEFAULT NULL, KEY recipe_id (recipe_id) ) $c;"); $wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}recipe_categories ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, slug VARCHAR(100) NOT NULL UNIQUE, description TEXT, type VARCHAR(50) DEFAULT 'beer' ) $c;"); $wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}recipe_category_map ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, recipe_id BIGINT(20) UNSIGNED NOT NULL, category_id BIGINT(20) UNSIGNED NOT NULL, UNIQUE KEY recipe_category (recipe_id, category_id), KEY recipe_id (recipe_id) ) $c;"); $wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}recipe_tags ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL UNIQUE, slug VARCHAR(100) NOT NULL UNIQUE ) $c;"); $wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}recipe_tag_map ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, recipe_id BIGINT(20) UNSIGNED NOT NULL, tag_id BIGINT(20) UNSIGNED NOT NULL, UNIQUE KEY recipe_tag (recipe_id, tag_id), KEY recipe_id (recipe_id) ) $c;"); $wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}user_saved_recipes ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) UNSIGNED NOT NULL, recipe_id BIGINT(20) UNSIGNED NOT NULL, saved_at DATETIME NOT NULL, UNIQUE KEY user_recipe (user_id, recipe_id), KEY user_id (user_id) ) $c;"); } // Get recipe with all data function cjbb_get_recipe($recipe_id) { global $wpdb; $recipe = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}recipes WHERE id = %d", $recipe_id )); if (!$recipe) return false; $recipe->ingredients = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}recipe_ingredients WHERE recipe_id = %d ORDER BY sort_order", $recipe_id )); $recipe->instructions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}recipe_instructions WHERE recipe_id = %d ORDER BY step_number", $recipe_id )); return $recipe; } /** * CJ's Brew & BBQ - Recipe Form HTML Template * Complete multi-step form with all 5 steps * * This outputs the full form structure. CSS and JS handle the rest. * Include this in your template or use the shortcode in recipe-submission-form.php */ if (!defined('ABSPATH')) { exit; } /** * Complete recipe form with all 5 steps * Call this from your page template */ function cjbb_display_complete_recipe_form() { if (!is_user_logged_in()) { return ' Please log in to submit a recipe. '; } ob_start(); ?> Basic Info Ingredients Instructions Categories Preview Tell us about your recipe Start with the basics. We'll get more detailed in the next steps. Recipe Title * type="text" id="recipe-title" name="recipe_title" class="form-control" placeholder="e.g., Belgian Tripel Ale or Texas Brisket Rub" required maxlength="200" > Give your recipe a catchy name (max 200 characters) Recipe Type * -- Select One -- 🍺 Beer 🔥 BBQ Difficulty Level * -- Select One -- Beginner Intermediate Advanced Description *
id="recipe-description"
name="recipe_description"
class="form-control"
rows="4"
placeholder="Tell us what makes this recipe special. What's the story behind it?"
required
maxlength="1000"
> A short description helps other enthusiasts understand your recipe (max 1000 characters) Prep Time (minutes) type="number" id="recipe-prep-time" name="recipe_prep_time" class="form-control" min="0" max="1440" placeholder="30" > Cook/Brew Time (minutes) type="number" id="recipe-cook-time" name="recipe_cook_time" class="form-control" min="0" max="10080" placeholder="120" > Yield (servings/batches) type="number" id="recipe-yield" name="recipe_yield" class="form-control" min="1" max="1000" placeholder="12" > What goes into this? List all ingredients. You can add or remove rows as needed. Item * type="text" name="ingredient_item[]" class="form-control ingredient-item" placeholder="e.g., Pale Malt or Brisket" maxlength="200" required > Quantity * type="text" name="ingredient_quantity[]" class="form-control ingredient-quantity" placeholder="e.g., 2, 1/2, 0.75" required > Unit * type="text" name="ingredient_unit[]" class="form-control ingredient-unit" placeholder="e.g., cups, lbs" maxlength="50" required > Remove + Add Another Ingredient How do you make it? Step-by-step instructions. Keep them clear so anyone can follow along. Step 1 *
name="instruction_step[]"
class="form-control instruction-step"
rows="3"
placeholder="What's the first thing someone should do?"
maxlength="1000"
required
> Instruction text Temperature (°F) - Optional type="number" name="instruction_temperature[]" class="form-control instruction-temperature" placeholder="e.g., 68" > Time (minutes) - Optional type="number" name="instruction_time[]" class="form-control instruction-time" placeholder="e.g., 30" min="0" > Remove This Step + Add Another Step Categorize it Pick categories that match your recipe. Tags are optional but help people find it. Categories * Select at least one Loading categories... Tags (Optional) Add tags separated by commas (e.g., hoppy, dry rub, summer)
id="recipe-tags"
name="recipe_tags"
class="form-control"
rows="2"
placeholder="hoppy, citrus, american"
maxlength="500"
>
Here's what you're submitting
Take a look at everything. Go back to edit if needed.
Recipe Title
Type: - | Difficulty: -
Description:
Prep: -
Cook: -
Yield: -
Ingredients
Instructions
Categories
← Previous
Next →
Submit Recipe ✓
return ob_get_clean();
}