# easy-invoice/2.3.2/includes/Forms/FormProcessor.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.3.2. 443 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.3.2/code/includes/Forms/FormProcessor.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.3.2/raw/includes/Forms/FormProcessor.php
- Modified: 2026-02-22T10:07:34+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.3.2/code/includes/Forms/FormProcessor.php#L10-L20`.

```php
<?php
/**
 * Form Processor Class
 *
 * @package     EasyInvoice
 * @author      Your Name
 * @copyright   Copyright (c) 2023, Your Company
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.0.0
 */

namespace EasyInvoice\Forms;

/**
 * Form Processor
 *
 * Handles form data processing and mapping to database structure.
 * Field validation and sanitization are handled by field callbacks.
 *
 * @since 1.0.0
 */
class FormProcessor {
    
    /**
     * Allowed HTML tags for textarea fields
     *
     * @since 1.0.0
     * @var array
     */
    private static $allowed_textarea_tags = [
        'p' => [],
        'br' => [],
        'strong' => [],
        'em' => [],
        'i' => [],
        'b' => [],
        'u' => [],
        'ul' => [],
        'ol' => [],
        'li' => [],
        'h1' => [],
        'h2' => [],
        'h3' => [],
        'h4' => [],
        'h5' => [],
        'h6' => [],
        'a' => [
            'href' => [],
            'title' => [],
            'target' => [],
        ],
        'span' => [],
        'div' => [],
    ];
    
    /**
     * Sanitize textarea field allowing basic HTML tags
     *
     * @since 1.0.0
     * @param string $value The value to sanitize
     * @return string Sanitized value with allowed HTML tags
     */
    public static function sanitizeTextareaWithHtml(string $value): string {
        // Use wp_kses to allow only safe HTML tags
        return wp_kses($value, self::$allowed_textarea_tags);
    }
    
    /**
     * Process form data
     *
     * @since 1.0.0
     * @param array $raw_data Raw form data
     * @param array $field_definitions Field definitions for reference
     * @return array Processed data with error handling
     */
    public function processFormData(array $raw_data, array $field_definitions): array {
        $processed_data = [];
        $errors = [];
        
        // Handle payment_gateways field specially
        $payment_gateways_value = '';
        if (isset($raw_data['payment_gateways_hidden'])) {
            $payment_gateways_value = $raw_data['payment_gateways_hidden'];
        } elseif (isset($raw_data['payment_gateways'])) {
            $payment_gateways_value = $raw_data['payment_gateways'];
        }
        
        // Always include payment_gateways in raw_data
        $raw_data['payment_gateways'] = $payment_gateways_value;
        
        $known_field_names = [];
        foreach ($field_definitions as $field) {
            $field_name = $field['name'] ?? '';
            if ($field_name) {
                $known_field_names[] = $field_name;
            }
            $required = $field['required'] ?? false;
            $field_type = $field['type'] ?? 'text';
            $raw_value = $raw_data[$field_name] ?? '';
            
            // Check required fields
            if ($required && empty($raw_value)) {
                $errors[$field_name] = sprintf(
                    __('%s is required.', 'easy-invoice'),
                    $field['label'] ?? $field_name
                );
                continue;
            }
            
            // Always include textarea fields (like description) even when empty
            // This allows users to clear these fields by submitting empty values
            $always_include_fields = ['description', 'notes', 'terms', 'internal_notes'];
            $should_always_include = in_array($field_name, $always_include_fields) || $field_type === 'textarea';
            
            // Skip empty non-required fields (unless they should always be included)
            if (empty($raw_value) && !$required && $raw_value !== '0' && !$should_always_include) {
                continue;
            }
            
            // Apply field-specific sanitization if defined
            $processed_value = $this->applyFieldSanitization($field, $raw_value);
            
            // Handle currency fields with "global" option - save "global" as the value
            if (in_array($field_name, ['currency_code', 'currency_position']) && $processed_value === 'global') {
                $processed_value = 'global';
            }
            
            // Apply field-specific processing if defined
            $processed_value = $this->applyFieldProcessing($field, $processed_value);
            
            // Apply field-specific validation if defined
            $validation_result = $this->applyFieldValidation($field, $processed_value);
            if ($validation_result !== true) {
                $errors[$field_name] = $validation_result;
                continue;
            }
            
            $processed_data[$field_name] = $processed_value;
        }
        
        // Always include payment_gateways in processed data
        $processed_data['payment_gateways'] = $payment_gateways_value;
        
        // Allow plugins to modify the processed data
        $processed_data = apply_filters('easy_invoice_form_processor_data', $processed_data, $raw_data);
        
        return [
            'data' => $processed_data,
            'errors' => $errors
        ];
    }
    
    /**
     * Process item data
     *
     * @since 1.0.0
     * @param array $raw_item_data Raw item data
     * @param array $item_field_definitions Item field definitions
     * @return array Processed item data
     */
    public function processItemData(array $raw_item_data, array $item_field_definitions): array {
        $processed_item = [];
        
        foreach ($item_field_definitions as $field) {
            $field_name = $field['name'] ?? '';
            $required = $field['required'] ?? false;
            
            if (empty($field_name)) {
                continue;
            }
            
            $raw_value = $raw_item_data[$field_name] ?? '';
            
            // Check required fields
            if ($required && empty($raw_value) && $field['type'] !== 'checkbox') {
                // For items, we'll skip invalid items rather than throwing errors
                continue;
            }
            
            // For non-required fields, include them even if empty (but not for checkboxes)
            if ($field['type'] === 'checkbox') {
                // For checkboxes, only include if they have a value
                if (isset($raw_item_data[$field_name])) {
                    $processed_value = $this->applyFieldSanitization($field, $raw_value);
                    $processed_item[$field_name] = $processed_value;
                }
            } else {
                // For non-checkbox fields, include them even if empty
                $processed_value = $this->applyFieldSanitization($field, $raw_value);
                $processed_item[$field_name] = $processed_value;
            }
        }
        
        return $processed_item;
    }
    
    /**
     * Process items data
     *
     * @since 1.0.0
     * @param array $raw_items_data Raw items data
     * @param array $item_field_definitions Item field definitions
     * @return array Processed items data
     */
    public function processItemsData(array $raw_items_data, array $item_field_definitions): array {
        $processed_items = [];
        foreach ($raw_items_data as $item_index => $raw_item_data) {
            // Ensure all checkbox fields are set
            foreach ($item_field_definitions as $field) {
                 
                if (($field['type'] ?? '') === 'checkbox') {
                    $field_name = $field['name'] ?? '';
                    if ($field_name && !isset($raw_item_data[$field_name])) {
                        $raw_item_data[$field_name] = '0';
                    }
                }
            }
            
            $processed_item = $this->processItemData($raw_item_data, $item_field_definitions);
            if (!empty($processed_item)) {
                $processed_items[] = $processed_item;
            }
        }
         
        return $processed_items;
    }
    
    /**
     * Save form data to database using field configuration
     *
     * @since 1.0.0
     * @param array $form_data Processed form data
     * @param array $field_definitions Field definitions for reference
     * @param object $model The model object (Invoice, Quote, etc.)
     * @return void
     */
    public function saveFormDataToDatabase(array $form_data, array $field_definitions, $model): void {

        
        // Process each field from the configuration
        foreach ($field_definitions as $field) {
            $field_name = $field['name'] ?? '';
            if (empty($field_name)) {
                continue;
            }
            
            // Generate database key with _easy_invoice_ prefix
            $db_key = '_easy_invoice_' . $field_name;
            
            // Get value from form data
            $value = $form_data[$field_name] ?? null;
            
            // Fields that should always be saved, even if empty (to allow clearing)
            $always_save_fields = ['description', 'notes', 'terms', 'internal_notes'];
            $should_always_save = in_array($field_name, $always_save_fields);
            
            // Save if value exists and is not empty (or is 0), OR if it's a field that should always be saved
            if (($value !== null && $value !== '') || ($should_always_save && array_key_exists($field_name, $form_data))) {
                // Use custom save callback if provided
                if (isset($field['save_callback']) && is_callable($field['save_callback'])) {
                    $field['save_callback']($value ?? '', $model);
                } else {
                    // Default save to meta data
                    if (method_exists($model, 'setMetaData')) {
                        $model->setMetaData($db_key, $value ?? '');
                    }
                }
            }
        }
        
        // Also process any extra fields that might not be in the configuration
        $always_save_fields = ['description', 'notes', 'terms', 'internal_notes'];
        foreach ($form_data as $field_name => $value) {
            $should_always_save = in_array($field_name, $always_save_fields);
            
            if (($value !== null && $value !== '') || ($should_always_save && array_key_exists($field_name, $form_data))) {
                $db_key = '_easy_invoice_' . $field_name;
                
                // Check if this field wasn't already processed above
                $already_processed = false;
                foreach ($field_definitions as $field) {
                    if (($field['name'] ?? '') === $field_name) {
                        $already_processed = true;
                        break;
                    }
                }
                
                if (!$already_processed && method_exists($model, 'setMetaData')) {
                    $model->setMetaData($db_key, $value ?? '');
                }
            }
        }
    }

    /**
     * Generate database key from field name
     *
     * @since 1.0.0
     * @param string $field_name The field name
     * @return string Database key with _easy_invoice_ prefix
     */
    public function generateDatabaseKey(string $field_name): string {
        return '_easy_invoice_' . $field_name;
    }
    
    /**
     * Process field value based on field type
     *
     * @since 1.0.0
     * @param array $field Field configuration
     * @param mixed $value Raw value
     * @return mixed Processed value
     */
    private function processFieldValue(array $field, $value) {
        $type = $field['type'] ?? 'text';
        
        switch ($type) {
            case 'number':
                return is_numeric($value) ? floatval($value) : 0;
                
            case 'checkbox':
                if (is_string($value)) {
                    $value = strtolower($value);
                    return ($value === '1' || $value === 'true' || $value === 'yes' || $value === 'on') ? '1' : '0';
                }
                return ($value == '1' || $value === true) ? '1' : '0';
                
            case 'payment_gateways':
                // Handle array of selected payment gateways
                if (is_array($value)) {
                    // Sanitize each gateway ID and filter out empty values
                    $sanitized_gateways = array_filter(array_map('sanitize_text_field', $value));
                    return implode(',', $sanitized_gateways);
                } elseif (is_string($value)) {
                    // If it's already a string (comma-separated), sanitize it
                    return sanitize_text_field($value);
                }
                return '';
                
            case 'textarea':
                // Allow basic HTML tags in textarea fields
                return self::sanitizeTextareaWithHtml($value);
                
            case 'email':
                return sanitize_email($value);
                
            case 'url':
                return esc_url_raw($value);
                
            case 'date':
                return sanitize_text_field($value);
                
            case 'select':
            case 'text':
            default:
                return sanitize_text_field($value);
        }
    }
    
    /**
     * Apply field-specific sanitization if defined
     *
     * @since 1.0.0
     * @param array $field Field configuration
     * @param mixed $value Raw value
     * @return mixed Processed value
     */
    private function applyFieldSanitization(array $field, $value) {
        // Check for sanitize_callback in field configuration
        $sanitize_callback = $field['sanitize_callback'] ?? null;
        
        if (is_callable($sanitize_callback)) {
            // If callback is 'sanitize_textarea_field', use our HTML-allowing version for textarea fields
            if ($sanitize_callback === 'sanitize_textarea_field' && ($field['type'] ?? '') === 'textarea') {
                return self::sanitizeTextareaWithHtml($value);
            }
            return $sanitize_callback($value);
        }
        
        // Fallback to basic field type processing
        return $this->processFieldValue($field, $value);
    }
    
    /**
     * Apply field-specific processing if defined
     *
     * @since 1.0.0
     * @param array $field Field configuration
     * @param mixed $value Processed value
     * @return mixed Processed value
     */
    private function applyFieldProcessing(array $field, $value) {
        // Check for process_callback in field configuration
        $process_callback = $field['process_callback'] ?? null;
        
        if (is_callable($process_callback)) {
            return $process_callback($value);
        }
        
        return $value;
    }
    
    /**
     * Apply field-specific validation if defined
     *
     * @since 1.0.0
     * @param array $field Field configuration
     * @param mixed $value Processed value
     * @return bool|string True if valid, error message if invalid
     */
    private function applyFieldValidation(array $field, $value) {
        // Check for validate_callback in field configuration
        $validate_callback = $field['validate_callback'] ?? null;
        
        if (is_callable($validate_callback)) {
            return $validate_callback($value);
        }
        
        return true;
    }
    
    /**
     * Get global currency value for a specific field
     *
     * @since 1.0.0
     * @param string $field_name Field name (currency_code or currency_position)
     * @return string Global currency value
     */
    private function getGlobalCurrencyValue(string $field_name): string {
        switch ($field_name) {
            case 'currency_code':
                return get_option('easy_invoice_currency_code', 'USD');
            case 'currency_position':
                $position = get_option('easy_invoice_currency_position', 'left');
                // Convert from old format if needed
                if ($position === 'l') return 'before';
                if ($position === 'r') return 'after';
                return $position;
            default:
                return '';
        }
    }
} 
```
