| 1 |
<?php |
| 2 |
|
| 3 |
namespace SyncBasalam\Services\Products; |
| 4 |
|
| 5 |
defined('ABSPATH') || exit; |
| 6 |
|
| 7 |
class DescriptionErrorSanitizer |
| 8 |
{ |
| 9 |
private const SEPARATOR = '[\s\x{200c}\x{00a0}\-\_\.\–\—]*'; |
| 10 |
|
| 11 |
public static function extractDescriptionValues(?array $responseData): array |
| 12 |
{ |
| 13 |
if (!is_array($responseData)) return []; |
| 14 |
|
| 15 |
$messages = $responseData['messages'] ?? null; |
| 16 |
if (!is_array($messages)) return []; |
| 17 |
|
| 18 |
$values = []; |
| 19 |
|
| 20 |
foreach ($messages as $message) { |
| 21 |
if (!is_array($message)) continue; |
| 22 |
|
| 23 |
$fields = $message['fields'] ?? []; |
| 24 |
if (!is_array($fields) || !in_array('description', $fields, true)) continue; |
| 25 |
|
| 26 |
$detected = $message['detected'] ?? []; |
| 27 |
if (!is_array($detected)) continue; |
| 28 |
|
| 29 |
foreach ($detected as $item) { |
| 30 |
foreach (self::valuesFromDetectedItem($item) as $value) { |
| 31 |
$values[$value] = $value; |
| 32 |
} |
| 33 |
} |
| 34 |
} |
| 35 |
|
| 36 |
$values = array_values($values); |
| 37 |
usort($values, static function ($a, $b) { |
| 38 |
return mb_strlen($b) <=> mb_strlen($a); |
| 39 |
}); |
| 40 |
|
| 41 |
return $values; |
| 42 |
} |
| 43 |
|
| 44 |
private static function valuesFromDetectedItem($item): array |
| 45 |
{ |
| 46 |
if (is_string($item)) { |
| 47 |
$item = trim($item); |
| 48 |
return $item !== '' ? [$item] : []; |
| 49 |
} |
| 50 |
|
| 51 |
if (!is_array($item)) return []; |
| 52 |
|
| 53 |
$found = []; |
| 54 |
|
| 55 |
$snippet = $item['snippet'] ?? ''; |
| 56 |
if (is_string($snippet) && $snippet !== '') { |
| 57 |
if (preg_match_all('/<em>(.*?)<\/em>/isu', $snippet, $matches)) { |
| 58 |
foreach ($matches[1] as $match) { |
| 59 |
$value = trim(wp_strip_all_tags(html_entity_decode($match, ENT_QUOTES | ENT_HTML5, 'UTF-8'))); |
| 60 |
if ($value === '') continue; |
| 61 |
|
| 62 |
$found[] = $value; |
| 63 |
|
| 64 |
// Basalam highlights a whole window of text and returns it normalized |
| 65 |
// (e.g. "یک" comes back as "1"), so the full phrase often matches nothing |
| 66 |
// in the original description. The part that actually triggers the |
| 67 |
// "social id" signal is the handle-like token, so target it separately. |
| 68 |
foreach (self::handleLikeTokens($value) as $token) { |
| 69 |
$found[] = $token; |
| 70 |
} |
| 71 |
} |
| 72 |
} |
| 73 |
} |
| 74 |
|
| 75 |
$value = $item['value'] ?? ''; |
| 76 |
if (is_string($value)) { |
| 77 |
$value = trim($value); |
| 78 |
if ($value !== '') $found[] = $value; |
| 79 |
} |
| 80 |
|
| 81 |
return $found; |
| 82 |
} |
| 83 |
|
| 84 |
/** |
| 85 |
* Pulls out handle/domain-like tokens ("s.exy", "س.کسی", "insta.gram") from a |
| 86 |
* highlighted snippet. |
| 87 |
* |
| 88 |
* @return string[] |
| 89 |
*/ |
| 90 |
private static function handleLikeTokens(string $text): array |
| 91 |
{ |
| 92 |
if (!preg_match_all('/[\p{L}\p{N}_]+(?:\.[\p{L}\p{N}_]+)+/u', $text, $matches)) return []; |
| 93 |
|
| 94 |
return array_values(array_filter($matches[0], static function ($token) { |
| 95 |
return mb_strlen($token) >= 3; |
| 96 |
})); |
| 97 |
} |
| 98 |
|
| 99 |
public static function sanitize(string $description, array $values): string |
| 100 |
{ |
| 101 |
$result = $description; |
| 102 |
|
| 103 |
foreach ($values as $value) { |
| 104 |
if (!is_string($value)) continue; |
| 105 |
$result = self::removeValue($result, $value); |
| 106 |
} |
| 107 |
|
| 108 |
$result = preg_replace('/[ \t]{2,}/u', ' ', $result); |
| 109 |
$result = preg_replace('/\n{3,}/u', "\n\n", $result); |
| 110 |
|
| 111 |
return trim($result); |
| 112 |
} |
| 113 |
|
| 114 |
private static function removeValue(string $text, string $value): string |
| 115 |
{ |
| 116 |
$value = trim($value); |
| 117 |
if ($value === '') return $text; |
| 118 |
|
| 119 |
$pattern = self::buildPattern($value); |
| 120 |
if ($pattern !== null) { |
| 121 |
$replaced = preg_replace($pattern, ' ', $text); |
| 122 |
if (is_string($replaced) && $replaced !== $text) return $replaced; |
| 123 |
} |
| 124 |
|
| 125 |
$replaced = str_ireplace($value, ' ', $text); |
| 126 |
|
| 127 |
return is_string($replaced) ? $replaced : $text; |
| 128 |
} |
| 129 |
|
| 130 |
private static function buildPattern(string $value): ?string |
| 131 |
{ |
| 132 |
$segments = preg_split('/[\s\x{200c}\x{00a0}\-\_\.\–\—]+/u', $value, -1, PREG_SPLIT_NO_EMPTY); |
| 133 |
if (empty($segments)) return null; |
| 134 |
|
| 135 |
$escaped = array_map(static function ($segment) { |
| 136 |
return preg_quote($segment, '/'); |
| 137 |
}, $segments); |
| 138 |
|
| 139 |
$pattern = '/' . implode(self::SEPARATOR, $escaped) . '/iu'; |
| 140 |
|
| 141 |
if (@preg_match($pattern, '') === false) return null; |
| 142 |
|
| 143 |
return $pattern; |
| 144 |
} |
| 145 |
} |
| 146 |
|