1: <?php
2: /*
3: * Copyright (c) 2015 @trashtoy
4: * https://github.com/trashtoy/
5: *
6: * Permission is hereby granted, free of charge, to any person obtaining a copy of
7: * this software and associated documentation files (the "Software"), to deal in
8: * the Software without restriction, including without limitation the rights to use,
9: * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
10: * Software, and to permit persons to whom the Software is furnished to do so,
11: * subject to the following conditions:
12: *
13: * The above copyright notice and this permission notice shall be included in all
14: * copies or substantial portions of the Software.
15: *
16: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17: * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18: * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19: * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20: * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21: * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22: */
23: /**
24: * PHP class file.
25: * @auhtor trashtoy
26: * @since 2.1.0
27: */
28: namespace Peach\Markup;
29:
30: /**
31: * XML の仕様書で定義されている以下の EBNF に基づいて,
32: * 要素名や属性名などのバリデーションを行います.
33: *
34: * <pre>
35: * NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] |
36: * [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] |
37: * [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
38: * NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
39: * Name ::= NameStartChar (NameChar)*
40: * </pre>
41: *
42: * 参考文献: {@link http://www.w3.org/TR/REC-xml/ Extensible Markup Language (XML) 1.0 (Fifth Edition)}
43: */
44: class NameValidator
45: {
46: /**
47: * このクラスはインスタンス化できません.
48: */
49: private function __construct() {}
50:
51: /**
52: * 指定された文字列が XML で定義されている Name のネーミングルールに合致するかどうか調べます.
53: *
54: * @param string $name 検査対象の文字列
55: * @return bool 指定された文字列が Name として妥当な場合のみ true
56: */
57: public static function validate($name)
58: {
59: if (self::validateFast($name)) {
60: return true;
61: }
62:
63: $utf8Codec = new \Peach\DF\Utf8Codec();
64: $codepoints = $utf8Codec->decode($name);
65: if (!count($codepoints)) {
66: return false;
67: }
68: $start = array_shift($codepoints);
69: if (!self::validateNameStartChar($start)) {
70: return false;
71: }
72: foreach ($codepoints as $c) {
73: if (!self::validateNameChar($c)) {
74: return false;
75: }
76: }
77: return true;
78: }
79:
80: /**
81: * 使用頻度の高い Name 文字列 (例えば HTML タグなど) について,
82: * 簡易な正規表現を使って検査します.
83: *
84: * @param string $name 検査対象の文字列
85: * @return bool "h1", "img" など, ASCII 文字から成る妥当な Name 文字列の場合のみ true
86: */
87: private static function validateFast($name)
88: {
89: $firstNameCharClass = "[a-zA-Z_:]";
90: $nameCharClass = "[a-zA-Z0-9_:\\.\\-]";
91: $pattern = "/\\A{$firstNameCharClass}{$nameCharClass}*\\z/";
92: return (0 < preg_match($pattern, $name));
93: }
94:
95: /**
96: * 指定された Unicode 符号点が NameStartChar の範囲に適合するかどうかを調べます.
97: *
98: * @param int $codepoint Unicode 符号点
99: * @return bool 引数が NameStartChar である場合のみ true
100: */
101: private static function validateNameStartChar($codepoint)
102: {
103: // @codeCoverageIgnoreStart
104: static $range = null;
105: if ($range === null) {
106: $first = new NameValidator_Range(0x61, 0x7A); // a-z
107: $range = $first
108: ->add(0x41, 0x5A)->add(0x3A)->add(0x5F) // A-Z, ":", "_"
109: ->add(0xC0, 0xD6)->add(0xD8, 0xF6)->add(0xF8, 0x2FF)
110: ->add(0x370, 0x37D)->add(0x37F, 0x1FFF)->add(0x200C, 0x200D)
111: ->add(0x2070, 0x218F)->add(0x2C00, 0x2FEF)->add(0x3001, 0xD7FF)
112: ->add(0xF900, 0xFDCF)->add(0xFDF0, 0xFFFD)->add(0x10000, 0xEFFFF);
113: }
114: // @codeCoverageIgnoreEnd
115:
116: return $range->validate($codepoint);
117: }
118:
119: /**
120: * 指定された Unicode 符号点が NameChar の範囲に適合するかどうかを調べます.
121: *
122: * @param int $codepoint Unicode 符号点
123: * @return bool 引数が NameChar である場合のみ true
124: */
125: private static function validateNameChar($codepoint)
126: {
127: // @codeCoverageIgnoreStart
128: static $range = null;
129: if ($range === null) {
130: $first = new NameValidator_Range(0x30, 0x39); // 0-9
131: $range = $first
132: ->add(0x2D, 0x2E)->add(0xB7) // "-", ".", MIDDLE DOT
133: ->add(0x0300, 0x036F)->add(0x203F, 0x2040);
134: }
135: // @codeCoverageIgnoreEnd
136:
137: return self::validateNameStartChar($codepoint) || $range->validate($codepoint);
138: }
139: }
140:
141: /**
142: * 指定された Unicode 符号点が, 特定の範囲内に存在するかどうかを確認します.
143: * @ignore
144: */
145: class NameValidator_Range
146: {
147: /**
148: * 範囲の最小値です.
149: * @var int
150: */
151: private $min;
152:
153: /**
154: * 範囲の最大値です.
155: * @var int
156: */
157: private $max;
158:
159: /**
160: * 検査対象の Unicode 符号点がこのオブジェクトの
161: * $min と $max の範囲内になかった場合に使用される次の Range オブジェクトです.
162: *
163: * @var NameValidator_Range
164: */
165: private $next;
166:
167: /**
168: * 指定された Unicode 符号点の範囲を持つ NameValidator_Range オブジェクトを構築します.
169: *
170: * @param int $min 範囲の最小値
171: * @param int $max 範囲の最大値
172: * @param NameValidator_Range $next 次の Range オブジェクト (add が使用します)
173: * @codeCoverageIgnore
174: */
175: public function __construct($min, $max, NameValidator_Range $next = null)
176: {
177: $this->min = $min;
178: $this->max = ($max === null) ? $min : $max;
179: $this->next = $next;
180: }
181:
182: /**
183: * このオブジェクトを次の Range オブジェクトとして設定した新しい Range オブジェクトを返します.
184: *
185: * @param int $min 範囲の最小値
186: * @param int $max 範囲の最大値 (単一の値を指定する場合は省略可)
187: * @return NameValidator_Range
188: * @codeCoverageIgnore
189: */
190: public function add($min, $max = null)
191: {
192: return new self($min, $max, $this);
193: }
194:
195: /**
196: * 指定された Unicode 符号点がこのオブジェクトが示す範囲内に存在するかどうか調べます.
197: * 次の Range オブジェクトが設定されている場合, 再帰的に validate() を実行した結果を返します.
198: *
199: * @param int $codepoint Unicode 符号点
200: * @return bool 引数が範囲内に存在する場合のみ true
201: */
202: public function validate($codepoint)
203: {
204: return
205: ($this->min <= $codepoint && $codepoint <= $this->max) ||
206: ($this->next !== null && $this->next->validate($codepoint));
207: }
208: }
209: