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