diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -25,6 +25,8 @@ 'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php', 'AphrontDeadlockQueryException' => 'aphront/storage/exception/AphrontDeadlockQueryException.php', 'AphrontDuplicateKeyQueryException' => 'aphront/storage/exception/AphrontDuplicateKeyQueryException.php', + 'AphrontHTTPHeaderParser' => 'aphront/headerparser/AphrontHTTPHeaderParser.php', + 'AphrontHTTPHeaderParserTestCase' => 'aphront/headerparser/__tests__/AphrontHTTPHeaderParserTestCase.php', 'AphrontInvalidCredentialsQueryException' => 'aphront/storage/exception/AphrontInvalidCredentialsQueryException.php', 'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php', 'AphrontLockTimeoutQueryException' => 'aphront/storage/exception/AphrontLockTimeoutQueryException.php', @@ -633,6 +635,8 @@ 'AphrontDatabaseTransactionState' => 'Phobject', 'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException', 'AphrontDuplicateKeyQueryException' => 'AphrontQueryException', + 'AphrontHTTPHeaderParser' => 'Phobject', + 'AphrontHTTPHeaderParserTestCase' => 'PhutilTestCase', 'AphrontInvalidCredentialsQueryException' => 'AphrontQueryException', 'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException', diff --git a/src/aphront/headerparser/AphrontHTTPHeaderParser.php b/src/aphront/headerparser/AphrontHTTPHeaderParser.php new file mode 100644 --- /dev/null +++ b/src/aphront/headerparser/AphrontHTTPHeaderParser.php @@ -0,0 +1,150 @@ +name = null; + $this->content = null; + + $parts = explode(':', $raw_header, 2); + $this->name = trim($parts[0]); + if (count($parts) > 1) { + $this->content = trim($parts[1]); + } + + $this->pairs = null; + + return $this; + } + + public function getHeaderName() { + $this->requireParse(); + return $this->name; + } + + public function getHeaderContent() { + $this->requireParse(); + return $this->content; + } + + public function getHeaderContentAsPairs() { + $content = $this->getHeaderContent(); + + + $state = 'prekey'; + $length = strlen($content); + + $pair_name = null; + $pair_value = null; + + $pairs = array(); + $ii = 0; + while ($ii < $length) { + $c = $content[$ii]; + + switch ($state) { + case 'prekey'; + // We're eating space in front of a key. + if ($c == ' ') { + $ii++; + break; + } + $pair_name = ''; + $state = 'key'; + break; + case 'key'; + // We're parsing a key name until we find "=" or ";". + if ($c == ';') { + $state = 'done'; + break; + } + + if ($c == '=') { + $ii++; + $state = 'value'; + break; + } + + $ii++; + $pair_name .= $c; + break; + case 'value': + // We found an "=", so now figure out if the value is quoted + // or not. + if ($c == '"') { + $ii++; + $state = 'quoted'; + break; + } + $state = 'unquoted'; + break; + case 'quoted': + // We're in a quoted string, parse until we find the closing quote. + if ($c == '"') { + $ii++; + $state = 'done'; + break; + } + + $ii++; + $pair_value .= $c; + break; + case 'unquoted': + // We're in an unquoted string, parse until we find a space or a + // semicolon. + if ($c == ' ' || $c == ';') { + $state = 'done'; + break; + } + $ii++; + $pair_value .= $c; + break; + case 'done': + // We parsed something, so eat any trailing whitespace and semicolons + // and look for a new value. + if ($c == ' ' || $c == ';') { + $ii++; + break; + } + + $pairs[] = array( + $pair_name, + $pair_value, + ); + + $pair_name = null; + $pair_value = null; + + $state = 'prekey'; + break; + } + } + + if ($state == 'quoted') { + throw new Exception( + pht( + 'Header has unterminated double quote for key "%s".', + $pair_name)); + } + + if ($pair_name !== null) { + $pairs[] = array( + $pair_name, + $pair_value, + ); + } + + return $pairs; + } + + private function requireParse() { + if ($this->name === null) { + throw new PhutilInvalidStateException('parseRawHeader'); + } + } + +} diff --git a/src/aphront/headerparser/__tests__/AphrontHTTPHeaderParserTestCase.php b/src/aphront/headerparser/__tests__/AphrontHTTPHeaderParserTestCase.php new file mode 100644 --- /dev/null +++ b/src/aphront/headerparser/__tests__/AphrontHTTPHeaderParserTestCase.php @@ -0,0 +1,108 @@ +parseRawHeader($input); + + $actual_name = $parser->getHeaderName(); + $actual_content = $parser->getHeaderContent(); + + $this->assertEqual( + $expect_name, + $actual_name, + pht('Header name for: %s', $input)); + + $this->assertEqual( + $expect_content, + $actual_content, + pht('Header content for: %s', $input)); + + if (isset($case[3])) { + $expect_pairs = $case[3]; + + $caught = null; + try { + $actual_pairs = $parser->getHeaderContentAsPairs(); + } catch (Exception $ex) { + $caught = $ex; + } + + if ($expect_pairs === false) { + $this->assertEqual( + true, + ($caught instanceof Exception), + pht('Expect exception for header pairs of: %s', $input)); + } else { + $this->assertEqual( + $expect_pairs, + $actual_pairs, + pht('Header pairs for: %s', $input)); + } + } + } + } + + +}