├── .gitignore ├── CHANGELOG.md ├── Source ├── Exception │ ├── CrossBufferization.php │ └── Exception.php ├── Http.php ├── Request.php ├── Response │ ├── Download.php │ └── Response.php └── Runtime.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | * Quality: Happy new year! (Alexis von Glasow, 2017-01-09T21:36:57+01:00) 2 | * Documentation: Use HTPS in `composer.json`. (Ivan Enderlin, 2016-10-07T07:58:33+02:00) 3 | * Documentation: Update support properties. (Alexis von Glasow, 2016-10-06T19:15:43+02:00) 4 | 5 | # 1.16.09.06 6 | 7 | * Add `Response::getStream` that return STDOUT resource (Metalaka, 2016-02-22T22:44:40+01:00) 8 | 9 | # 1.16.01.15 10 | 11 | * Composer: New stable library. (Ivan Enderlin, 2016-01-14T21:56:49+01:00) 12 | 13 | # 1.16.01.14 14 | 15 | * Quality: Drop PHP5.4. (Ivan Enderlin, 2016-01-11T09:15:26+01:00) 16 | * Quality: Run devtools:cs. (Ivan Enderlin, 2016-01-09T09:02:38+01:00) 17 | * Core: Remove `Hoa\Core`. (Ivan Enderlin, 2016-01-09T08:17:16+01:00) 18 | * Consistency: Use `Hoa\Consistency`. (Ivan Enderlin, 2015-12-08T11:15:15+01:00) 19 | * Exception: Use `Hoa\Exception`. (Ivan Enderlin, 2015-11-20T07:51:39+01:00) 20 | * Fix CS. (Ivan Enderlin, 2015-12-13T21:49:37+01:00) 21 | * Fix typos in HTTP status. (Metalaka, 2015-11-01T20:53:13+01:00) 22 | * Fix phpDoc. (Metalaka, 2015-11-01T20:53:13+01:00) 23 | 24 | # 0.15.09.08 25 | 26 | * Add a `.gitignore` file. (Stéphane HULARD, 2015-08-03T11:33:13+02:00) 27 | 28 | # 0.15.05.29 29 | 30 | * Move to PSR-1-2 & drop PHP5.3 & `from`/`import`. (Ivan Enderlin, 2015-05-29T08:54:03+02:00) 31 | * Add the CHANGELOG.md file. (Ivan Enderlin, 2015-02-20T09:28:15+01:00) 32 | 33 | # 0.15.01.06 34 | 35 | * s/_RESUME_INCOMPLETE/_PERMANENT_REDIRECT/ (Ivan Enderlin, 2015-01-05T23:44:27+01:00) 36 | * Happy new year! (Ivan Enderlin, 2015-01-05T14:33:41+01:00) 37 | 38 | # 0.14.12.10 39 | 40 | * Move to PSR-4. (Ivan Enderlin, 2014-12-09T13:50:18+01:00) 41 | 42 | # 0.14.11.15 43 | 44 | * Just order RFC references :-). (Ivan Enderlin, 2014-11-15T11:34:33+01:00) 45 | * Add RFC keywords (jubianchi, 2014-11-14T23:30:06+01:00) 46 | * Add RFC references for all status. (Ivan Enderlin, 2014-11-10T08:23:42+01:00) 47 | * Fix message for 408. (Ivan Enderlin, 2014-11-10T08:13:49+01:00) 48 | * 306 is now unused. (Ivan Enderlin, 2014-11-10T08:12:07+01:00) 49 | * Uncomment `STATUS_IM_USED`. (Ivan Enderlin, 2014-11-10T08:08:26+01:00) 50 | * Status 103 has never been standardized. Remove it. (Ivan Enderlin, 2014-11-04T16:54:59+01:00) 51 | * Fix messages for 308, 412 and 428. (Ivan Enderlin, 2014-11-04T16:46:10+01:00) 52 | * Update case of 418. (Ivan Enderlin, 2014-11-04T16:41:56+01:00) 53 | * Add status. (Ivan Enderlin, 2014-11-04T16:17:34+01:00) 54 | 55 | # 0.14.09.23 56 | 57 | * Add `branch-alias`. (Stéphane PY, 2014-09-23T11:51:01+02:00) 58 | 59 | # 0.14.09.17 60 | 61 | * Drop PHP5.3. (Ivan Enderlin, 2014-09-17T17:18:57+02:00) 62 | 63 | (first snapshot) 64 | -------------------------------------------------------------------------------- /Source/Exception/CrossBufferization.php: -------------------------------------------------------------------------------- 1 | _httpVersion; 90 | $this->_httpVersion = $version; 91 | 92 | return $old; 93 | } 94 | 95 | /** 96 | * Get request HTTP version. 97 | */ 98 | public function getHttpVersion(): float 99 | { 100 | return $this->_httpVersion; 101 | } 102 | 103 | /** 104 | * Parse a HTTP packet. 105 | */ 106 | abstract public function parse(string $packet): void; 107 | 108 | /** 109 | * Helper to parse HTTP headers and distribute them in array accesses. 110 | */ 111 | protected function _parse(array $headers): array 112 | { 113 | unset($this->_headers); 114 | $this->_headers = []; 115 | 116 | foreach ($headers as $header) { 117 | list($name, $value) = explode(':', $header, 2); 118 | $this->_headers[strtolower($name)] = trim($value); 119 | } 120 | 121 | return $this->_headers; 122 | } 123 | 124 | /** 125 | * Get headers. 126 | */ 127 | public function getHeaders(): array 128 | { 129 | return $this->_headers; 130 | } 131 | 132 | /** 133 | * Get headers (formatted). 134 | */ 135 | public function getHeadersFormatted(): array 136 | { 137 | $out = []; 138 | 139 | foreach ($this->getHeaders() as $header => $value) { 140 | if ('x-' == strtolower(substr($header, 0, 2))) { 141 | $header = 'http_' . $header; 142 | } 143 | 144 | $out[strtoupper(str_replace('-', '_', $header))] = $value; 145 | } 146 | 147 | return $out; 148 | } 149 | 150 | /** 151 | * Check if header exists. 152 | */ 153 | public function offsetExists($offset): bool 154 | { 155 | return array_key_exists($offset, $this->_headers); 156 | } 157 | 158 | /** 159 | * Get a header's value. 160 | */ 161 | public function offsetGet($offset): ?string 162 | { 163 | if (false === $this->offsetExists($offset)) { 164 | return null; 165 | } 166 | 167 | return $this->_headers[$offset]; 168 | } 169 | 170 | /** 171 | * Set a value to a header. 172 | */ 173 | public function offsetSet($offset, $value): void 174 | { 175 | $this->_headers[$offset] = $value; 176 | } 177 | 178 | /** 179 | * Unset a header. 180 | */ 181 | public function offsetUnset($offset): void 182 | { 183 | unset($this->_headers[$offset]); 184 | } 185 | 186 | /** 187 | * Get iterator. 188 | */ 189 | public function getIterator(): \ArrayIterator 190 | { 191 | return new \ArrayIterator($this->getHeaders()); 192 | } 193 | 194 | /** 195 | * Count number of headers. 196 | */ 197 | public function count(): int 198 | { 199 | return count($this->getHeaders()); 200 | } 201 | 202 | /** 203 | * Set request body. 204 | */ 205 | public function setBody(?string $body): ?string 206 | { 207 | $old = $this->_body; 208 | $this->_body = $body; 209 | 210 | return $old; 211 | } 212 | 213 | /** 214 | * Get request body. 215 | */ 216 | public function getBody(): ?string 217 | { 218 | return $this->_body; 219 | } 220 | 221 | /** 222 | * Dump (parse^-1). 223 | */ 224 | public function __toString(): string 225 | { 226 | $out = null; 227 | 228 | foreach ($this->getHeaders() as $key => $value) { 229 | $out .= $key . ': ' . $value . CRLF; 230 | } 231 | 232 | return $out; 233 | } 234 | } 235 | 236 | /** 237 | * Flex entity. 238 | */ 239 | Consistency::flexEntity(Http::class); 240 | -------------------------------------------------------------------------------- /Source/Request.php: -------------------------------------------------------------------------------- 1 | setBody(null); 118 | 119 | foreach ($headers as $i => $header) { 120 | if ('' == trim($header)) { 121 | unset($headers[$i]); 122 | $this->setBody( 123 | trim( 124 | implode("\r\n", array_splice($headers, $i)) 125 | ) 126 | ); 127 | 128 | break; 129 | } 130 | } 131 | 132 | if (0 === preg_match('#^([^\s]+)\s+([^\s]+)\s+HTTP/(1\.(?:0|1))$#i', $http, $matches)) { 133 | throw new Exception( 134 | 'HTTP headers are not well-formed: %s', 135 | 0, 136 | $http 137 | ); 138 | } 139 | 140 | switch ($method = strtolower($matches[1])) { 141 | case self::METHOD_CONNECT: 142 | case self::METHOD_DELETE: 143 | case self::METHOD_GET: 144 | case self::METHOD_HEAD: 145 | case self::METHOD_OPTIONS: 146 | case self::METHOD_PATCH: 147 | case self::METHOD_POST: 148 | case self::METHOD_PUT: 149 | case self::METHOD_TRACE: 150 | $this->_method = $method; 151 | 152 | break; 153 | 154 | default: 155 | $this->_method = self::METHOD_EXTENDED; 156 | } 157 | 158 | $this->setUrl($matches[2]); 159 | $this->setHttpVersion((float) $matches[3]); 160 | 161 | $this->_parse($headers); 162 | } 163 | 164 | /** 165 | * Set request method. 166 | */ 167 | public function setMethod(string $method): ?string 168 | { 169 | $old = $this->_method; 170 | $this->_method = $method; 171 | 172 | return $old; 173 | } 174 | 175 | /** 176 | * Get request method. 177 | */ 178 | public function getMethod(): ?string 179 | { 180 | return $this->_method; 181 | } 182 | 183 | /** 184 | * Set request URL. 185 | */ 186 | public function setUrl(string $url): ?string 187 | { 188 | $old = $this->_url; 189 | $this->_url = $url; 190 | 191 | return $old; 192 | } 193 | 194 | /** 195 | * Get request URL. 196 | */ 197 | public function getUrl(): ?string 198 | { 199 | return $this->_url; 200 | } 201 | 202 | /** 203 | * Dump (parse^-1). 204 | */ 205 | public function __toString(): string 206 | { 207 | return 208 | strtoupper($this->getMethod()) . ' ' . 209 | $this->getUrl() . ' ' . 210 | 'HTTP/' . $this->getHttpVersion() . CRLF . 211 | parent::__toString() . CRLF; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Source/Response/Download.php: -------------------------------------------------------------------------------- 1 | _hash = spl_object_hash($this); 369 | 370 | if (true === $newBuffer) { 371 | $this->newBuffer($callable, $size); 372 | } 373 | 374 | if (empty($this->_status)) { 375 | $reflection = new \ReflectionClass($this); 376 | 377 | foreach ($reflection->getConstants() as $value) { 378 | $this->_status[$this->getStatus($value)] = $value; 379 | } 380 | } 381 | 382 | return; 383 | } 384 | 385 | /** 386 | * Parse a HTTP packet. 387 | */ 388 | public function parse(string $packet): void 389 | { 390 | $headers = explode("\r\n", $packet); 391 | $status = array_shift($headers); 392 | $this->setBody(null); 393 | 394 | foreach ($headers as $i => $header) { 395 | if ('' == trim($header)) { 396 | unset($headers[$i]); 397 | $this->setBody( 398 | trim( 399 | implode("\r\n", array_splice($headers, $i)) 400 | ) 401 | ); 402 | 403 | break; 404 | } 405 | } 406 | 407 | if (0 === preg_match('#^HTTP/(1\.(?:0|1))\s+(\d{3})#i', $status, $matches)) { 408 | throw new Http\Exception( 409 | 'HTTP status is not well-formed: %s.', 410 | 0, 411 | $status 412 | ); 413 | } 414 | 415 | if (!isset($this->_status[$matches[2]])) { 416 | throw new Http\Exception( 417 | 'Unknown HTTP status %d in %s.', 418 | 1, 419 | [$matches[2], $status] 420 | ); 421 | } 422 | 423 | $this->setHttpVersion((float) $matches[1]); 424 | $this->_parse($headers); 425 | $this['status'] = $this->_status[$matches[2]]; 426 | } 427 | 428 | /** 429 | * Get real status from static::STATUS_* constants. 430 | */ 431 | public static function getStatus(string $status): int 432 | { 433 | return (int) substr($status, 0, 3); 434 | } 435 | 436 | /** 437 | * Send a new status. 438 | */ 439 | public function sendStatus(string $status, bool $replace = true): void 440 | { 441 | $this->sendHeader('status', $status, $replace, $status); 442 | } 443 | 444 | /** 445 | * Send a new header. 446 | * 447 | * @param string $header Header. 448 | * @param string $value Value. 449 | * @param bool $replace Whether replace an existing sent header. 450 | * @param string $status Force a specific status. Please, see 451 | * static::STATUS_* constants. 452 | * @return void 453 | */ 454 | public function sendHeader( 455 | string $header, 456 | string $value, 457 | bool $replace = true, 458 | string $status = null 459 | ): void { 460 | if (0 === strcasecmp('status', $header) && 461 | false === self::$_fcgi) { 462 | header( 463 | 'HTTP/1.1 ' . $value, 464 | $replace, 465 | static::getStatus($value) 466 | ); 467 | } else { 468 | header( 469 | $header . ': ' . $value, 470 | $replace, 471 | null !== $status ? static::getStatus($status) : null 472 | ); 473 | } 474 | } 475 | 476 | /** 477 | * Send all headers. 478 | */ 479 | public function sendHeaders() 480 | { 481 | foreach ($this->_headers as $header => $value) { 482 | $this->sendHeader($header, $value); 483 | } 484 | } 485 | 486 | /** 487 | * Get send headers. 488 | */ 489 | public function getSentHeaders(): string 490 | { 491 | return implode("\r\n", headers_list()); 492 | } 493 | 494 | /** 495 | * Start a new buffer. 496 | * The callable acts like a filter. 497 | */ 498 | public function newBuffer(callable $callable = null, int $size = null): int 499 | { 500 | $last = current(self::$_stack); 501 | $hash = $this->getHash(); 502 | 503 | if (false === $last || $hash != $last[0]) { 504 | self::$_stack[] = [ 505 | 0 => $hash, 506 | 1 => 1 507 | ]; 508 | } else { 509 | ++self::$_stack[key(self::$_stack)][1]; 510 | } 511 | 512 | end(self::$_stack); 513 | 514 | if (null === $callable) { 515 | ob_start(); 516 | } else { 517 | ob_start(xcallable($callable), null === $size ? 0 : $size); 518 | } 519 | 520 | return $this->getBufferLevel(); 521 | } 522 | 523 | /** 524 | * Flush the buffer. 525 | */ 526 | public function flush(bool $force = false) 527 | { 528 | if (0 >= $this->getBufferSize()) { 529 | return; 530 | } 531 | 532 | ob_flush(); 533 | 534 | if (true === $force) { 535 | flush(); 536 | } 537 | 538 | return; 539 | } 540 | 541 | /** 542 | * Delete buffer. 543 | */ 544 | public function deleteBuffer(): bool 545 | { 546 | $key = key(self::$_stack); 547 | 548 | if ($this->getHash() != self::$_stack[$key][0]) { 549 | throw new Http\Exception\CrossBufferization( 550 | 'Cannot delete this buffer because it was not opened by this ' . 551 | 'class (%s, %s).', 552 | 2, 553 | [get_class($this), $this->getHash()] 554 | ); 555 | } 556 | 557 | $out = ob_end_clean(); 558 | 559 | if (false === $out) { 560 | return false; 561 | } 562 | 563 | --self::$_stack[$key][1]; 564 | 565 | if (0 >= self::$_stack[$key][1]) { 566 | unset(self::$_stack[$key]); 567 | } 568 | 569 | return true; 570 | } 571 | 572 | /** 573 | * Get buffer level. 574 | */ 575 | public function getBufferLevel(): int 576 | { 577 | return ob_get_level(); 578 | } 579 | 580 | /** 581 | * Get buffer size. 582 | */ 583 | public function getBufferSize(): int 584 | { 585 | return ob_get_length(); 586 | } 587 | 588 | /** 589 | * Write n characters. 590 | */ 591 | public function write(string $string, int $length) 592 | { 593 | if (0 > $length) { 594 | throw new Http\Exception( 595 | 'Length must be greater than 0, given %d.', 596 | 3, 597 | $length 598 | ); 599 | } 600 | 601 | if (strlen($string) > $length) { 602 | $string = substr($string, 0, $length); 603 | } 604 | 605 | echo $string; 606 | 607 | return; 608 | } 609 | 610 | /** 611 | * Write a string. 612 | */ 613 | public function writeString(string $string) 614 | { 615 | echo $string; 616 | 617 | return; 618 | } 619 | 620 | /** 621 | * Write a character. 622 | */ 623 | public function writeCharacter(string $character) 624 | { 625 | echo $character[0]; 626 | 627 | return; 628 | } 629 | 630 | /** 631 | * Write a boolean. 632 | */ 633 | public function writeBoolean(bool $boolean) 634 | { 635 | echo (string) $boolean; 636 | 637 | return; 638 | } 639 | 640 | /** 641 | * Write an integer. 642 | * 643 | * @param int $integer Integer. 644 | * @return mixed 645 | */ 646 | public function writeInteger(int $integer) 647 | { 648 | echo (string) $integer; 649 | 650 | return; 651 | } 652 | 653 | /** 654 | * Write a float. 655 | * 656 | * @param float $float Float. 657 | * @return mixed 658 | */ 659 | public function writeFloat(float $float) 660 | { 661 | echo (string) $float; 662 | 663 | return; 664 | } 665 | 666 | /** 667 | * Write an array. 668 | */ 669 | public function writeArray(array $array) 670 | { 671 | echo var_export($array, true); 672 | 673 | return; 674 | } 675 | 676 | /** 677 | * Write a line. 678 | * 679 | * @param string $line Line. 680 | * @return mixed 681 | */ 682 | public function writeLine(string $line) 683 | { 684 | if (false !== $n = strpos($line, "\n")) { 685 | $line = substr($line, 0, $n + 1); 686 | } 687 | 688 | echo $line; 689 | 690 | return; 691 | } 692 | 693 | /** 694 | * Write all, i.e. as much as possible. 695 | * 696 | * @param string $string String. 697 | * @return mixed 698 | */ 699 | public function writeAll(string $string) 700 | { 701 | echo $string; 702 | 703 | return; 704 | } 705 | 706 | /** 707 | * Truncate a file to a given length. 708 | * 709 | * @param int $size Size. 710 | * @return bool 711 | */ 712 | public function truncate(int $size): bool 713 | { 714 | if (0 === $size) { 715 | ob_clean(); 716 | 717 | return true; 718 | } 719 | 720 | $bSize = $this->getBufferSize(); 721 | 722 | if ($size >= $bSize) { 723 | return true; 724 | } 725 | 726 | echo substr(ob_get_clean(), 0, $size); 727 | 728 | return true; 729 | } 730 | 731 | /** 732 | * Get the current stream. 733 | */ 734 | public function getStream() 735 | { 736 | return fopen('php://stdout', 'w'); 737 | } 738 | 739 | /** 740 | * Get this object hash. 741 | */ 742 | public function getHash(): ?string 743 | { 744 | return $this->_hash; 745 | } 746 | 747 | /** 748 | * Delete head buffer. 749 | */ 750 | public function __destruct() 751 | { 752 | $last = current(self::$_stack); 753 | 754 | if ($this->getHash() != $last[0]) { 755 | return; 756 | } 757 | 758 | for ($i = 0, $max = $last[1]; $i < $max; ++$i) { 759 | $this->flush(); 760 | 761 | if (0 < $this->getBufferLevel()) { 762 | $this->deleteBuffer(); 763 | } 764 | } 765 | 766 | return; 767 | } 768 | } 769 | 770 | /** 771 | * Flex entity. 772 | */ 773 | Consistency::flexEntity(Response::class); 774 | -------------------------------------------------------------------------------- /Source/Runtime.php: -------------------------------------------------------------------------------- 1 | $value) { 144 | $_headers[strtolower($header)] = $value; 145 | } 146 | } else { 147 | if (isset($_SERVER['CONTENT_TYPE'])) { 148 | $_headers['content-type'] = $_SERVER['CONTENT_TYPE']; 149 | } 150 | 151 | if (isset($_SERVER['CONTENT_LENGTH'])) { 152 | $_headers['content-length'] = $_SERVER['CONTENT_LENGTH']; 153 | } 154 | 155 | foreach ($_SERVER as $key => $value) { 156 | if ('HTTP_' === substr($key, 0, 5)) { 157 | $_headers[strtolower(str_replace('_', '-', substr($key, 5)))] 158 | = $value; 159 | } 160 | } 161 | } 162 | 163 | return $_headers; 164 | } 165 | 166 | /** 167 | * Get a specific header. 168 | */ 169 | public static function getHeader(string $header): ?string 170 | { 171 | $headers = static::getHeaders(); 172 | $header = strtolower($header); 173 | 174 | if (true !== array_key_exists($header, $headers)) { 175 | return null; 176 | } 177 | 178 | return $headers[$header]; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "hoa/http", 3 | "description": "The Hoa\\Http library.", 4 | "type" : "library", 5 | "keywords" : ["library", "http", "request", "response", "runtime", 6 | "rfc2295", "rfc2324", "rfc2518", "rfc2774", "rfc3229", 7 | "rfc4918", "rfc5842", "rfc6585", "rfc7231", "rfc7232", 8 | "rfc7233", "rfc7235", "rfc7237"], 9 | "homepage" : "https://hoa-project.net/", 10 | "license" : "BSD-3-Clause", 11 | "authors" : [ 12 | { 13 | "name" : "Ivan Enderlin", 14 | "email": "ivan.enderlin@hoa-project.net" 15 | }, 16 | { 17 | "name" : "Hoa community", 18 | "homepage": "https://hoa-project.net/" 19 | } 20 | ], 21 | "support": { 22 | "email" : "support@hoa-project.net", 23 | "irc" : "irc://chat.freenode.net/hoaproject", 24 | "forum" : "https://users.hoa-project.net/", 25 | "docs" : "https://central.hoa-project.net/Documentation/Library/Http", 26 | "source": "https://central.hoa-project.net/Resource/Library/Http" 27 | }, 28 | "require": { 29 | "php" : ">=7.1", 30 | "hoa/consistency": "dev-master", 31 | "hoa/exception" : "dev-master", 32 | "hoa/stream" : "dev-master" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Hoa\\Http\\": "Source" 37 | } 38 | }, 39 | "extra": { 40 | "branch-alias": { 41 | "dev-master": "1.x-dev" 42 | } 43 | } 44 | } 45 | --------------------------------------------------------------------------------