]> git.immae.eu Git - github/wallabag/wallabag.git/blame - inc/3rdparty/libraries/humble-http-agent/CookieJar.php
[change] we now use Full-Text RSS 3.1, thank you so much @fivefilters
[github/wallabag/wallabag.git] / inc / 3rdparty / libraries / humble-http-agent / CookieJar.php
CommitLineData
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
24class 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?>