20000 || trim($input) === '') { return self::error(); } $input = trim($input); if (str_starts_with($input, 'https://')) { // Ссылку записи разбираем только как текст, не расширяя допустимые script src. if (preg_match('~\Ahttps://time\.nakogda\.ru/([A-Za-z0-9_-]{1,64})/?(?:\?widget=([A-Za-z0-9_-]{1,64}))?\z~D', $input, $match)) { return array('organization_uid' => $match[1], 'widget_uid' => $match[2] ?? ''); } $uid = self::chat_url($input); return is_wp_error($uid) ? $uid : array('organization_uid' => $uid, 'widget_uid' => ''); } // Разрешительная грамматика вместо исправляющего HTML-парсера: ничего не декодируем. if (preg_match('/[&`\\\\\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $input)) { return self::error(); } $organizations = array(); $widgets = array(); $offset = 0; while ($offset < strlen($input)) { if (!preg_match('/\G\s*<(div|script)((?:\s+[^<>]*?)?)>\s*<\/\1\s*>\s*/Ai', $input, $match, 0, $offset)) { return self::error(); } $offset += strlen($match[0]); $tag = strtolower($match[1]); $attributes = self::attributes($match[2]); if (is_wp_error($attributes)) { return $attributes; } $allowed = $tag === 'div' ? array('data-nakogda-id', 'data-nakogda-widget') : array('src', 'async', 'defer', 'data-nakogda-chat'); if (array_diff(array_keys($attributes), $allowed)) { return self::error(); } if ($tag === 'div') { $organization = self::uid($attributes['data-nakogda-id'] ?? null, false); $widget = self::uid($attributes['data-nakogda-widget'] ?? ''); if (is_wp_error($organization) || is_wp_error($widget)) { return self::error(); } $organizations[$organization] = true; if ($widget !== '') { $widgets[$widget] = true; } } else { foreach (array('async', 'defer') as $boolean) { if (isset($attributes[$boolean]) && !in_array($attributes[$boolean], array('', $boolean), true)) { return self::error(); } } $src = $attributes['src'] ?? ''; if ($src === 'https://time.nakogda.ru/widget/v1/widget.js') { if (isset($attributes['data-nakogda-chat'])) { return self::error(); } } else { $organization = $src === 'https://time.nakogda.ru/chat/v1/chat.js' ? self::uid($attributes['data-nakogda-chat'] ?? null, false) : self::chat_url($src); if (is_wp_error($organization)) { return $organization; } if (isset($attributes['data-nakogda-chat']) && $attributes['data-nakogda-chat'] !== $organization) { return self::error(); } $organizations[$organization] = true; } } } if (count($organizations) !== 1 || count($widgets) > 1) { return self::error(); } return array('organization_uid' => (string) array_key_first($organizations), 'widget_uid' => (string) (array_key_first($widgets) ?? '')); } private static function attributes($input) { $result = array(); $offset = 0; while ($offset < strlen($input)) { if (trim(substr($input, $offset)) === '') { break; } if (!preg_match('/\G\s+([a-z][a-z0-9-]*)(?:\s*=\s*(?:"([^"<>]*)"|\x27([^\x27<>]*)\x27))?(?=\s|$)/Ai', $input, $match, 0, $offset)) { return self::error(); } $name = strtolower($match[1]); if (array_key_exists($name, $result)) { return self::error(); } $has_value = str_contains($match[0], '='); if (!$has_value && !in_array($name, array('async', 'defer'), true)) { return self::error(); } $result[$name] = $match[2] ?? ''; if (isset($match[3]) && $match[3] !== '') { $result[$name] = $match[3]; } $offset += strlen($match[0]); } return $result; } private static function chat_url($url) { if (!is_string($url) || !preg_match('~\Ahttps://time\.nakogda\.ru/chat/v1/([A-Za-z0-9_-]{1,64})\.js\z~D', $url, $match) || $match[1] === 'chat') { return self::error(); } return $match[1]; } private static function error() { return new \WP_Error('nakogda_invalid_embed', __('Не удалось распознать код или ссылку НаКогда. Вставьте штатный код одной организации, её ссылку записи или персональный адрес чата.', 'nakogda')); } }