PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.7
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.7
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Forms / FormProcessor.php

FormProcessor.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.7, at includes/Forms/FormProcessor.php

443 lines 15.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Form Processor Class
4 *
5 * @package EasyInvoice
6 * @author Your Name
7 * @copyright Copyright (c) 2023, Your Company
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 */
11
12 namespace EasyInvoice\Forms;
13
14 /**
15 * Form Processor
16 *
17 * Handles form data processing and mapping to database structure.
18 * Field validation and sanitization are handled by field callbacks.
19 *
20 * @since 1.0.0
21 */
22 class FormProcessor {
23
24 /**
25 * Allowed HTML tags for textarea fields
26 *
27 * @since 1.0.0
28 * @var array
29 */
30 private static $allowed_textarea_tags = [
31 'p' => [],
32 'br' => [],
33 'strong' => [],
34 'em' => [],
35 'i' => [],
36 'b' => [],
37 'u' => [],
38 'ul' => [],
39 'ol' => [],
40 'li' => [],
41 'h1' => [],
42 'h2' => [],
43 'h3' => [],
44 'h4' => [],
45 'h5' => [],
46 'h6' => [],
47 'a' => [
48 'href' => [],
49 'title' => [],
50 'target' => [],
51 ],
52 'span' => [],
53 'div' => [],
54 ];
55
56 /**
57 * Sanitize textarea field allowing basic HTML tags
58 *
59 * @since 1.0.0
60 * @param string $value The value to sanitize
61 * @return string Sanitized value with allowed HTML tags
62 */
63 public static function sanitizeTextareaWithHtml(string $value): string {
64 // Use wp_kses to allow only safe HTML tags
65 return wp_kses($value, self::$allowed_textarea_tags);
66 }
67
68 /**
69 * Process form data
70 *
71 * @since 1.0.0
72 * @param array $raw_data Raw form data
73 * @param array $field_definitions Field definitions for reference
74 * @return array Processed data with error handling
75 */
76 public function processFormData(array $raw_data, array $field_definitions): array {
77 $processed_data = [];
78 $errors = [];
79
80 // Handle payment_gateways field specially
81 $payment_gateways_value = '';
82 if (isset($raw_data['payment_gateways_hidden'])) {
83 $payment_gateways_value = $raw_data['payment_gateways_hidden'];
84 } elseif (isset($raw_data['payment_gateways'])) {
85 $payment_gateways_value = $raw_data['payment_gateways'];
86 }
87
88 // Always include payment_gateways in raw_data
89 $raw_data['payment_gateways'] = $payment_gateways_value;
90
91 $known_field_names = [];
92 foreach ($field_definitions as $field) {
93 $field_name = $field['name'] ?? '';
94 if ($field_name) {
95 $known_field_names[] = $field_name;
96 }
97 $required = $field['required'] ?? false;
98 $field_type = $field['type'] ?? 'text';
99 $raw_value = $raw_data[$field_name] ?? '';
100
101 // Check required fields
102 if ($required && empty($raw_value)) {
103 $errors[$field_name] = sprintf(
104 __('%s is required.', 'easy-invoice'),
105 $field['label'] ?? $field_name
106 );
107 continue;
108 }
109
110 // Always include textarea fields (like description) even when empty
111 // This allows users to clear these fields by submitting empty values
112 $always_include_fields = ['description', 'notes', 'terms', 'internal_notes'];
113 $should_always_include = in_array($field_name, $always_include_fields) || $field_type === 'textarea';
114
115 // Skip empty non-required fields (unless they should always be included)
116 if (empty($raw_value) && !$required && $raw_value !== '0' && !$should_always_include) {
117 continue;
118 }
119
120 // Apply field-specific sanitization if defined
121 $processed_value = $this->applyFieldSanitization($field, $raw_value);
122
123 // Handle currency fields with "global" option - save "global" as the value
124 if (in_array($field_name, ['currency_code', 'currency_position']) && $processed_value === 'global') {
125 $processed_value = 'global';
126 }
127
128 // Apply field-specific processing if defined
129 $processed_value = $this->applyFieldProcessing($field, $processed_value);
130
131 // Apply field-specific validation if defined
132 $validation_result = $this->applyFieldValidation($field, $processed_value);
133 if ($validation_result !== true) {
134 $errors[$field_name] = $validation_result;
135 continue;
136 }
137
138 $processed_data[$field_name] = $processed_value;
139 }
140
141 // Always include payment_gateways in processed data
142 $processed_data['payment_gateways'] = $payment_gateways_value;
143
144 // Allow plugins to modify the processed data
145 $processed_data = apply_filters('easy_invoice_form_processor_data', $processed_data, $raw_data);
146
147 return [
148 'data' => $processed_data,
149 'errors' => $errors
150 ];
151 }
152
153 /**
154 * Process item data
155 *
156 * @since 1.0.0
157 * @param array $raw_item_data Raw item data
158 * @param array $item_field_definitions Item field definitions
159 * @return array Processed item data
160 */
161 public function processItemData(array $raw_item_data, array $item_field_definitions): array {
162 $processed_item = [];
163
164 foreach ($item_field_definitions as $field) {
165 $field_name = $field['name'] ?? '';
166 $required = $field['required'] ?? false;
167
168 if (empty($field_name)) {
169 continue;
170 }
171
172 $raw_value = $raw_item_data[$field_name] ?? '';
173
174 // Check required fields
175 if ($required && empty($raw_value) && $field['type'] !== 'checkbox') {
176 // For items, we'll skip invalid items rather than throwing errors
177 continue;
178 }
179
180 // For non-required fields, include them even if empty (but not for checkboxes)
181 if ($field['type'] === 'checkbox') {
182 // For checkboxes, only include if they have a value
183 if (isset($raw_item_data[$field_name])) {
184 $processed_value = $this->applyFieldSanitization($field, $raw_value);
185 $processed_item[$field_name] = $processed_value;
186 }
187 } else {
188 // For non-checkbox fields, include them even if empty
189 $processed_value = $this->applyFieldSanitization($field, $raw_value);
190 $processed_item[$field_name] = $processed_value;
191 }
192 }
193
194 return $processed_item;
195 }
196
197 /**
198 * Process items data
199 *
200 * @since 1.0.0
201 * @param array $raw_items_data Raw items data
202 * @param array $item_field_definitions Item field definitions
203 * @return array Processed items data
204 */
205 public function processItemsData(array $raw_items_data, array $item_field_definitions): array {
206 $processed_items = [];
207 foreach ($raw_items_data as $item_index => $raw_item_data) {
208 // Ensure all checkbox fields are set
209 foreach ($item_field_definitions as $field) {
210
211 if (($field['type'] ?? '') === 'checkbox') {
212 $field_name = $field['name'] ?? '';
213 if ($field_name && !isset($raw_item_data[$field_name])) {
214 $raw_item_data[$field_name] = '0';
215 }
216 }
217 }
218
219 $processed_item = $this->processItemData($raw_item_data, $item_field_definitions);
220 if (!empty($processed_item)) {
221 $processed_items[] = $processed_item;
222 }
223 }
224
225 return $processed_items;
226 }
227
228 /**
229 * Save form data to database using field configuration
230 *
231 * @since 1.0.0
232 * @param array $form_data Processed form data
233 * @param array $field_definitions Field definitions for reference
234 * @param object $model The model object (Invoice, Quote, etc.)
235 * @return void
236 */
237 public function saveFormDataToDatabase(array $form_data, array $field_definitions, $model): void {
238
239
240 // Process each field from the configuration
241 foreach ($field_definitions as $field) {
242 $field_name = $field['name'] ?? '';
243 if (empty($field_name)) {
244 continue;
245 }
246
247 // Generate database key with _easy_invoice_ prefix
248 $db_key = '_easy_invoice_' . $field_name;
249
250 // Get value from form data
251 $value = $form_data[$field_name] ?? null;
252
253 // Fields that should always be saved, even if empty (to allow clearing)
254 $always_save_fields = ['description', 'notes', 'terms', 'internal_notes'];
255 $should_always_save = in_array($field_name, $always_save_fields);
256
257 // Save if value exists and is not empty (or is 0), OR if it's a field that should always be saved
258 if (($value !== null && $value !== '') || ($should_always_save && array_key_exists($field_name, $form_data))) {
259 // Use custom save callback if provided
260 if (isset($field['save_callback']) && is_callable($field['save_callback'])) {
261 $field['save_callback']($value ?? '', $model);
262 } else {
263 // Default save to meta data
264 if (method_exists($model, 'setMetaData')) {
265 $model->setMetaData($db_key, $value ?? '');
266 }
267 }
268 }
269 }
270
271 // Also process any extra fields that might not be in the configuration
272 $always_save_fields = ['description', 'notes', 'terms', 'internal_notes'];
273 foreach ($form_data as $field_name => $value) {
274 $should_always_save = in_array($field_name, $always_save_fields);
275
276 if (($value !== null && $value !== '') || ($should_always_save && array_key_exists($field_name, $form_data))) {
277 $db_key = '_easy_invoice_' . $field_name;
278
279 // Check if this field wasn't already processed above
280 $already_processed = false;
281 foreach ($field_definitions as $field) {
282 if (($field['name'] ?? '') === $field_name) {
283 $already_processed = true;
284 break;
285 }
286 }
287
288 if (!$already_processed && method_exists($model, 'setMetaData')) {
289 $model->setMetaData($db_key, $value ?? '');
290 }
291 }
292 }
293 }
294
295 /**
296 * Generate database key from field name
297 *
298 * @since 1.0.0
299 * @param string $field_name The field name
300 * @return string Database key with _easy_invoice_ prefix
301 */
302 public function generateDatabaseKey(string $field_name): string {
303 return '_easy_invoice_' . $field_name;
304 }
305
306 /**
307 * Process field value based on field type
308 *
309 * @since 1.0.0
310 * @param array $field Field configuration
311 * @param mixed $value Raw value
312 * @return mixed Processed value
313 */
314 private function processFieldValue(array $field, $value) {
315 $type = $field['type'] ?? 'text';
316
317 switch ($type) {
318 case 'number':
319 return is_numeric($value) ? floatval($value) : 0;
320
321 case 'checkbox':
322 if (is_string($value)) {
323 $value = strtolower($value);
324 return ($value === '1' || $value === 'true' || $value === 'yes' || $value === 'on') ? '1' : '0';
325 }
326 return ($value == '1' || $value === true) ? '1' : '0';
327
328 case 'payment_gateways':
329 // Handle array of selected payment gateways
330 if (is_array($value)) {
331 // Sanitize each gateway ID and filter out empty values
332 $sanitized_gateways = array_filter(array_map('sanitize_text_field', $value));
333 return implode(',', $sanitized_gateways);
334 } elseif (is_string($value)) {
335 // If it's already a string (comma-separated), sanitize it
336 return sanitize_text_field($value);
337 }
338 return '';
339
340 case 'textarea':
341 // Allow basic HTML tags in textarea fields
342 return self::sanitizeTextareaWithHtml($value);
343
344 case 'email':
345 return sanitize_email($value);
346
347 case 'url':
348 return esc_url_raw($value);
349
350 case 'date':
351 return sanitize_text_field($value);
352
353 case 'select':
354 case 'text':
355 default:
356 return sanitize_text_field($value);
357 }
358 }
359
360 /**
361 * Apply field-specific sanitization if defined
362 *
363 * @since 1.0.0
364 * @param array $field Field configuration
365 * @param mixed $value Raw value
366 * @return mixed Processed value
367 */
368 private function applyFieldSanitization(array $field, $value) {
369 // Check for sanitize_callback in field configuration
370 $sanitize_callback = $field['sanitize_callback'] ?? null;
371
372 if (is_callable($sanitize_callback)) {
373 // If callback is 'sanitize_textarea_field', use our HTML-allowing version for textarea fields
374 if ($sanitize_callback === 'sanitize_textarea_field' && ($field['type'] ?? '') === 'textarea') {
375 return self::sanitizeTextareaWithHtml($value);
376 }
377 return $sanitize_callback($value);
378 }
379
380 // Fallback to basic field type processing
381 return $this->processFieldValue($field, $value);
382 }
383
384 /**
385 * Apply field-specific processing if defined
386 *
387 * @since 1.0.0
388 * @param array $field Field configuration
389 * @param mixed $value Processed value
390 * @return mixed Processed value
391 */
392 private function applyFieldProcessing(array $field, $value) {
393 // Check for process_callback in field configuration
394 $process_callback = $field['process_callback'] ?? null;
395
396 if (is_callable($process_callback)) {
397 return $process_callback($value);
398 }
399
400 return $value;
401 }
402
403 /**
404 * Apply field-specific validation if defined
405 *
406 * @since 1.0.0
407 * @param array $field Field configuration
408 * @param mixed $value Processed value
409 * @return bool|string True if valid, error message if invalid
410 */
411 private function applyFieldValidation(array $field, $value) {
412 // Check for validate_callback in field configuration
413 $validate_callback = $field['validate_callback'] ?? null;
414
415 if (is_callable($validate_callback)) {
416 return $validate_callback($value);
417 }
418
419 return true;
420 }
421
422 /**
423 * Get global currency value for a specific field
424 *
425 * @since 1.0.0
426 * @param string $field_name Field name (currency_code or currency_position)
427 * @return string Global currency value
428 */
429 private function getGlobalCurrencyValue(string $field_name): string {
430 switch ($field_name) {
431 case 'currency_code':
432 return get_option('easy_invoice_currency_code', 'USD');
433 case 'currency_position':
434 $position = get_option('easy_invoice_currency_position', 'left');
435 // Convert from old format if needed
436 if ($position === 'l') return 'before';
437 if ($position === 'r') return 'after';
438 return $position;
439 default:
440 return '';
441 }
442 }
443 }