PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.2
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.2
2.4.3 2.4.4 2.4.2 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 All 60 releases
← All changes | includes/Forms/FormProcessor.php +91 -20 2.1.1 → 2.4.2 View file →
@@ -21,8 +21,52 @@
21 21 */
22 22 class FormProcessor {
23 23
24 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 + /**
25 69 * Process form data
26 70 *
27 71 * @since 1.0.0
28 72 * @param array $raw_data Raw form data
@@ -50,13 +94,23 @@
50 94 if ($field_name) {
51 95 $known_field_names[] = $field_name;
52 96 }
53 97 $required = $field['required'] ?? false;
98 + $field_type = $field['type'] ?? 'text';
54 99 $raw_value = $raw_data[$field_name] ?? '';
55 -
100 +
101 + // A browser omits an unchecked checkbox from the post. Treating "absent"
102 + // as "leave it alone" meant the box could never be turned off: with tax
103 + // on globally, "Apply Tax to This Invoice" always came back checked and
104 + // saved as yes. Absent now means unchecked, the way a browser means it.
105 + if ('checkbox' === $field_type && ! array_key_exists($field_name, $raw_data)) {
106 + $raw_value = '0';
107 + }
108 +
56 109 // Check required fields
57 110 if ($required && empty($raw_value)) {
58 111 $errors[$field_name] = sprintf(
112 + /* translators: %s: field name. */
59 113 __('%s is required.', 'easy-invoice'),
60 114 $field['label'] ?? $field_name
61 115 );
62 116 continue;
@@ -61,10 +115,17 @@
61 115 );
62 116 continue;
63 117 }
64 118
65 - // Skip empty non-required fields
66 - if (empty($raw_value) && !$required && $raw_value !== '0') {
119 + // Always include textarea fields (like description) even when empty
120 + // This allows users to clear these fields by submitting empty values
121 + // Attachments too: an emptied list must reach the save, or the
122 + // last file can never be removed.
123 + $always_include_fields = ['description', 'notes', 'terms', 'internal_notes', 'attachments'];
124 + $should_always_include = in_array($field_name, $always_include_fields) || in_array($field_type, ['textarea', 'attachments'], true);
125 +
126 + // Skip empty non-required fields (unless they should always be included)
127 + if (empty($raw_value) && !$required && $raw_value !== '0' && !$should_always_include) {
67 128 continue;
68 129 }
69 130
70 131 // Apply field-specific sanitization if defined
@@ -87,8 +148,17 @@
87 148
88 149 $processed_data[$field_name] = $processed_value;
89 150 }
90 151
152 + // A percentage discount above 100 or a negative discount is a typo, not a
153 + // deal; say so instead of saving a document whose total the model has to clamp.
154 + if (isset($processed_data['discount_type']) && 'percentage' === $processed_data['discount_type'] && (float) ($processed_data['discount_value'] ?? 0) > 100) {
155 + $errors['discount_value'] = __('A percentage discount cannot be more than 100%.', 'easy-invoice');
156 + }
157 + if (isset($processed_data['discount_value']) && '' !== $processed_data['discount_value'] && (float) $processed_data['discount_value'] < 0) {
158 + $errors['discount_value'] = __('The discount cannot be negative.', 'easy-invoice');
159 + }
160 +
91 161 // Always include payment_gateways in processed data
92 162 $processed_data['payment_gateways'] = $payment_gateways_value;
93 163
94 164 // Allow plugins to modify the processed data
@@ -110,11 +180,8 @@
110 180 */
111 181 public function processItemData(array $raw_item_data, array $item_field_definitions): array {
112 182 $processed_item = [];
113 183
114 - // Debug: Log the raw item data
115 - error_log("Processing item data: " . print_r($raw_item_data, true));
116 -
117 184 foreach ($item_field_definitions as $field) {
118 185 $field_name = $field['name'] ?? '';
119 186 $required = $field['required'] ?? false;
120 187
@@ -126,9 +193,8 @@
126 193
127 194 // Check required fields
128 195 if ($required && empty($raw_value) && $field['type'] !== 'checkbox') {
129 196 // For items, we'll skip invalid items rather than throwing errors
130 - error_log("Skipping required field {$field_name} - empty value");
131 197 continue;
132 198 }
133 199
134 200 // For non-required fields, include them even if empty (but not for checkboxes)
@@ -144,11 +210,8 @@
144 210 $processed_item[$field_name] = $processed_value;
145 211 }
146 212 }
147 213
148 - // Debug: Log the processed item
149 - error_log("Processed item: " . print_r($processed_item, true));
150 -
151 214 return $processed_item;
152 215 }
153 216
154 217 /**
@@ -206,21 +269,21 @@
206 269
207 270 // Get value from form data
208 271 $value = $form_data[$field_name] ?? null;
209 272
210 -
273 + // Fields that should always be saved, even if empty (to allow clearing)
274 + $always_save_fields = ['description', 'notes', 'terms', 'internal_notes', 'attachments'];
275 + $should_always_save = in_array($field_name, $always_save_fields);
211 276
212 - // Only save if value exists and is not empty (or is 0)
213 - if ($value !== null && $value !== '') {
277 + // Save if value exists and is not empty (or is 0), OR if it's a field that should always be saved
278 + if (($value !== null && $value !== '') || ($should_always_save && array_key_exists($field_name, $form_data))) {
214 279 // Use custom save callback if provided
215 280 if (isset($field['save_callback']) && is_callable($field['save_callback'])) {
216 - $field['save_callback']($value, $model);
281 + $field['save_callback']($value ?? '', $model);
217 282 } else {
218 283 // Default save to meta data
219 284 if (method_exists($model, 'setMetaData')) {
220 - $model->setMetaData($db_key, $value);
221 -
222 -
285 + $model->setMetaData($db_key, $value ?? '');
223 286 }
224 287 }
225 288 }
226 289 }
@@ -225,10 +288,13 @@
225 288 }
226 289 }
227 290
228 291 // Also process any extra fields that might not be in the configuration
292 + $always_save_fields = ['description', 'notes', 'terms', 'internal_notes', 'attachments'];
229 293 foreach ($form_data as $field_name => $value) {
230 - if ($value !== null && $value !== '') {
294 + $should_always_save = in_array($field_name, $always_save_fields);
295 +
296 + if (($value !== null && $value !== '') || ($should_always_save && array_key_exists($field_name, $form_data))) {
231 297 $db_key = '_easy_invoice_' . $field_name;
232 298
233 299 // Check if this field wasn't already processed above
234 300 $already_processed = false;
@@ -239,9 +305,9 @@
239 305 }
240 306 }
241 307
242 308 if (!$already_processed && method_exists($model, 'setMetaData')) {
243 - $model->setMetaData($db_key, $value);
309 + $model->setMetaData($db_key, $value ?? '');
244 310 }
245 311 }
246 312 }
247 313 }
@@ -291,9 +357,10 @@
291 357 }
292 358 return '';
293 359
294 360 case 'textarea':
295 - return sanitize_textarea_field($value);
361 + // Allow basic HTML tags in textarea fields
362 + return self::sanitizeTextareaWithHtml($value);
296 363
297 364 case 'email':
298 365 return sanitize_email($value);
299 366
@@ -322,8 +389,12 @@
322 389 // Check for sanitize_callback in field configuration
323 390 $sanitize_callback = $field['sanitize_callback'] ?? null;
324 391
325 392 if (is_callable($sanitize_callback)) {
393 + // If callback is 'sanitize_textarea_field', use our HTML-allowing version for textarea fields
394 + if ($sanitize_callback === 'sanitize_textarea_field' && ($field['type'] ?? '') === 'textarea') {
395 + return self::sanitizeTextareaWithHtml($value);
396 + }
326 397 return $sanitize_callback($value);
327 398 }
328 399
329 400 // Fallback to basic field type processing