]>
Commit | Line | Data |
---|---|---|
ec397236 NL |
1 | <?php |
2 | /** | |
3 | * Cookie Jar | |
4 | * | |
5 | * PHP class for handling cookies, as defined by the Netscape spec: | |
6 | * <http://curl.haxx.se/rfc/cookie_spec.html> | |
7 | * | |
8 | * This class should be used to handle cookies (storing cookies from HTTP response messages, and | |
9 | * sending out cookies in HTTP request messages). This has been adapted for FiveFilters.org | |
10 | * from the original version used in HTTP Navigator. See http://www.keyvan.net/code/http-navigator/ | |
11 | * | |
12 | * This class is mainly based on Cookies.pm <http://search.cpan.org/author/GAAS/libwww-perl-5.65/ | |
13 | * lib/HTTP/Cookies.pm> from the libwww-perl collection <http://www.linpro.no/lwp/>. | |
14 | * Unlike Cookies.pm, this class only supports the Netscape cookie spec, not RFC 2965. | |
15 | * | |
16 | * @version 0.5 | |
17 | * @date 2011-03-15 | |
18 | * @see http://php.net/HttpRequestPool | |
19 | * @author Keyvan Minoukadeh | |
20 | * @copyright 2011 Keyvan Minoukadeh | |
21 | * @license http://www.gnu.org/licenses/agpl-3.0.html AGPL v3 | |
22 | */ | |
23 | ||
24 | class CookieJar | |
25 | { | |
26 | /** | |
27 | * Cookies - array containing all cookies. | |
28 | * | |
29 | * <pre> | |
30 | * Cookies are stored like this: | |
31 | * [domain][path][name] = array | |
32 | * where array is: | |
33 | * 0 => value, 1 => secure, 2 => expires | |
34 | * </pre> | |
35 | * @var array | |
36 | * @access private | |
37 | */ | |
38 | public $cookies = array(); | |
39 | public $debug = false; | |
40 | ||
41 | /** | |
42 | * Constructor | |
43 | */ | |
44 | function __construct() { | |
45 | } | |
46 | ||
47 | protected function debug($msg, $file=null, $line=null) { | |
48 | if ($this->debug) { | |
49 | $mem = round(memory_get_usage()/1024, 2); | |
50 | $memPeak = round(memory_get_peak_usage()/1024, 2); | |
51 | echo '* ',$msg; | |
52 | if (isset($file, $line)) echo " ($file line $line)"; | |
53 | echo ' - mem used: ',$mem," (peak: $memPeak)\n"; | |
54 | ob_flush(); | |
55 | flush(); | |
56 | } | |
57 | } | |
58 | ||
59 | /** | |
60 | * Get matching cookies | |
61 | * | |
62 | * Only use this method if you cannot use add_cookie_header(), for example, if you want to use | |
63 | * this cookie jar class without using the request class. | |
64 | * | |
65 | * @param array $param associative array containing 'domain', 'path', 'secure' keys | |
66 | * @return string | |
67 | * @see add_cookie_header() | |
68 | */ | |
69 | public function getMatchingCookies($url) | |
70 | { | |
71 | if (($parts = @parse_url($url)) && isset($parts['scheme'], $parts['host'], $parts['path'])) { | |
72 | $param['domain'] = $parts['host']; | |
73 | $param['path'] = $parts['path']; | |
74 | $param['secure'] = (strtolower($parts['scheme']) == 'https'); | |
75 | unset($parts); | |
76 | } else { | |
77 | return false; | |
78 | } | |
79 | // RFC 2965 notes: | |
80 | // If multiple cookies satisfy the criteria above, they are ordered in | |
81 | // the Cookie header such that those with more specific Path attributes | |
82 | // precede those with less specific. Ordering with respect to other | |
83 | // attributes (e.g., Domain) is unspecified. | |
84 | $domain = $param['domain']; | |
85 | if (strpos($domain, '.') === false) $domain .= '.local'; | |
86 | $request_path = $param['path']; | |
87 | if ($request_path == '') $request_path = '/'; | |
88 | $request_secure = $param['secure']; | |
89 | $now = time(); | |
90 | $matched_cookies = array(); | |
91 | // domain - find matching domains | |
92 | $this->debug('Finding matching domains for '.$domain, __FILE__, __LINE__); | |
93 | while (strpos($domain, '.') !== false) { | |
94 | if (isset($this->cookies[$domain])) { | |
95 | $this->debug(' domain match found: '.$domain); | |
96 | $cookies =& $this->cookies[$domain]; | |
97 | } else { | |
98 | $domain = $this->_reduce_domain($domain); | |
99 | continue; | |
100 | } | |
101 | // paths - find matching paths starting from most specific | |
102 | $this->debug(' - Finding matching paths for '.$request_path); | |
103 | $paths = array_keys($cookies); | |
104 | usort($paths, array($this, '_cmp_length')); | |
105 | foreach ($paths as $path) { | |
106 | // continue to next cookie if request path does not path-match cookie path | |
107 | if (!$this->_path_match($request_path, $path)) continue; | |
108 | // loop through cookie names | |
109 | $this->debug(' path match found: '.$path); | |
110 | foreach ($cookies[$path] as $name => $values) { | |
111 | // if this cookie is secure but request isn't, continue to next cookie | |
112 | if ($values[1] && !$request_secure) continue; | |
113 | // if cookie is not a session cookie and has expired, continue to next cookie | |
114 | if (is_int($values[2]) && ($values[2] < $now)) continue; | |
115 | // cookie matches request | |
116 | $this->debug(' cookie match: '.$name.'='.$values[0]); | |
117 | $matched_cookies[] = $name.'='.$values[0]; | |
118 | } | |
119 | } | |
120 | $domain = $this->_reduce_domain($domain); | |
121 | } | |
122 | // return cookies | |
123 | return implode('; ', $matched_cookies); | |
124 | } | |
125 | ||
126 | /** | |
127 | * Parse Set-Cookie values. | |
128 | * | |
129 | * Only use this method if you cannot use extract_cookies(), for example, if you want to use | |
130 | * this cookie jar class without using the response class. | |
131 | * | |
132 | * @param array $set_cookies array holding 1 or more "Set-Cookie" header values | |
133 | * @param array $param associative array containing 'host', 'path' keys | |
134 | * @return void | |
135 | * @see extract_cookies() | |
136 | */ | |
137 | public function storeCookies($url, $set_cookies) | |
138 | { | |
139 | if (count($set_cookies) == 0) return; | |
140 | $param = @parse_url($url); | |
141 | if (!is_array($param) || !isset($param['host'])) return; | |
142 | $request_host = $param['host']; | |
143 | if (strpos($request_host, '.') === false) $request_host .= '.local'; | |
144 | $request_path = @$param['path']; | |
145 | if ($request_path == '') $request_path = '/'; | |
146 | // | |
147 | // loop through set-cookie headers | |
148 | // | |
149 | foreach ($set_cookies as $set_cookie) { | |
150 | $this->debug('Parsing: '.$set_cookie); | |
151 | // temporary cookie store (before adding to jar) | |
152 | $tmp_cookie = array(); | |
153 | $param = explode(';', $set_cookie); | |
154 | // loop through params | |
155 | for ($x=0; $x<count($param); $x++) { | |
156 | $key_val = explode('=', $param[$x], 2); | |
157 | if (count($key_val) != 2) { | |
158 | // if the first param isn't a name=value pair, continue to the next set-cookie | |
159 | // header | |
160 | if ($x == 0) continue 2; | |
161 | // check for secure flag | |
162 | if (strtolower(trim($key_val[0])) == 'secure') $tmp_cookie['secure'] = true; | |
163 | // continue to next param | |
164 | continue; | |
165 | } | |
166 | list($key, $val) = array_map('trim', $key_val); | |
167 | // first name=value pair is the cookie name and value | |
168 | // the name and value are stored under 'name' and 'value' to avoid conflicts | |
169 | // with later parameters. | |
170 | if ($x == 0) { | |
171 | $tmp_cookie = array('name'=>$key, 'value'=>$val); | |
172 | continue; | |
173 | } | |
174 | $key = strtolower($key); | |
175 | if (in_array($key, array('expires', 'path', 'domain', 'secure'))) { | |
176 | $tmp_cookie[$key] = $val; | |
177 | } | |
178 | } | |
179 | // | |
180 | // set cookie | |
181 | // | |
182 | // check domain | |
183 | if (isset($tmp_cookie['domain']) && ($tmp_cookie['domain'] != $request_host) && | |
184 | ($tmp_cookie['domain'] != ".$request_host")) { | |
185 | $domain = $tmp_cookie['domain']; | |
186 | if ((strpos($domain, '.') === false) && ($domain != 'local')) { | |
187 | $this->debug(' - domain "'.$domain.'" has no dot and is not a local domain'); | |
188 | continue; | |
189 | } | |
190 | if (preg_match('/\.[0-9]+$/', $domain)) { | |
191 | $this->debug(' - domain "'.$domain.'" appears to be an ip address'); | |
192 | continue; | |
193 | } | |
194 | if (substr($domain, 0, 1) != '.') $domain = ".$domain"; | |
195 | if (!$this->_domain_match($request_host, $domain)) { | |
196 | $this->debug(' - request host "'.$request_host.'" does not domain-match "'.$domain.'"'); | |
197 | continue; | |
198 | } | |
199 | } else { | |
200 | // if domain is not specified in the set-cookie header, domain will default to | |
201 | // the request host | |
202 | $domain = $request_host; | |
203 | } | |
204 | // check path | |
205 | if (isset($tmp_cookie['path']) && ($tmp_cookie['path'] != '')) { | |
206 | $path = urldecode($tmp_cookie['path']); | |
207 | if (!$this->_path_match($request_path, $path)) { | |
208 | $this->debug(' - request path "'.$request_path.'" does not path-match "'.$path.'"'); | |
209 | continue; | |
210 | } | |
211 | } else { | |
212 | $path = $request_path; | |
213 | $path = substr($path, 0, strrpos($path, '/')); | |
214 | if ($path == '') $path = '/'; | |
215 | } | |
216 | // check if secure | |
217 | $secure = (isset($tmp_cookie['secure'])) ? true : false; | |
218 | // check expiry | |
219 | if (isset($tmp_cookie['expires'])) { | |
220 | if (($expires = strtotime($tmp_cookie['expires'])) < 0) { | |
221 | $expires = null; | |
222 | } | |
223 | } else { | |
224 | $expires = null; | |
225 | } | |
226 | // set cookie | |
227 | $this->set_cookie($domain, $path, $tmp_cookie['name'], $tmp_cookie['value'], $secure, $expires); | |
228 | } | |
229 | } | |
230 | ||
231 | // return array of set-cookie values extracted from HTTP response headers (string $h) | |
232 | public function extractCookies($h) { | |
233 | $x = 0; | |
234 | $lines = 0; | |
235 | $headers = array(); | |
236 | $last_match = false; | |
237 | $h = explode("\n", $h); | |
238 | foreach ($h as $line) { | |
239 | $line = rtrim($line); | |
240 | $lines++; | |
241 | ||
242 | $trimmed_line = trim($line); | |
243 | if (isset($line_last)) { | |
244 | // check if we have \r\n\r\n (indicating the end of headers) | |
245 | // some servers will not use CRLF (\r\n), so we make CR (\r) optional. | |
246 | // if (preg_match('/\015?\012\015?\012/', $line_last.$line)) { | |
247 | // break; | |
248 | // } | |
249 | // As an alternative, we can check if the current trimmed line is empty | |
250 | if ($trimmed_line == '') { | |
251 | break; | |
252 | } | |
253 | ||
254 | // check for continuation line... | |
255 | // RFC 2616 Section 2.2 "Basic Rules": | |
256 | // HTTP/1.1 header field values can be folded onto multiple lines if the | |
257 | // continuation line begins with a space or horizontal tab. All linear | |
258 | // white space, including folding, has the same semantics as SP. A | |
259 | // recipient MAY replace any linear white space with a single SP before | |
260 | // interpreting the field value or forwarding the message downstream. | |
261 | if ($last_match && preg_match('/^\s+(.*)/', $line, $match)) { | |
262 | // append to previous header value | |
263 | $headers[$x-1] .= ' '.rtrim($match[1]); | |
264 | continue; | |
265 | } | |
266 | } | |
267 | $line_last = $line; | |
268 | ||
269 | // split header name and value | |
270 | if (preg_match('/^Set-Cookie\s*:\s*(.*)/i', $line, $match)) { | |
271 | $headers[$x++] = rtrim($match[1]); | |
272 | $last_match = true; | |
273 | } else { | |
274 | $last_match = false; | |
275 | } | |
276 | } | |
277 | return $headers; | |
278 | } | |
279 | ||
280 | /** | |
281 | * Set Cookie | |
282 | * @param string $domain | |
283 | * @param string $path | |
284 | * @param string $name cookie name | |
285 | * @param string $value cookie value | |
286 | * @param bool $secure | |
287 | * @param int $expires expiry time (null if session cookie, <= 0 will delete cookie) | |
288 | * @return void | |
289 | */ | |
290 | function set_cookie($domain, $path, $name, $value, $secure=false, $expires=null) | |
291 | { | |
292 | if ($domain == '') return; | |
293 | if ($path == '') return; | |
294 | if ($name == '') return; | |
295 | // check if cookie needs to go | |
296 | if (isset($expires) && ($expires <= 0)) { | |
297 | if (isset($this->cookies[$domain][$path][$name])) unset($this->cookies[$domain][$path][$name]); | |
298 | return; | |
299 | } | |
300 | if ($value == '') return; | |
301 | $this->cookies[$domain][$path][$name] = array($value, $secure, $expires); | |
302 | return; | |
303 | } | |
304 | ||
305 | /** | |
306 | * Clear cookies - [domain [,path [,name]]] - call method with no arguments to clear all cookies. | |
307 | * @param string $domain | |
308 | * @param string $path | |
309 | * @param string $name | |
310 | * @return void | |
311 | */ | |
312 | function clear($domain=null, $path=null, $name=null) | |
313 | { | |
314 | if (!isset($domain)) { | |
315 | $this->cookies = array(); | |
316 | } elseif (!isset($path)) { | |
317 | if (isset($this->cookies[$domain])) unset($this->cookies[$domain]); | |
318 | } elseif (!isset($name)) { | |
319 | if (isset($this->cookies[$domain][$path])) unset($this->cookies[$domain][$path]); | |
320 | } elseif (isset($name)) { | |
321 | if (isset($this->cookies[$domain][$path][$name])) unset($this->cookies[$domain][$path][$name]); | |
322 | } | |
323 | } | |
324 | ||
325 | /** | |
326 | * Compare string length - used for sorting | |
327 | * @access private | |
328 | * @return int | |
329 | */ | |
330 | function _cmp_length($a, $b) | |
331 | { | |
332 | $la = strlen($a); $lb = strlen($b); | |
333 | if ($la == $lb) return 0; | |
334 | return ($la > $lb) ? -1 : 1; | |
335 | } | |
336 | ||
337 | /** | |
338 | * Reduce domain | |
339 | * @param string $domain | |
340 | * @return string | |
341 | * @access private | |
342 | */ | |
343 | function _reduce_domain($domain) | |
344 | { | |
345 | if ($domain == '') return ''; | |
346 | if (substr($domain, 0, 1) == '.') return substr($domain, 1); | |
347 | return substr($domain, strpos($domain, '.')); | |
348 | } | |
349 | ||
350 | /** | |
351 | * Path match - check if path1 path-matches path2 | |
352 | * | |
353 | * From RFC 2965: | |
354 | * <i>For two strings that represent paths, P1 and P2, P1 path-matches P2 | |
355 | * if P2 is a prefix of P1 (including the case where P1 and P2 string- | |
356 | * compare equal). Thus, the string /tec/waldo path-matches /tec.</i> | |
357 | * @param string $path1 | |
358 | * @param string $path2 | |
359 | * @return bool | |
360 | * @access private | |
361 | */ | |
362 | function _path_match($path1, $path2) | |
363 | { | |
364 | return (substr($path1, 0, strlen($path2)) == $path2); | |
365 | } | |
366 | ||
367 | /** | |
368 | * Domain match - check if domain1 domain-matches domain2 | |
369 | * | |
370 | * A few extracts from RFC 2965: | |
371 | * - A Set-Cookie2 from request-host y.x.foo.com for Domain=.foo.com | |
372 | * would be rejected, because H is y.x and contains a dot. | |
373 | * | |
374 | * - A Set-Cookie2 from request-host x.foo.com for Domain=.foo.com | |
375 | * would be accepted. | |
376 | * | |
377 | * - A Set-Cookie2 with Domain=.com or Domain=.com., will always be | |
378 | * rejected, because there is no embedded dot. | |
379 | * | |
380 | * - A Set-Cookie2 from request-host example for Domain=.local will | |
381 | * be accepted, because the effective host name for the request- | |
382 | * host is example.local, and example.local domain-matches .local. | |
383 | * | |
384 | * I'm ignoring the first point for now (must check to see how other browsers handle | |
385 | * this rule for Set-Cookie headers) | |
386 | * | |
387 | * @param string $domain1 | |
388 | * @param string $domain2 | |
389 | * @return bool | |
390 | * @access private | |
391 | */ | |
392 | function _domain_match($domain1, $domain2) | |
393 | { | |
394 | $domain1 = strtolower($domain1); | |
395 | $domain2 = strtolower($domain2); | |
396 | while (strpos($domain1, '.') !== false) { | |
397 | if ($domain1 == $domain2) return true; | |
398 | $domain1 = $this->_reduce_domain($domain1); | |
399 | continue; | |
400 | } | |
401 | return false; | |
402 | } | |
403 | } | |
404 | ?> |