1: <?php
2: /*
3: * Copyright (c) 2016 @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.2.0
27: */
28: namespace Peach\Http\Header;
29:
30: use InvalidArgumentException;
31: use Peach\DT\Timestamp;
32: use Peach\DT\Util;
33: use Peach\Util\Values;
34:
35: /**
36: * Set-Cookie ヘッダーの各種属性をあらわすクラスです.
37: * このクラスのオブジェクトに対する変更は, 関連するすべての CookieItem オブジェクトに影響を与えます.
38: */
39: class CookieOptions
40: {
41: /**
42: * expires 属性の時刻をあらわす Timestamp オブジェクトです.
43: *
44: * @var Timestamp
45: */
46: private $expires;
47:
48: /**
49: * このプログラムが扱う時刻のタイムゾーンです.
50: * この値が null の場合はデフォルトのタイムゾーンを適用します.
51: *
52: * @var int
53: */
54: private $timeZoneOffset;
55:
56: /**
57: * max-age 属性をあらわす整数です.
58: * @var int
59: */
60: private $maxAge;
61:
62: /**
63: * domain 属性をあらわす文字列です.
64: *
65: * @var string
66: */
67: private $domain;
68:
69: /**
70: * path 属性をあらわす文字列です.
71: *
72: * @var string
73: */
74: private $path;
75:
76: /**
77: * secure 属性をあらわす論理値です.
78: * true の場合のみ secure 属性が付与されます.
79: *
80: * @var bool
81: */
82: private $secure;
83:
84: /**
85: * 属性を何も持たない, 新しい CookieOptions オブジェクトを構築します.
86: */
87: public function __construct()
88: {
89: $this->secure = false;
90: $this->httpOnly = false;
91: }
92:
93: /**
94: * expires 属性の時刻を設定します.
95: * 引数に null を設定した場合は expires 属性を削除します.
96: *
97: * @param Timestamp $expires Set-Cookie の expires 属性として表現される時刻
98: */
99: public function setExpires(Timestamp $expires = null)
100: {
101: $this->expires = $expires;
102: }
103:
104: /**
105: * expires 属性の時刻を返します.
106: * expires 属性が設定されていない場合は null を返します.
107: *
108: * @return Timestamp expires 属性の時刻
109: */
110: public function getExpires()
111: {
112: return $this->expires;
113: }
114:
115: /**
116: * このオブジェクトが取り扱う Timestamp オブジェクトの時差を分単位でセットします.
117: * このメソッドは expires 属性の出力に影響します.
118: * PHP の date.timezone 設定がシステムの時差と異なる場合に使用してください.
119: * 通常はこのメソッドを使用する必要はありません.
120: *
121: * もしも引数に -23:45 (1425) 以上または +23:45 (-1425) 未満の値を指定した場合は
122: * -23:45 または +23:45 に丸めた結果を返します.
123: *
124: * @param int $offset 時差
125: * @see Util::cleanTimeZoneOffset()
126: */
127: public function setTimeZoneOffset($offset)
128: {
129: $this->timeZoneOffset = ($offset === null) ? null : Util::cleanTimeZoneOffset($offset);
130: }
131:
132: /**
133: * このオブジェクトが取り扱う Timestamp オブジェクトの時差を返します.
134: * このメソッドはデフォルトで null を返します.
135: *
136: * @return int 時差. ただしデフォルトの場合は null
137: */
138: public function getTimeZoneOffset()
139: {
140: return $this->timeZoneOffset;
141: }
142:
143: /**
144: * max-age 属性の値をセットします.
145: * 引数に 0 をセットした場合は対象の Cookie がブラウザから削除されます.
146: * 引数が 0 未満の値の場合は 0 として扱われます.
147: *
148: * @param int $maxAge max-age 属性の値
149: */
150: public function setMaxAge($maxAge)
151: {
152: $this->maxAge = ($maxAge === null) ? null : Values::intValue($maxAge, 0);
153: }
154:
155: /**
156: * max-age 属性の値を返します.
157: * もしも max-age 属性がセットされていない場合は null を返します.
158: *
159: * @return int max-age 属性の値. セットされていない場合は null
160: */
161: public function getMaxAge()
162: {
163: return $this->maxAge;
164: }
165:
166: /**
167: * domain 属性の値をセットします.
168: * 引数に null をセットした場合は domain 属性を削除します.
169: *
170: * @param string $domain domain 属性の値
171: */
172: public function setDomain($domain)
173: {
174: if (!$this->validateDomain($domain)) {
175: throw new InvalidArgumentException("Invalid domain: '{$domain}'");
176: }
177: $this->domain = $domain;
178: }
179:
180: /**
181: * 指定された文字列が, ドメイン名として妥当かどうかを確認します.
182: * RFC 1035 に基づいて, 引数の文字列が以下の BNF 記法を満たすかどうかを調べます.
183: * 妥当な場合は true, そうでない場合は false を返します.
184: *
185: * ただし, 本来は Invalid にも関わらず実際に使われているドメイン名に対応するため
186: * label の先頭の数字文字列を敢えて許す実装となっています.
187: *
188: * <pre>
189: * {domain} ::= {subdomain} | " "
190: * {subdomain} ::= {label} | {subdomain} "." {label}
191: * {label} ::= {letter} [ [ {ldh-str} ] {let-dig} ]
192: * {ldh-str} ::= {let-dig-hyp} | {let-dig-hyp} {ldh-str}
193: * {let-dig-hyp} ::= {let-dig} | "-"
194: * {let-dig} ::= {letter} | {digit}
195: * </pre>
196: *
197: * @param string $domain 検査対象のドメイン名
198: * @return bool 引数がドメイン名として妥当な場合のみ true
199: */
200: private function validateDomain($domain)
201: {
202: if ($domain === null) {
203: return true;
204: }
205: $letter = "[a-zA-Z0-9]";
206: $letDigHyp = "(-|{$letter})";
207: $label = "{$letter}({$letDigHyp}*{$letter})*";
208: $pattern = "{$label}(\\.{$label})*";
209: return preg_match("/\\A{$pattern}\\z/", $domain);
210: }
211:
212: /**
213: * domain 属性の値を返します.
214: * domain 属性がセットされていない場合は null を返します.
215: *
216: * @return string
217: */
218: public function getDomain()
219: {
220: return $this->domain;
221: }
222:
223: /**
224: * path 属性の値をセットします.
225: * 引数に null をセットした場合は path 属性を削除します.
226: *
227: * @param string $path
228: */
229: public function setPath($path)
230: {
231: if (!$this->validatePath($path)) {
232: throw new InvalidArgumentException("Invalid path: '{$path}'");
233: }
234: $this->path = $path;
235: }
236:
237: /**
238: * 指定された文字列が RFC 3986 にて定義される URI のパス文字列として妥当かどうかを検証します.
239: * フォーマットは以下の BNF 記法に基づきます.
240: *
241: * <pre>
242: * path-absolute = "/" [ segment-nz *( "/" segment ) ]
243: * segment = *pchar
244: * segment-nz = 1*pchar
245: * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
246: * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
247: * pct-encoded = "%" HEXDIG HEXDIG
248: * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
249: * </pre>
250: *
251: * @param string $path 検査対象のパス
252: * @return bool 引数がパスとして妥当な場合のみ true
253: */
254: private function validatePath($path)
255: {
256: if ($path === null) {
257: return true;
258: }
259:
260: $classUnreserved = "a-zA-Z0-9\\-\\._~";
261: $classSubDelims = "!\$&'\\(\\)";
262: $classOthers = ":@";
263: $validChars = "[{$classUnreserved}{$classSubDelims}{$classOthers}]";
264: $pctEncoded = "%[0-9a-fA-F]{2}";
265: $pchar = "{$validChars}|{$pctEncoded}";
266: $segment = "({$pchar})*";
267: $segmentNz = "({$pchar})+";
268: $pathAbsolute = "\\/({$segmentNz}(\\/{$segment})*)?";
269: return preg_match("/\\A{$pathAbsolute}\\z/", $path);
270: }
271:
272: /**
273: * path 属性の値を返します.
274: * もしも path 属性がセットされていない場合は null を返します.
275: *
276: * @return string path 属性の値. セットされていない場合は null
277: */
278: public function getPath()
279: {
280: return $this->path;
281: }
282:
283: /**
284: * secure 属性をセットします.
285: * もしも引数が true の場合は secure 属性を有効化, false の場合は無効化します.
286: *
287: * @param bool $secure secure 属性を有効化する場合は true, 無効化する場合は false
288: */
289: public function setSecure($secure)
290: {
291: $this->secure = (bool) $secure;
292: }
293:
294: /**
295: * secure 属性が有効かどうかを判定します.
296: * secure 属性が有効な場合は true, そうでない場合は false を返します.
297: * もしもこのオブジェクトの setSecure() を一度も実行していない場合,
298: * secure 属性は無効となるため false を返します.
299: *
300: * @return bool secure 属性が有効な場合は true, そうでない場合は false
301: */
302: public function hasSecure()
303: {
304: return $this->secure;
305: }
306:
307: /**
308: * httponly 属性をセットします.
309: * もしも引数が true の場合は httponly 属性を有効化, false の場合は無効化します.
310: *
311: * @param bool $httpOnly httponly 属性を有効化する場合は true, 無効化する場合は false
312: */
313: public function setHttpOnly($httpOnly)
314: {
315: $this->httpOnly = (bool) $httpOnly;
316: }
317:
318: /**
319: * httponly 属性が有効かどうかを判定します.
320: * httponly 属性が有効な場合は true, そうでない場合は false を返します.
321: * もしもこのオブジェクトの setHttpOnly() を一度も実行していない場合,
322: * httponly 属性は無効となるため false を返します.
323: *
324: * @return bool httponly 属性が有効な場合は true, そうでない場合は false
325: */
326: public function hasHttpOnly()
327: {
328: return $this->httpOnly;
329: }
330:
331: /**
332: * このオブジェクトが持つ各属性を書式化し, 結果を配列で返します.
333: *
334: * @return array 各属性を書式化した結果の配列
335: * @ignore
336: * @todo 複数の Set-Cookie ヘッダーで同じオプションを適用することを想定し, 返り値をキャッシュできるようにする
337: */
338: public function formatOptions()
339: {
340: $result = array();
341: if ($this->expires !== null) {
342: $result[] = $this->formatExpires();
343: }
344: if ($this->maxAge !== null) {
345: $result[] = "max-age={$this->maxAge}";
346: }
347: if ($this->domain !== null) {
348: $result[] = "domain={$this->domain}";
349: }
350: if ($this->path !== null) {
351: $result[] = "path={$this->path}";
352: }
353: if ($this->secure) {
354: $result[] = "secure";
355: }
356: if ($this->httpOnly) {
357: $result[] = "httponly";
358: }
359: return $result;
360: }
361:
362: /**
363: * expires 属性を書式化します.
364: *
365: * @return string "expires=Wdy, DD-Mon-YY HH:MM:SS GMT" 形式の文字列
366: */
367: private function formatExpires()
368: {
369: $format = CookieExpiresFormat::getInstance();
370: $offset = Util::cleanTimeZoneOffset($this->timeZoneOffset);
371: $date = $format->format($this->expires, $offset);
372: return "expires={$date}";
373: }
374: }
375: