尽管问题很老,但到目前为止,这一点一直被忽略:通过 PHP 的 pcre 函数进行名称验证,这些函数已通过 XML 规范进行了简化。
XML 的定义非常清楚其规范中的元素名称 (Extensible Markup Language (XML) 1.0 (Fifth Edition)):
[4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
[4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
[5] Name ::= NameStartChar (NameChar)*
此表示法可以转换为与preg_match 一起使用的 UTF-8 兼容正则表达式,这里作为要逐字复制的单引号 PHP 字符串:
'~^[:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}\\x{10000}-\\x{EFFFF}][:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}\\x{10000}-\\x{EFFFF}.\\-0-9\\xB7\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}]*$~u'
或者以更易读的方式作为具有命名子模式的另一个变体:
'~
# XML 1.0 Name symbol PHP PCRE regex <http://www.w3.org/TR/REC-xml/#NT-Name>
(?(DEFINE)
(?<NameStartChar> [:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}\\x{10000}-\\x{EFFFF}])
(?<NameChar> (?&NameStartChar) | [.\\-0-9\\xB7\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}])
(?<Name> (?&NameStartChar) (?&NameChar)*)
)
^(?&Name)$
~ux'
请注意,此模式包含冒号 :,出于 XML 命名空间验证的原因(例如,对 NCName 的测试),您可能希望排除该冒号(第一个模式中有两个外观,第二个出现一个)。
用法示例:
$name = '::...';
$pattern = '~
# XML 1.0 Name symbol PHP PCRE regex <http://www.w3.org/TR/REC-xml/#NT-Name>
(?(DEFINE)
(?<NameStartChar> [:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}\\x{10000}-\\x{EFFFF}])
(?<NameChar> (?&NameStartChar) | [.\\-0-9\\xB7\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}])
(?<Name> (?&NameStartChar) (?&NameChar)*)
)
^(?&Name)$
~ux';
$valid = 1 === preg_match($pattern, $name); # bool(true)
不可能以XML(小写或大写字母)开头的元素名称的说法是不正确的。 <XML/> 是一个格式完美的 XML,XML 是一个格式完美的元素名称。
只是这些名称位于格式良好的元素名称的子集中,为标准化保留(XML 版本 1.0 及更高版本)。很容易测试一个(格式良好的)元素名称是否通过字符串比较保留:
$reserved = $valid && 0 === stripos($name, 'xml'));
或者另一个正则表达式:
$reserved = $valid && 1 === preg_match('~^[Xx][Mm][Ll]~', $name);
PHP's DOMDocument 可以不测试保留名称至少我不知道如何做到这一点,而且我一直在寻找。
一个有效的元素名称需要一个唯一元素类型声明,这似乎超出了这里的问题范围,因为没有提供这样的声明。因此,答案没有考虑到这一点。如果有元素类型声明,您只需要针对所有(区分大小写)名称的白名单进行验证,因此这将是一个简单的区分大小写的字符串比较。
游览:DOMDocument 与正则表达式有何不同?
与DOMDocument / DOMElement 相比,有效元素名称的限定存在一些差异。 DOM 扩展处于某种混合模式,这使得它验证的内容难以预测。以下短途旅行说明了该行为并展示了如何控制它。
让我们以$name 实例化一个元素:
$element = new DOMElement($name);
结果取决于:
所以第一个字符决定了比较模式。
正则表达式专门编写了要检查的内容,这里是 XML 1.0 Name 符号。
您可以使用DOMElement 在名称前加上冒号来达到同样的效果:
function isValidXmlName($name)
{
try {
new DOMElement(":$name");
return TRUE;
} catch (DOMException $e) {
return FALSE;
}
}
要显式检查QName,可以通过将其转换为PrefixedName 来实现,以防它是UnprefixedName:
function isValidXmlnsQname($qname)
{
$prefixedName = (!strpos($qname, ':') ? 'prefix:' : '') . $qname;
try {
new DOMElement($prefixedName, NULL, 'uri:ns');
return TRUE;
} catch (DOMException $e) {
return FALSE;
}
}