## Problem E-conomic API returns HTTP 400 when text-line descriptions contain certain characters. The most common case is `/` in the order reference field, which causes the entire draft-invoice export to fail. ## Root cause When `order.reference` (or notes, reg_*, po) contains `/`, e-conomic's text-line validation rejects the entire draft with HTTP 400. Same for control characters and very long strings. ## Fix Adds `economic_export_sanitizer` class that sanitizes all user-input fields flowing into e-conomic: - `/` → `-` (the reported 400 trigger) - Control chars stripped (\x00-\x1F except \t and \n) - Tab and newline → single space - Whitespace normalized and trimmed - Lengths capped (text 250, product 50, description 500) with `...` suffix - Multibyte safe (æ, ø, å, emoji, Chinese) ## Applied to In `economic_invoice_draft.php`: - `order.po` - `order.reference` (PRIMARY FIX for the reported issue) - `order.notes` - `order.reg_1/2/3` - `order_item.reference` - `order_item.notes` - `product.description` - `product.productNumber` - `department_name` ## Test coverage - 31 unit tests with 45 assertions - All edge cases (null, empty, control chars, multibyte, very long, HTML, control chars in every position) - Lint and test suite both pass ## Linear Refs: TRU-189, TRU-190, TRU-191, TRU-192, TRU-193, TRU-194, TRU-196 Co-authored-by: OpenClaw <openclaw@copenhagentruckwash.io>
112 lines
4.0 KiB
PHP
112 lines
4.0 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
/**
|
|
* Sanitizes user-input fields that are sent to the e-conomic API.
|
|
*
|
|
* Background: e-conomic returns 400 errors when description fields contain
|
|
* certain characters. The known issue is "/" in the order reference field
|
|
* (TRU-188), but we sanitize defensively for all such cases.
|
|
*
|
|
* - sanitizeTextLine(): for plain text lines (reference, notes, po, etc.)
|
|
* - sanitizeProductNumber(): for product identifiers
|
|
* - sanitizeProductDescription(): for product-line descriptions
|
|
* - sanitizeForEconApi(): catch-all for arbitrary user input
|
|
*/
|
|
class economic_export_sanitizer
|
|
{
|
|
/** E-conomic soft limit for a single description line. */
|
|
public const TEXT_LINE_MAX_LENGTH = 250;
|
|
/** E-conomic soft limit for a product description. */
|
|
public const PRODUCT_DESCRIPTION_MAX_LENGTH = 500;
|
|
/** E-conomic soft limit for a product number. */
|
|
public const PRODUCT_NUMBER_MAX_LENGTH = 50;
|
|
|
|
/** Characters that are illegal in product numbers on most e-conomic setups. */
|
|
private const PRODUCT_NUMBER_FORBIDDEN = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', "\0"];
|
|
|
|
/**
|
|
* Sanitize a value for use in a single-line text description.
|
|
*
|
|
* Transformations (in order):
|
|
* 1. Replaces "/" with "-" (the reported 400 trigger)
|
|
* 2. Strips control characters (\x00-\x1F) except \t and \n
|
|
* 3. Replaces tab with single space
|
|
* 4. Collapses newlines into spaces (text lines are single-line)
|
|
* 5. Collapses runs of spaces to a single space
|
|
* 6. Trims leading/trailing whitespace
|
|
* 7. Truncates to $maxLength with "..." suffix if needed
|
|
*/
|
|
public static function sanitizeTextLine(mixed $value, int $maxLength = self::TEXT_LINE_MAX_LENGTH): string
|
|
{
|
|
if ($value === null) {
|
|
return '';
|
|
}
|
|
$text = (string)$value;
|
|
if ($text === '') {
|
|
return '';
|
|
}
|
|
// 1. Strip control characters except \t and \n
|
|
$text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text);
|
|
// 2. Replace tab with single space
|
|
$text = str_replace("\t", ' ', $text);
|
|
// 3. Collapse newlines to single space (text lines are single-line)
|
|
$text = preg_replace('/[\r\n]+/u', ' ', $text);
|
|
// 4. Replace forward slashes (the reported 400 trigger)
|
|
$text = str_replace('/', '-', $text);
|
|
// 5. Collapse runs of spaces
|
|
$text = preg_replace('/\s+/u', ' ', $text);
|
|
// 6. Trim
|
|
$text = trim($text);
|
|
// 7. Truncate with ellipsis if too long
|
|
if ($maxLength > 3 && mb_strlen($text) > $maxLength) {
|
|
$text = mb_substr($text, 0, $maxLength - 3) . '...';
|
|
} elseif (mb_strlen($text) > $maxLength) {
|
|
$text = mb_substr($text, 0, $maxLength);
|
|
}
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* Sanitize a product number/identifier.
|
|
*
|
|
* Removes characters that are illegal in product numbers on most
|
|
* e-conomic setups (filesystem-unsafe + path separators).
|
|
*/
|
|
public static function sanitizeProductNumber(mixed $value): string
|
|
{
|
|
if ($value === null) {
|
|
return '';
|
|
}
|
|
$text = (string)$value;
|
|
if ($text === '') {
|
|
return '';
|
|
}
|
|
$text = str_replace(self::PRODUCT_NUMBER_FORBIDDEN, '', $text);
|
|
$text = preg_replace('/[\x00-\x1F\x7F]/u', '', $text);
|
|
$text = trim($text);
|
|
if (mb_strlen($text) > self::PRODUCT_NUMBER_MAX_LENGTH) {
|
|
$text = mb_substr($text, 0, self::PRODUCT_NUMBER_MAX_LENGTH);
|
|
}
|
|
return $text;
|
|
}
|
|
|
|
/**
|
|
* Sanitize a longer product description.
|
|
*/
|
|
public static function sanitizeProductDescription(mixed $value): string
|
|
{
|
|
return self::sanitizeTextLine($value, self::PRODUCT_DESCRIPTION_MAX_LENGTH);
|
|
}
|
|
|
|
/**
|
|
* Catch-all sanitizer for any user-input value going to e-conomic.
|
|
* Defaults to text-line rules.
|
|
*/
|
|
public static function sanitizeForEconApi(mixed $value): string
|
|
{
|
|
return self::sanitizeTextLine($value);
|
|
}
|
|
}
|