Parser.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. <?php
  2. /**
  3. * SimplePie
  4. *
  5. * A PHP-Based RSS and Atom Feed Framework.
  6. * Takes the hard work out of managing a complete RSS/Atom solution.
  7. *
  8. * Copyright (c) 2004-2016, Ryan Parman, Geoffrey Sneddon, Ryan McCue, and contributors
  9. * All rights reserved.
  10. *
  11. * Redistribution and use in source and binary forms, with or without modification, are
  12. * permitted provided that the following conditions are met:
  13. *
  14. * * Redistributions of source code must retain the above copyright notice, this list of
  15. * conditions and the following disclaimer.
  16. *
  17. * * Redistributions in binary form must reproduce the above copyright notice, this list
  18. * of conditions and the following disclaimer in the documentation and/or other materials
  19. * provided with the distribution.
  20. *
  21. * * Neither the name of the SimplePie Team nor the names of its contributors may be used
  22. * to endorse or promote products derived from this software without specific prior
  23. * written permission.
  24. *
  25. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
  26. * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
  27. * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS
  28. * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  29. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  30. * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  31. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
  32. * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  33. * POSSIBILITY OF SUCH DAMAGE.
  34. *
  35. * @package SimplePie
  36. * @copyright 2004-2016 Ryan Parman, Geoffrey Sneddon, Ryan McCue
  37. * @author Ryan Parman
  38. * @author Geoffrey Sneddon
  39. * @author Ryan McCue
  40. * @link http://simplepie.org/ SimplePie
  41. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  42. */
  43. /**
  44. * HTTP Response Parser
  45. *
  46. * @package SimplePie
  47. * @subpackage HTTP
  48. */
  49. class SimplePie_HTTP_Parser
  50. {
  51. /**
  52. * HTTP Version
  53. *
  54. * @var float
  55. */
  56. public $http_version = 0.0;
  57. /**
  58. * Status code
  59. *
  60. * @var int
  61. */
  62. public $status_code = 0;
  63. /**
  64. * Reason phrase
  65. *
  66. * @var string
  67. */
  68. public $reason = '';
  69. /**
  70. * Key/value pairs of the headers
  71. *
  72. * @var array
  73. */
  74. public $headers = array();
  75. /**
  76. * Body of the response
  77. *
  78. * @var string
  79. */
  80. public $body = '';
  81. /**
  82. * Current state of the state machine
  83. *
  84. * @var string
  85. */
  86. protected $state = 'http_version';
  87. /**
  88. * Input data
  89. *
  90. * @var string
  91. */
  92. protected $data = '';
  93. /**
  94. * Input data length (to avoid calling strlen() everytime this is needed)
  95. *
  96. * @var int
  97. */
  98. protected $data_length = 0;
  99. /**
  100. * Current position of the pointer
  101. *
  102. * @var int
  103. */
  104. protected $position = 0;
  105. /**
  106. * Name of the hedaer currently being parsed
  107. *
  108. * @var string
  109. */
  110. protected $name = '';
  111. /**
  112. * Value of the hedaer currently being parsed
  113. *
  114. * @var string
  115. */
  116. protected $value = '';
  117. /**
  118. * Create an instance of the class with the input data
  119. *
  120. * @param string $data Input data
  121. */
  122. public function __construct($data)
  123. {
  124. $this->data = $data;
  125. $this->data_length = strlen($this->data);
  126. }
  127. /**
  128. * Parse the input data
  129. *
  130. * @return bool true on success, false on failure
  131. */
  132. public function parse()
  133. {
  134. while ($this->state && $this->state !== 'emit' && $this->has_data())
  135. {
  136. $state = $this->state;
  137. $this->$state();
  138. }
  139. $this->data = '';
  140. if ($this->state === 'emit' || $this->state === 'body')
  141. {
  142. return true;
  143. }
  144. else
  145. {
  146. $this->http_version = '';
  147. $this->status_code = '';
  148. $this->reason = '';
  149. $this->headers = array();
  150. $this->body = '';
  151. return false;
  152. }
  153. }
  154. /**
  155. * Check whether there is data beyond the pointer
  156. *
  157. * @return bool true if there is further data, false if not
  158. */
  159. protected function has_data()
  160. {
  161. return (bool) ($this->position < $this->data_length);
  162. }
  163. /**
  164. * See if the next character is LWS
  165. *
  166. * @return bool true if the next character is LWS, false if not
  167. */
  168. protected function is_linear_whitespace()
  169. {
  170. return (bool) ($this->data[$this->position] === "\x09"
  171. || $this->data[$this->position] === "\x20"
  172. || ($this->data[$this->position] === "\x0A"
  173. && isset($this->data[$this->position + 1])
  174. && ($this->data[$this->position + 1] === "\x09" || $this->data[$this->position + 1] === "\x20")));
  175. }
  176. /**
  177. * Parse the HTTP version
  178. */
  179. protected function http_version()
  180. {
  181. if (strpos($this->data, "\x0A") !== false && strtoupper(substr($this->data, 0, 5)) === 'HTTP/')
  182. {
  183. $len = strspn($this->data, '0123456789.', 5);
  184. $this->http_version = substr($this->data, 5, $len);
  185. $this->position += 5 + $len;
  186. if (substr_count($this->http_version, '.') <= 1)
  187. {
  188. $this->http_version = (float) $this->http_version;
  189. $this->position += strspn($this->data, "\x09\x20", $this->position);
  190. $this->state = 'status';
  191. }
  192. else
  193. {
  194. $this->state = false;
  195. }
  196. }
  197. else
  198. {
  199. $this->state = false;
  200. }
  201. }
  202. /**
  203. * Parse the status code
  204. */
  205. protected function status()
  206. {
  207. if ($len = strspn($this->data, '0123456789', $this->position))
  208. {
  209. $this->status_code = (int) substr($this->data, $this->position, $len);
  210. $this->position += $len;
  211. $this->state = 'reason';
  212. }
  213. else
  214. {
  215. $this->state = false;
  216. }
  217. }
  218. /**
  219. * Parse the reason phrase
  220. */
  221. protected function reason()
  222. {
  223. $len = strcspn($this->data, "\x0A", $this->position);
  224. $this->reason = trim(substr($this->data, $this->position, $len), "\x09\x0D\x20");
  225. $this->position += $len + 1;
  226. $this->state = 'new_line';
  227. }
  228. /**
  229. * Deal with a new line, shifting data around as needed
  230. */
  231. protected function new_line()
  232. {
  233. $this->value = trim($this->value, "\x0D\x20");
  234. if ($this->name !== '' && $this->value !== '')
  235. {
  236. $this->name = strtolower($this->name);
  237. // We should only use the last Content-Type header. c.f. issue #1
  238. if (isset($this->headers[$this->name]) && $this->name !== 'content-type')
  239. {
  240. $this->headers[$this->name] .= ', ' . $this->value;
  241. }
  242. else
  243. {
  244. $this->headers[$this->name] = $this->value;
  245. }
  246. }
  247. $this->name = '';
  248. $this->value = '';
  249. if (substr($this->data[$this->position], 0, 2) === "\x0D\x0A")
  250. {
  251. $this->position += 2;
  252. $this->state = 'body';
  253. }
  254. elseif ($this->data[$this->position] === "\x0A")
  255. {
  256. $this->position++;
  257. $this->state = 'body';
  258. }
  259. else
  260. {
  261. $this->state = 'name';
  262. }
  263. }
  264. /**
  265. * Parse a header name
  266. */
  267. protected function name()
  268. {
  269. $len = strcspn($this->data, "\x0A:", $this->position);
  270. if (isset($this->data[$this->position + $len]))
  271. {
  272. if ($this->data[$this->position + $len] === "\x0A")
  273. {
  274. $this->position += $len;
  275. $this->state = 'new_line';
  276. }
  277. else
  278. {
  279. $this->name = substr($this->data, $this->position, $len);
  280. $this->position += $len + 1;
  281. $this->state = 'value';
  282. }
  283. }
  284. else
  285. {
  286. $this->state = false;
  287. }
  288. }
  289. /**
  290. * Parse LWS, replacing consecutive LWS characters with a single space
  291. */
  292. protected function linear_whitespace()
  293. {
  294. do
  295. {
  296. if (substr($this->data, $this->position, 2) === "\x0D\x0A")
  297. {
  298. $this->position += 2;
  299. }
  300. elseif ($this->data[$this->position] === "\x0A")
  301. {
  302. $this->position++;
  303. }
  304. $this->position += strspn($this->data, "\x09\x20", $this->position);
  305. } while ($this->has_data() && $this->is_linear_whitespace());
  306. $this->value .= "\x20";
  307. }
  308. /**
  309. * See what state to move to while within non-quoted header values
  310. */
  311. protected function value()
  312. {
  313. if ($this->is_linear_whitespace())
  314. {
  315. $this->linear_whitespace();
  316. }
  317. else
  318. {
  319. switch ($this->data[$this->position])
  320. {
  321. case '"':
  322. // Workaround for ETags: we have to include the quotes as
  323. // part of the tag.
  324. if (strtolower($this->name) === 'etag')
  325. {
  326. $this->value .= '"';
  327. $this->position++;
  328. $this->state = 'value_char';
  329. break;
  330. }
  331. $this->position++;
  332. $this->state = 'quote';
  333. break;
  334. case "\x0A":
  335. $this->position++;
  336. $this->state = 'new_line';
  337. break;
  338. default:
  339. $this->state = 'value_char';
  340. break;
  341. }
  342. }
  343. }
  344. /**
  345. * Parse a header value while outside quotes
  346. */
  347. protected function value_char()
  348. {
  349. $len = strcspn($this->data, "\x09\x20\x0A\"", $this->position);
  350. $this->value .= substr($this->data, $this->position, $len);
  351. $this->position += $len;
  352. $this->state = 'value';
  353. }
  354. /**
  355. * See what state to move to while within quoted header values
  356. */
  357. protected function quote()
  358. {
  359. if ($this->is_linear_whitespace())
  360. {
  361. $this->linear_whitespace();
  362. }
  363. else
  364. {
  365. switch ($this->data[$this->position])
  366. {
  367. case '"':
  368. $this->position++;
  369. $this->state = 'value';
  370. break;
  371. case "\x0A":
  372. $this->position++;
  373. $this->state = 'new_line';
  374. break;
  375. case '\\':
  376. $this->position++;
  377. $this->state = 'quote_escaped';
  378. break;
  379. default:
  380. $this->state = 'quote_char';
  381. break;
  382. }
  383. }
  384. }
  385. /**
  386. * Parse a header value while within quotes
  387. */
  388. protected function quote_char()
  389. {
  390. $len = strcspn($this->data, "\x09\x20\x0A\"\\", $this->position);
  391. $this->value .= substr($this->data, $this->position, $len);
  392. $this->position += $len;
  393. $this->state = 'value';
  394. }
  395. /**
  396. * Parse an escaped character within quotes
  397. */
  398. protected function quote_escaped()
  399. {
  400. $this->value .= $this->data[$this->position];
  401. $this->position++;
  402. $this->state = 'quote';
  403. }
  404. /**
  405. * Parse the body
  406. */
  407. protected function body()
  408. {
  409. $this->body = substr($this->data, $this->position);
  410. if (!empty($this->headers['transfer-encoding']))
  411. {
  412. unset($this->headers['transfer-encoding']);
  413. $this->state = 'chunked';
  414. }
  415. else
  416. {
  417. $this->state = 'emit';
  418. }
  419. }
  420. /**
  421. * Parsed a "Transfer-Encoding: chunked" body
  422. */
  423. protected function chunked()
  424. {
  425. if (!preg_match('/^([0-9a-f]+)[^\r\n]*\r\n/i', trim($this->body)))
  426. {
  427. $this->state = 'emit';
  428. return;
  429. }
  430. $decoded = '';
  431. $encoded = $this->body;
  432. while (true)
  433. {
  434. $is_chunked = (bool) preg_match( '/^([0-9a-f]+)[^\r\n]*\r\n/i', $encoded, $matches );
  435. if (!$is_chunked)
  436. {
  437. // Looks like it's not chunked after all
  438. $this->state = 'emit';
  439. return;
  440. }
  441. $length = hexdec(trim($matches[1]));
  442. if ($length === 0)
  443. {
  444. // Ignore trailer headers
  445. $this->state = 'emit';
  446. $this->body = $decoded;
  447. return;
  448. }
  449. $chunk_length = strlen($matches[0]);
  450. $decoded .= $part = substr($encoded, $chunk_length, $length);
  451. $encoded = substr($encoded, $chunk_length + $length + 2);
  452. if (trim($encoded) === '0' || empty($encoded))
  453. {
  454. $this->state = 'emit';
  455. $this->body = $decoded;
  456. return;
  457. }
  458. }
  459. }
  460. }