From e6c534f566f843b7b406dc35cb17e6abe751af1a Mon Sep 17 00:00:00 2001 From: Kevin Day Date: Tue, 7 Mar 2017 23:48:31 -0600 Subject: [PATCH] Progress: continuing development, http headers work More progress with writing HTTP header processing code. Other minor bugfixes and changes. --- common/base/classes/base_http.php | 1142 ++++++++++++++++++----------- common/base/classes/base_rfc_char.php | 115 ++- common/base/classes/base_rfc_string.php | 1216 +++++++++++++++++++++++++++++-- examples/test.php | 6 +- program/reservation/index.php | 3 +- 5 files changed, 2001 insertions(+), 481 deletions(-) diff --git a/common/base/classes/base_http.php b/common/base/classes/base_http.php index 9e7ff1b..1b87310 100644 --- a/common/base/classes/base_http.php +++ b/common/base/classes/base_http.php @@ -19,11 +19,6 @@ require_once('common/base/classes/base_mime.php'); /** * A generic class for managing the HTTP protocol. * - * @todo: many of the HTTP response fields can become HTML meta header fields. - * add a function to return an array of HTTP attributes that can be translated into HTTP tags. - * no processing is to be done here, let a class that is explicitly designed for HTML process it. - * example, content-security-policy (https://en.wikipedia.org/wiki/Content_Security_Policy). - * * @see: https://www.iana.org/assignments/message-headers/message-headers.xhtml * @see: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields * @@ -207,7 +202,7 @@ class c_base_http extends c_base_rfc_string { const ENCODING_GZIP = 4; // Compression Options: -1 -> 9. const ENCODING_BZIP = 5; // Compression Options: 1 -> 9. const ENCODING_LZO = 6; // Compression Options: LZO1_99, LZO1A_99, LZO1B_999, LZO1C_999, LZO1F_999, LZO1X_999, LZO1Y_999, LZO1Z_999, LZO2A_999 (and many more). - const ENCODING_XZ = 7; + const ENCODING_XZ = 7; // currently unsupported due to available libraries being defunct. const ENCODING_EXI = 8; const ENCODING_IDENTITY = 9; const ENCODING_SDCH = 10; @@ -308,6 +303,32 @@ class c_base_http extends c_base_rfc_string { } /** + * Returns a list of HTTP headers that can be used as an HTML meta tag. + * + * The relationship between HTTP headers and HTML headers is not always one to one. + * These should be used + * @todo: this list will need to be reviewed once I work on the HTML meta handling code. + * + * The HTML language supports HTTP headers as HTML tags. + * + * @see: https://html.spec.whatwg.org/multipage/semantics.html#standard-metadata-names + * @see: https://www.w3.org/TR/html5/document-metadata.html#the-meta-element + */ + public function get_response_headers_for_meta() { + return array( + self::RESPONSE_CACHE_CONTROL => self::RESPONSE_CACHE_CONTROL, + self::RESPONSE_CONTENT_ENCODING => self::RESPONSE_CONTENT_ENCODING, + self::RESPONSE_CONTENT_LANGUAGE => self::RESPONSE_CONTENT_LANGUAGE, + self::RESPONSE_CONTENT_SECURITY_POLICY => self::RESPONSE_CONTENT_SECURITY_POLICY, + self::RESPONSE_CONTENT_TYPE => self::RESPONSE_CONTENT_TYPE, + self::RESPONSE_EXPIRES => self::RESPONSE_EXPIRES, + self::RESPONSE_LINK => self::RESPONSE_LINK, + self::RESPONSE_PRAGMA => self::RESPONSE_PRAGMA, + self::RESPONSE_REFRESH => self::RESPONSE_REFRESH, + ); + } + + /** * Get the HTTP request array. * * Load the entire HTTP request array or a specific request field. @@ -570,21 +591,18 @@ class c_base_http extends c_base_rfc_string { unset($headers['accept_datetime']); } - // @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing if (array_key_exists('access_control_request_method', $this->headers)) { - $this->p_load_request_rawish('access_control_request_method', $this->REQUEST_ACCESS_CONTROL_REQUEST_METHOD, 256); + $this->p_load_request_access_control_request_method(); unset($headers['access_control_request_method']); } - // @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing if (array_key_exists('access_control_request_headers', $this->headers)) { - $this->p_load_request_rawish('access_request_allow_headers', $this->REQUEST_ACCESS_CONTROL_REQUEST_HEADERS, 256); + $this->p_load_request_access_control_request_headers(); unset($headers['access_control_request_headers']); } - // @see: https://tools.ietf.org/html/rfc7235#section-4.2 if (array_key_exists('authorization', $this->headers)) { - $this->p_load_request_rawish('authorization', $this->REQUEST_AUTHORIZATION, 4096); + $this->p_load_request_authorization(); unset($headers['authorization']); } @@ -688,9 +706,8 @@ class c_base_http extends c_base_rfc_string { unset($headers['origin']); } - // @see: https://tools.ietf.org/html/rfc7235#section-4.4 if (array_key_exists('proxy_authorization', $this->headers)) { - $this->p_load_request_rawish('proxy_authorization', $this->REQUEST_PROXY_AUTHORIZATION, 4096); + $this->p_load_request_proxy_authorization(); unset($headers['proxy_authorization']); } @@ -725,22 +742,22 @@ class c_base_http extends c_base_rfc_string { } if (array_key_exists('x_requested_with', $this->headers)) { - $this->p_load_request_rawish('x_requested_with', $this->REQUEST_X_REQUESTED_WITH, 64); + $this->p_load_request_x_requested_with(); unset($headers['x_requested_with']); } if (array_key_exists('x_forwarded_for', $this->headers)) { - $this->p_load_request_rawish('x_forwarded_for', $this->REQUEST_X_FORWARDED_for, 512); + $this->p_load_request_x_requested_for(); unset($headers['x_forwarded_for']); } if (array_key_exists('x_forwarded_host', $this->headers)) { - $this->p_load_request_rawish('x_forwarded_host', $this->REQUEST_X_FORWARDED_HOST, 512); + $this->p_load_request_x_requested_host(); unset($headers['x_forwarded_host']); } if (array_key_exists('x_forwarded_proto', $this->headers)) { - $this->p_load_request_rawish('x_forwarded_proto', $this->REQUEST_X_FORWARDED_PROTO, 64); + $this->p_load_request_x_requested_proto(); unset($headers['x_forwarded_proto']); } @@ -776,8 +793,9 @@ class c_base_http extends c_base_rfc_string { /** * Assign HTTP response header: access-control-allow-origin. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. + * Note on multiple urls: The standard appears to only support one url. + * Therefore, to have multiple urls, the clients ORIGIN should be checked against a known list. + * Then, from that list either the default or the clients ORIGIN is sent. * * @param string $uri * The uri to assign to the specified header. @@ -788,6 +806,8 @@ class c_base_http extends c_base_rfc_string { * FALSE with error bit set is returned on error. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Origin */ public function set_response_access_control_allow_origin($uri) { if (!is_string($uri)) { @@ -800,13 +820,24 @@ class c_base_http extends c_base_rfc_string { $this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_ORIGIN] = array('wildcard' => TRUE); } else { - $parsed = $this->p_parse_uri($uri); + $text = $this->pr_rfc_string_prepare($uri); + if ($text['invalid']) { + unset($text); + + $error = c_base_error::s_log(NULL, array('arguments' => array(':operation_name' => 'this->pr_rfc_string_prepare', ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::OPERATION_FAILURE); + return c_base_return_error::s_false($error); + } + + $parsed = $this->pr_rfc_string_is_uri($text['ordinals'], $text['characters']); + unset($text); + if ($parsed['invalid']) { unset($parsed); $error = c_base_error::s_log(NULL, array('arguments' => array(':format_name' => 'uri', ':expected_format' => NULL, ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::INVALID_FORMAT); return c_base_return_error::s_false($error); } unset($parsed['invalid']); + unset($parsed['current']); $parsed['wildcard'] = FALSE; $this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_ORIGIN] = $parsed; @@ -819,9 +850,6 @@ class c_base_http extends c_base_rfc_string { /** * Assign HTTP response header: access-control-allow-credentials. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * * @param bool $allow_credentials * The value to assign to the specified header. * @@ -830,6 +858,8 @@ class c_base_http extends c_base_rfc_string { * FALSE with error bit set is returned on error. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Credentials */ public function set_response_access_control_allow_credentials($allow_credentials) { if (!is_bool($allow_credentials)) { @@ -844,10 +874,6 @@ class c_base_http extends c_base_rfc_string { /** * Assign HTTP response header: access-control-expose-headers. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * @todo: should this be written in the same way as set_response_allow()? - * * @param string $header_name * The header name to assign to the specified header. * @param bool $append @@ -859,6 +885,8 @@ class c_base_http extends c_base_rfc_string { * FALSE with error bit set is returned on error. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Expose-Headers */ public function set_response_access_control_expose_headers($header_name, $append = TRUE) { if (!is_string($header_name)) { @@ -898,34 +926,30 @@ class c_base_http extends c_base_rfc_string { /** * Assign HTTP response header: access-control-max-age. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * - * @param int|float $timestamp - * The timestamp to assign to the specified header. + * @param int|float $seconds + * The seconds to assign to the specified header. * * @return c_base_return_status * TRUE on success, FALSE otherwise. * FALSE with error bit set is returned on error. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Max-Age */ - public function set_response_access_control_max_age($timestamp) { - if (!is_int($timestamp) && !is_float($timestamp)) { - $error = c_base_error::s_log(NULL, array('arguments' => array(':argument_name' => 'timestamp', ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::INVALID_ARGUMENT); + public function set_response_access_control_max_age($seconds) { + if (!is_int($seconds) && !is_float($seconds)) { + $error = c_base_error::s_log(NULL, array('arguments' => array(':argument_name' => 'seconds', ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::INVALID_ARGUMENT); return c_base_return_error::s_false($error); } - $this->response[self::RESPONSE_ACCESS_CONTROL_MAX_AGE] = $timestamp; + $this->response[self::RESPONSE_ACCESS_CONTROL_MAX_AGE] = $seconds; return new c_base_return_true(); } /** * Assign HTTP response header: access-control-allow-methods. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * * @param int $method * The code representing the method to assign to the specified header. * @param bool $append @@ -937,6 +961,8 @@ class c_base_http extends c_base_rfc_string { * FALSE with error bit set is returned on error. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Methods */ public function set_response_access_control_allow_methods($method, $append = TRUE) { if (!is_int($method)) { @@ -950,6 +976,34 @@ class c_base_http extends c_base_rfc_string { } + // this method does nothing. + if ($method === self::HTTP_METHOD_NONE) { + return new c_base_return_true(); + } + + + // require only valid/known methods. + switch ($method) { + case self::HTTP_METHOD_NONE: + case self::HTTP_METHOD_GET: + case self::HTTP_METHOD_HEAD: + case self::HTTP_METHOD_POST: + case self::HTTP_METHOD_PUT: + case self::HTTP_METHOD_DELETE: + case self::HTTP_METHOD_TRACE: + case self::HTTP_METHOD_OPTIONS: + case self::HTTP_METHOD_CONNECT: + case self::HTTP_METHOD_PATCH: + case self::HTTP_METHOD_TRACK: + case self::HTTP_METHOD_DEBUG: + break; + + default: + $error = c_base_error::s_log(NULL, array('arguments' => array(':argument_name' => 'method', ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::INVALID_ARGUMENT); + return c_base_return_error::s_false($error); + } + + if ($append) { if (!isset($this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS]) || !is_array($this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS])) { $this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] = array(); @@ -967,9 +1021,6 @@ class c_base_http extends c_base_rfc_string { /** * Assign HTTP response header: access-control-allow-headers. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * * @param string $header_name * The header name to assign to the specified header. * @param bool $append @@ -977,6 +1028,8 @@ class c_base_http extends c_base_rfc_string { * If FALSE, then assign the header name. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Headers */ public function set_response_access_control_allow_headers($header_name, $append = TRUE) { if (!is_string($header_name)) { @@ -2148,10 +2201,10 @@ class c_base_http extends c_base_rfc_string { * The standard defines this as: * - (uri) *(";" 1*(tchar) "=" 1*(1*(tchar) / 1*(quoted-string))) * - * @param string|null $uri + * This standard likely supports multiple link headers. + * + * @param string $uri * The URI to assign to the specified header. - * May be NULL when append is TRUE. - * Both $null and $parameter_name may not be NULL. * @param string|null $parameter_name * (optional) A single link parameter to be added. * If NULL, then this is ignored. @@ -2160,8 +2213,9 @@ class c_base_http extends c_base_rfc_string { * (optional) A single link value to be added. * If NULL, then this is ignored. * @param bool $append - * (optional) If TRUE, then append the header name. - * If FALSE, then assign the header name. + * (optional) If TRUE, then append the parameter name. + * If FALSE, then assign the parameter name. + * When $parameter_name is NULL and this is FALSE, then assign an empty array for parameters associated with $uri. * * @return c_base_return_status * TRUE on success, FALSE otherwise. @@ -2202,7 +2256,17 @@ class c_base_http extends c_base_rfc_string { } - $parsed_uri = $this->p_parse_uri($uri); + $text = $this->pr_rfc_string_prepare($uri); + if ($text['invalid']) { + unset($text); + + $error = c_base_error::s_log(NULL, array('arguments' => array(':operation_name' => 'this->pr_rfc_string_prepare', ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::OPERATION_FAILURE); + return c_base_return_error::s_false($error); + } + + $parsed = $this->pr_rfc_string_is_uri($text['ordinals'], $text['characters']); + unset($text); + if ($parsed_uri['invalid']) { unset($parsed_uri); @@ -2212,6 +2276,22 @@ class c_base_http extends c_base_rfc_string { unset($parsed_uri['invalid']); + // when append is FALSE and there is no parameter name, then assign url instead of appending url. + if (!$append && is_null($parameter_name)) { + if (!isset($this->response[self::RESPONSE_LINK])) { + $this->response[self::RESPONSE_LINK] = array(); + } + + $this->response[self::RESPONSE_LINK][$uri] = array( + 'uri' => $parsed_uri, + 'parameters' => array(), + ); + unset($parsed_uri); + + return new c_base_return_true(); + } + + $prepared_parameter_name = NULL; if (is_string($parameter_name)) { $prepared_parameter_name = $this->p_prepare_token($parameter_name); @@ -2260,31 +2340,23 @@ class c_base_http extends c_base_rfc_string { if (!isset($this->response[self::RESPONSE_LINK])) { - // uri cannot be NULL if there is no uri currently assigned. - if (is_null($uri)) { - unset($prepared_token); - - $error = c_base_error::s_log(NULL, array('arguments' => array(':argument_name' => 'uri', ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::INVALID_ARGUMENT); - return c_base_return_error::s_false($error); - } - - $this->response[self::RESPONSE_LINK] = array( - 'uri' => NULL, - 'parameters' => array(), - ); + $this->response[self::RESPONSE_LINK] = array(); } - if (is_string($uri)) { - $this->response[self::RESPONSE_LINK]['uri'] = $parsed_uri; + if (!array_key_exists($uri, $this->response[self::RESPONSE_LINK])) { + $this->response[self::RESPONSE_LINK][$uri] = array( + 'uri' => $parsed_uri, + 'parameters' => array() + ); } unset($parsed_uri); if (is_string($parameter_name)) { if ($append) { - $this->response[self::RESPONSE_LINK]['parameters'][$prepared_parameter_name] = $parsed_parameter_value['text']; + $this->response[self::RESPONSE_LINK][$uri]['parameters'][$prepared_parameter_name] = $parsed_parameter_value['text']; } else { - $this->response[self::RESPONSE_LINK]['parameters'] = array($prepared_parameter_name => $parsed_parameter_value['text']); + $this->response[self::RESPONSE_LINK][$uri]['parameters'] = array($prepared_parameter_name => $parsed_parameter_value['text']); } } unset($prepared_parameter_name); @@ -2313,7 +2385,18 @@ class c_base_http extends c_base_rfc_string { return c_base_return_error::s_false($error); } - $parsed = $this->p_parse_uri($uri); + + $text = $this->pr_rfc_string_prepare($uri); + if ($text['invalid']) { + unset($text); + + $error = c_base_error::s_log(NULL, array('arguments' => array(':operation_name' => 'this->pr_rfc_string_prepare', ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::OPERATION_FAILURE); + return c_base_return_error::s_false($error); + } + + $parsed = $this->pr_rfc_string_is_uri($text['ordinals'], $text['characters']); + unset($text); + if ($parsed['invalid']) { unset($parsed); $error = c_base_error::s_log(NULL, array('arguments' => array(':format_name' => 'uri', ':expected_format' => NULL, ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::INVALID_FORMAT); @@ -2675,9 +2758,7 @@ class c_base_http extends c_base_rfc_string { unset($this->response[self::RESPONSE_CONTENT_LENGTH]); } - // RESPONSE_TRANSFER_ENCODING - - // @todo + // @todo: self::RESPONSE_TRANSFER_ENCODING $error = c_base_error::s_log(NULL, array('arguments' => array(':functionality_name' => 'http response transfer encoding', ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::NO_SUPPORT); return c_base_return_error::s_false($error); @@ -2727,7 +2808,7 @@ class c_base_http extends c_base_rfc_string { } - $prepared_token = $this->p_prepare_token($header_name); + $prepared_token = $this->p_prepare_token($header_name, FALSE); if ($prepared_token === FALSE) { unset($prepared_token); $error = c_base_error::s_log(NULL, array('arguments' => array(':operation_name' => 'this->p_prepare_token', ':function_name' => __CLASS__ . '->' . __FUNCTION__)), i_base_error_messages::OPERATION_FAILURE); @@ -3357,15 +3438,14 @@ class c_base_http extends c_base_rfc_string { /** * Obtain HTTP response header: access-control-allow-origin. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * * @return c_base_return_array|c_base_return_status * A decoded uri split into its different parts inside an array. * This array also contains a key called 'wildcard' which may be either TRUE or FALSE. * FALSE with error bit set is returned on error, including when the key is not defined. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Origin */ public function get_response_access_control_allow_origin() { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_ALLOW_ORIGIN, $this->response)) { @@ -3379,14 +3459,14 @@ class c_base_http extends c_base_rfc_string { /** * Obtain HTTP response header: access-control-allow-credentials. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * * @return c_base_return_bool|c_base_return_status * A boolean representing whether or not to allow credentials. * FALSE with error bit set is returned on error, including when the key is not defined. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Credentials + * */ public function get_response_access_control_allow_credentials() { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_ALLOW_CREDENTIALS, $this->response)) { @@ -3400,14 +3480,13 @@ class c_base_http extends c_base_rfc_string { /** * Obtain HTTP response header: access-control-expose-headers. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * * @return c_base_return_array|c_base_return_status * An array of headers to expose. * FALSE with error bit set is returned on error, including when the key is not defined. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Expose-Headers */ public function get_response_access_control_expose_headers() { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_EXPOSE_HEADERS, $this->response)) { @@ -3421,14 +3500,13 @@ class c_base_http extends c_base_rfc_string { /** * Obtain HTTP response header: access-control-max-age. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * * @return c_base_return_int|c_base_return_status * An Unix timestamp representing the specified header. * FALSE with error bit set is returned on error, including when the key is not defined. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Max-Age */ public function get_response_access_control_max_age() { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_MAX_AGE, $this->response)) { @@ -3442,13 +3520,12 @@ class c_base_http extends c_base_rfc_string { /** * Obtain HTTP response header: access-control-allow-methods. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * * @return c_base_return_array|c_base_return_status * FALSE with error bit set is returned on error, including when the key is not defined. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Methods */ public function get_response_access_control_allow_methods() { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS, $this->response)) { @@ -3462,12 +3539,13 @@ class c_base_http extends c_base_rfc_string { /** * Obtain HTTP response header: access-control-allow-headers. * - * @todo: I only glanced at the documentation (which was incomplete on wikipedia). - * More work is likely needed to be done with this and its structure is subject to change. - * * @return c_base_return_arrayl|c_base_return_status * An array of allowed headers is returned. * FALSE with error bit set is returned on error, including when the key is not defined. + * + * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Headers */ public function get_response_access_control_allow_headers() { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_ALLOW_HEADERS, $this->response)) { @@ -4447,7 +4525,7 @@ class c_base_http extends c_base_rfc_string { } $this->p_prepare_header_response_access_control_allow_origin($header_id_to_names[self::RESPONSE_ACCESS_CONTROL_ALLOW_ORIGIN], $header_output); - $this->p_prepare_header_response_simple_value($header_id_to_names[self::RESPONSE_ACCESS_CONTROL_ALLOW_CREDENTIALS], $header_output, self::RESPONSE_ACCESS_CONTROL_ALLOW_CREDENTIALS); + $this->p_prepare_header_response_boolean_value($header_id_to_names[self::RESPONSE_ACCESS_CONTROL_ALLOW_CREDENTIALS], $header_output, self::RESPONSE_ACCESS_CONTROL_ALLOW_CREDENTIALS); $this->p_prepare_header_response_access_control_expose_headers($header_id_to_names[self::RESPONSE_ACCESS_CONTROL_EXPOSE_HEADERS], $header_output); $this->p_prepare_header_response_simple_value($header_id_to_names[self::RESPONSE_ACCESS_CONTROL_MAX_AGE], $header_output, self::RESPONSE_ACCESS_CONTROL_MAX_AGE); $this->p_prepare_header_response_access_control_allow_methods($header_id_to_names[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS], $header_output); @@ -4804,7 +4882,7 @@ class c_base_http extends c_base_rfc_string { krsort($this->request[self::REQUEST_ACCEPT]['data']['weight']); // The NULL key should be the first key in the weight. - $this->p_prepend_array_value(NULL, $this->request[self::REQUEST_ACCEPT]['data']['weight']); + $this->pr_prepend_array_value(NULL, $this->request[self::REQUEST_ACCEPT]['data']['weight']); // rename 'choices' array key to 'accept'. $this->request[self::REQUEST_ACCEPT]['data']['accept'] = $this->request[self::REQUEST_ACCEPT]['data']['choices']; @@ -4875,7 +4953,7 @@ class c_base_http extends c_base_rfc_string { krsort($this->request[self::REQUEST_ACCEPT_LANGUAGE]['data']['weight']); // The NULL key should be the first key in the weight. - $this->p_prepend_array_value(NULL, $this->request[self::REQUEST_ACCEPT_LANGUAGE]['data']['weight']); + $this->pr_prepend_array_value(NULL, $this->request[self::REQUEST_ACCEPT_LANGUAGE]['data']['weight']); // rename 'choices' array key to 'language'. $this->request[self::REQUEST_ACCEPT_LANGUAGE]['data']['language'] = $this->request[self::REQUEST_ACCEPT_LANGUAGE]['data']['choices']; @@ -4986,7 +5064,7 @@ class c_base_http extends c_base_rfc_string { krsort($this->request[self::REQUEST_ACCEPT_ENCODING]['data']['weight']); // The NULL key should be the first key in the weight. - $this->p_prepend_array_value(NULL, $this->request[self::REQUEST_ACCEPT_ENCODING]['data']['weight']); + $this->pr_prepend_array_value(NULL, $this->request[self::REQUEST_ACCEPT_ENCODING]['data']['weight']); // rename 'choices' array key to 'encoding'. $this->request[self::REQUEST_ACCEPT_ENCODING]['data']['encoding'] = $this->request[self::REQUEST_ACCEPT_ENCODING]['data']['choices']; @@ -5142,7 +5220,7 @@ class c_base_http extends c_base_rfc_string { krsort($this->request[self::REQUEST_ACCEPT_CHARSET]['data']['weight']); // The NULL key should be the first key in the weight. - $this->p_prepend_array_value(NULL, $this->request[self::REQUEST_ACCEPT_CHARSET]['data']['weight']); + $this->pr_prepend_array_value(NULL, $this->request[self::REQUEST_ACCEPT_CHARSET]['data']['weight']); } unset($this->request[self::REQUEST_ACCEPT_CHARSET]['data']['invalid']); @@ -5177,6 +5255,167 @@ class c_base_http extends c_base_rfc_string { } /** + * Load and process the HTTP request parameter: accept-datetime. + * + * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Request-Method + */ + private function p_load_request_access_control_request_method() { + $method_string = mb_strtolower($this->headers['access_control_request_method']); + $method_string = str_replace(' ', '', $method_string); + + $methods = explode(',', $method_string); + unset($method_string); + + if (empty($methods)) { + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['invalid'] = TRUE; + return; + } + + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'] = array(); + + foreach ($methods as $method) { + switch ($method) { + case 'get': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_GET] = self::HTTP_METHOD_GET; + break; + + case 'head': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_HEAD] = self::HTTP_METHOD_HEAD; + break; + + case 'post': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_POST] = self::HTTP_METHOD_POST; + break; + + case 'put': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_PUT] = self::HTTP_METHOD_PUT; + break; + + case 'delete': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_DELETE] = self::HTTP_METHOD_DELETE; + break; + + case 'trace': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_TRACE] = self::HTTP_METHOD_TRACE; + break; + + case 'options': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_OPTIONS] = self::HTTP_METHOD_OPTIONS; + break; + + case 'connect': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_CONNECT] = self::HTTP_METHOD_CONNECT; + break; + + case 'patch': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_PATCH] = self::HTTP_METHOD_PATCH; + break; + + case 'track': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_TRACK] = self::HTTP_METHOD_TRACK; + break; + + case 'debug': + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'][self::HTTP_METHOD_DEBUG] = self::HTTP_METHOD_DEBUG; + break; + + default: + // skip unknown methods instead of failing so that any discovered known methods are still loaded. + break; + } + } + unset($method); + + if (!empty($methods) && empty($this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['data'])) { + unset($methods); + + // no valid methods were found, now the error can be reported. + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['invalid'] = TRUE; + return; + } + unset($methods); + + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['defined'] = TRUE; + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_METHOD]['invalid'] = FALSE; + } + + /** + * Load and process the HTTP request parameter: accept-datetime. + * + * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Request-Headers + */ + private function p_load_request_access_control_request_headers() { + if (empty($this->headers['access_control_request_headers'])) { + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['invalid'] = TRUE; + return; + } + + $text = $this->pr_rfc_string_prepare($this->headers['access_control_request_headers']); + if ($text['invalid']) { + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['invalid'] = TRUE; + unset($text); + return; + } + + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['data'] = $this->pr_rfc_string_is_token($text['ordinals'], $text['characters']); + unset($text); + + if ($this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['data']['invalid']) { + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['invalid'] = TRUE; + } + else { + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['defined'] = TRUE; + + // rename 'tokens' array key to 'headers'. + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['data']['headers'] = $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['data']['tokens']; + unset($this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['data']['tokens']); + } + unset($this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['data']['invalid']); + unset($this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['data']['current']); + + $this->request[self::REQUEST_ACCESS_CONTROL_REQUEST_HEADERS]['invalid'] = FALSE; + } + + /** + * Load and process the HTTP request parameter: authorization. + * + * 1*(tchar) 1*(wsp) 1*(tchar) *(wsp) "=" *(wsp) ( 1*(tchar) / quoted-string ) *( *(wsp) "," *(wsp) 1*(tchar) *(wsp) "=" *(wsp) ( 1*(tchar) / quoted-string ) ). + * + * @see: https://tools.ietf.org/html/rfc7235#section-4.2 + */ + private function p_load_request_authorization() { + if (empty($this->headers['authorization'])) { + $this->request[self::REQUEST_AUTHORIZATION]['invalid'] = TRUE; + return; + } + + $text = $this->pr_rfc_string_prepare($this->headers['authorization']); + if ($text['invalid']) { + $this->request[self::REQUEST_AUTHORIZATION]['invalid'] = TRUE; + unset($text); + return; + } + + $this->request[self::REQUEST_AUTHORIZATION]['data'] = $this->pr_rfc_string_is_credentials($text['ordinals'], $text['characters']); + unset($text); + + if ($this->request[self::REQUEST_AUTHORIZATION]['data']['invalid']) { + $this->request[self::REQUEST_AUTHORIZATION]['invalid'] = TRUE; + } + else { + $this->request[self::REQUEST_AUTHORIZATION]['defined'] = TRUE; + } + unset($this->request[self::REQUEST_AUTHORIZATION]['data']['invalid']); + unset($this->request[self::REQUEST_AUTHORIZATION]['data']['current']); + + $this->request[self::REQUEST_AUTHORIZATION]['invalid'] = FALSE; + } + + /** * Load and process the HTTP request parameter: cache-control. * * Only 'no-cache' is supported at this time for requests. @@ -5283,7 +5522,7 @@ class c_base_http extends c_base_rfc_string { // rename 'tokens' array key to 'connection'. $this->request[self::REQUEST_CONNECTION]['data']['connection'] = $this->request[self::REQUEST_CONNECTION]['data']['tokens']; - unset($this->request[self::REQUEST_CONNECTION]['data']['tokens']); + unset($this->request[self::REQUEST_CONNECTION]['data']['text']); } unset($this->request[self::REQUEST_CONNECTION]['data']['invalid']); unset($this->request[self::REQUEST_CONNECTION]['data']['current']); @@ -5505,7 +5744,16 @@ class c_base_http extends c_base_rfc_string { return; } - $this->request[self::REQUEST_HOST]['data'] = $this->p_parse_uri($this->headers['host']); + $text = $this->pr_rfc_string_prepare($this->headers['host']); + if ($text['invalid']) { + unset($text); + + $this->request[self::REQUEST_HOST]['invalid'] = TRUE; + return; + } + + $this->request[self::REQUEST_HOST]['data'] = $this->pr_rfc_string_is_uri($text['ordinals'], $text['characters']); + unset($text); if ($this->request[self::REQUEST_HOST]['data']['invalid']) { $this->request[self::REQUEST_HOST]['invalid'] = TRUE; @@ -5754,6 +6002,7 @@ class c_base_http extends c_base_rfc_string { * * @see: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ */ private function p_load_request_origin() { if (empty($this->headers['origin'])) { @@ -5761,7 +6010,16 @@ class c_base_http extends c_base_rfc_string { return; } - $this->request[self::REQUEST_ORIGIN]['data'] = $this->p_parse_uri($this->headers['origin']); + $text = $this->pr_rfc_string_prepare($this->headers['origin']); + if ($text['invalid']) { + unset($text); + + $this->request[self::REQUEST_ORIGIN]['invalid'] = TRUE; + return; + } + + $this->request[self::REQUEST_ORIGIN]['data'] = $this->pr_rfc_string_is_uri($text['ordinals'], $text['characters']); + unset($text); if ($this->request[self::REQUEST_ORIGIN]['data']['invalid']) { $this->request[self::REQUEST_ORIGIN]['invalid'] = TRUE; @@ -5775,6 +6033,41 @@ class c_base_http extends c_base_rfc_string { } /** + * Load and process the HTTP request parameter: proxy-authorization. + * + * 1*(tchar) 1*(wsp) 1*(tchar) *(wsp) "=" *(wsp) ( 1*(tchar) / quoted-string ) *( *(wsp) "," *(wsp) 1*(tchar) *(wsp) "=" *(wsp) ( 1*(tchar) / quoted-string ) ). + * + * @see: https://tools.ietf.org/html/rfc7235#section-4.4 + */ + private function p_load_request_proxy_authorization() { + if (empty($this->headers['proxy_authorization'])) { + $this->request[self::REQUEST_PROXY_AUTHORIZATION]['invalid'] = TRUE; + return; + } + + $text = $this->pr_rfc_string_prepare($this->headers['proxy_authorization']); + if ($text['invalid']) { + $this->request[self::REQUEST_PROXY_AUTHORIZATION]['invalid'] = TRUE; + unset($text); + return; + } + + $this->request[self::REQUEST_PROXY_AUTHORIZATION]['data'] = $this->pr_rfc_string_is_credentials($text['ordinals'], $text['characters']); + unset($text); + + if ($this->request[self::REQUEST_PROXY_AUTHORIZATION]['data']['invalid']) { + $this->request[self::REQUEST_PROXY_AUTHORIZATION]['invalid'] = TRUE; + } + else { + $this->request[self::REQUEST_PROXY_AUTHORIZATION]['defined'] = TRUE; + } + unset($this->request[self::REQUEST_PROXY_AUTHORIZATION]['data']['invalid']); + unset($this->request[self::REQUEST_PROXY_AUTHORIZATION]['data']['current']); + + $this->request[self::REQUEST_PROXY_AUTHORIZATION]['invalid'] = FALSE; + } + + /** * Load and process the HTTP request parameter: referer. * * @see: https://tools.ietf.org/html/rfc7231#section-5.5.2 @@ -5785,7 +6078,16 @@ class c_base_http extends c_base_rfc_string { return; } - $this->request[self::REQUEST_REFERER]['data'] = $this->p_parse_uri($this->headers['referer']); + $text = $this->pr_rfc_string_prepare($this->headers['referer']); + if ($text['invalid']) { + unset($text); + + $this->request[self::REQUEST_REFERER]['invalid'] = TRUE; + return; + } + + $this->request[self::REQUEST_REFERER]['data'] = $this->pr_rfc_string_is_uri($text['ordinals'], $text['characters']); + unset($text); if ($this->request[self::REQUEST_REFERER]['data']['invalid']) { $this->request[self::REQUEST_REFERER]['invalid'] = TRUE; @@ -6013,13 +6315,152 @@ class c_base_http extends c_base_rfc_string { } /** - * Load and process the HTTP request parameter: checksum_header. - * - * The following format is expected: - * - 1*(atext):1*(atext):1*(atext) + * Load and process the HTTP request parameter: x-requested-with. * - * The header should have either 'partial:checksum_type:checksum_value' or 'complete:checksum_type:checksum_value'. - * The checksum_value is stored in base64. + * I am just assuming the syntax to be 1*(tchar). + * + * @see: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields + */ + private function p_load_request_x_requested_with() { + if (empty($this->headers['x-requested-with'])) { + $this->request[self::REQUEST_X_REQUESTED_WITH]['invalid'] = TRUE; + return; + } + + $text = $this->pr_rfc_string_prepare($this->headers['x-requested-with']); + if ($text['invalid']) { + $this->request[self::REQUEST_X_REQUESTED_WITH]['invalid'] = TRUE; + unset($text); + return; + } + + $this->request[self::REQUEST_X_REQUESTED_WITH]['data'] = $this->pr_rfc_string_is_token($text['ordinals'], $text['characters']); + unset($text); + + if ($this->request[self::REQUEST_X_REQUESTED_WITH]['data']['invalid']) { + $this->request[self::REQUEST_X_REQUESTED_WITH]['invalid'] = TRUE; + } + else { + $this->request[self::REQUEST_X_REQUESTED_WITH]['defined'] = TRUE; + } + unset($this->request[self::REQUEST_X_REQUESTED_WITH]['data']['invalid']); + + $this->request[self::REQUEST_X_REQUESTED_WITH]['invalid'] = FALSE; + } + + /** + * Load and process the HTTP request parameter: x-requested-for. + * + * This could be a name or an ip address. + * I am just using 1*(tchar) because that also allows ip addresses. + * + * @see: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields + */ + private function p_load_request_x_requested_for() { + if (empty($this->headers['x-requested-for'])) { + $this->request[self::REQUEST_X_REQUESTED_FOR]['invalid'] = TRUE; + return; + } + + $text = $this->pr_rfc_string_prepare($this->headers['x-requested-for']); + if ($text['invalid']) { + $this->request[self::REQUEST_X_REQUESTED_FOR]['invalid'] = TRUE; + unset($text); + return; + } + + $this->request[self::REQUEST_X_REQUESTED_HOST]['data'] = $this->pr_rfc_string_is_token($text['ordinals'], $text['characters']); + unset($text); + + if ($this->request[self::REQUEST_X_REQUESTED_HOST]['data']['invalid']) { + $this->request[self::REQUEST_X_REQUESTED_HOST]['invalid'] = TRUE; + } + else { + $this->request[self::REQUEST_X_REQUESTED_HOST]['defined'] = TRUE; + } + unset($this->request[self::REQUEST_X_REQUESTED_HOST]['data']['invalid']); + unset($this->request[self::REQUEST_X_REQUESTED_HOST]['data']['uri']); + + $this->request[self::REQUEST_X_REQUESTED_HOST]['invalid'] = FALSE; + } + + /** + * Load and process the HTTP request parameter: x-requested-host. + * + * I am just assuming the syntax to be (url). + * + * @see: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields + */ + private function p_load_request_x_requested_host() { + if (empty($this->headers['x-requested-host'])) { + $this->request[self::REQUEST_X_REQUESTED_HOST]['invalid'] = TRUE; + return; + } + + $text = $this->pr_rfc_string_prepare($this->headers['x-requested-host']); + if ($text['invalid']) { + $this->request[self::REQUEST_X_REQUESTED_HOST]['invalid'] = TRUE; + unset($text); + return; + } + + $this->request[self::REQUEST_X_REQUESTED_HOST]['data'] = $this->pr_rfc_string_is_uri($text['ordinals'], $text['characters']); + unset($text); + + if ($this->request[self::REQUEST_X_REQUESTED_HOST]['data']['invalid'] || $this->request[self::REQUEST_X_REQUESTED_HOST]['data']['uri'] === FALSE) { + $this->request[self::REQUEST_X_REQUESTED_HOST]['invalid'] = TRUE; + } + else { + $this->request[self::REQUEST_X_REQUESTED_HOST]['defined'] = TRUE; + } + unset($this->request[self::REQUEST_X_REQUESTED_HOST]['data']['invalid']); + unset($this->request[self::REQUEST_X_REQUESTED_HOST]['data']['uri']); + + $this->request[self::REQUEST_X_REQUESTED_HOST]['invalid'] = FALSE; + } + + /** + * Load and process the HTTP request parameter: x-requested-proto. + * + * I am just assuming the syntax to be 1*(tchar). + * + * @see: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields + */ + private function p_load_request_x_requested_proto() { + if (empty($this->headers['x-requested-proto'])) { + $this->request[self::REQUEST_X_REQUESTED_PROTO]['invalid'] = TRUE; + return; + } + + $text = $this->pr_rfc_string_prepare($this->headers['x-requested-proto']); + if ($text['invalid']) { + $this->request[self::REQUEST_X_REQUESTED_PROTO]['invalid'] = TRUE; + unset($text); + return; + } + + $this->request[self::REQUEST_X_REQUESTED_PROTO]['data'] = $this->pr_rfc_string_is_token($text['ordinals'], $text['characters']); + unset($text); + + if ($this->request[self::REQUEST_X_REQUESTED_PROTO]['data']['invalid']) { + $this->request[self::REQUEST_X_REQUESTED_PROTO]['invalid'] = TRUE; + } + else { + $this->request[self::REQUEST_X_REQUESTED_PROTO]['defined'] = TRUE; + } + unset($this->request[self::REQUEST_X_REQUESTED_PROTO]['data']['invalid']); + + $this->request[self::REQUEST_X_REQUESTED_PROTO]['invalid'] = FALSE; + } + + /** + * Load and process the HTTP request parameter: checksum_header. + * + * The following format is expected: + * - 1*(atext):1*(atext):1*(atext) + * + * The header should have either 'partial:checksum_type:checksum_value' or 'complete:checksum_type:checksum_value'. + * The checksum_value is stored in base64. * * The content checksum represents the checksum of the HTTP content (HTTP packet body). * This should already be defined if and when a checksum_header is used. @@ -6290,253 +6731,6 @@ class c_base_http extends c_base_rfc_string { } /** - * Decode and check that the given uri is valid. - * - * This does not decode the uri, it separates it into its individual parts. - * - * Validation is done according to rfc3986. - * - * foo://example.com:8042/over/there?name=ferret#nose - * \_/ \______________/\_________/ \_________/ \__/ - * | | | | | - * scheme authority path query fragment - * | _____________________|__ - * / \ / \ - * urn:example:animal:ferret:nose - * - * - * @param string $uri - * The url to validate and decode. - * - * @return array - * A decoded uri split into its different parts inside an array. - * An array key called 'invalid' exists to designate that the uri is invalid. - * - * @see: https://tools.ietf.org/html/rfc3986 - */ - private function p_parse_uri($uri) { - $result = array( - 'scheme' => NULL, - 'authority' => NULL, - 'path' => NULL, - 'query' => NULL, - 'fragment' => NULL, - 'url' => TRUE, // @todo: set to FALSE when uri is a urn instead of a url. - 'invalid' => FALSE, - ); - - // @todo: completely rewrite below. - $result['invalid'] = TRUE; -/* - $matches = array(); - $matched = preg_match('!^((\w[=|\w|\d|\s|\.|-|_|~|%]*)*:)*([^#|?]*)(\?([^#]+))*(#(.+))*$!iu', $uri, $matches); - - if ($matched == FALSE || !array_key_exists(3, $matches)) { - unset($address); - unset($matches); - unset($matched); - $result['invalid'] = TRUE; - return $result; - } - unset($matched); - - - // process scheme. - if (array_key_exists(2, $matches) && self::p_length_string($matches[2]) > 0) { - $combined = $matches[3]; - if (array_key_exists(4, $matches)) { - $combined .= $matches[4]; - } - if (array_key_exists(6, $matches)) { - $combined .= $matches[6]; - } - - $scheme_string = preg_replace('!:' . $combined . '$!iu', '', $matches[0]); - $result['scheme'] = mb_split(':', $scheme_string); - unset($scheme_string); - unset($combined); - - foreach ($result['scheme'] as &$s) { - $s = urldecode($s); - } - unset($s); - } - - - // process authority. - if (self::p_length_string($matches[3]) > 0) { - // rfc6854 designates multiple uris, separated by commas. - $authority = mb_split(',', $matches[3]); - foreach ($authority as $a) { - $sub_matches = array(); - $sub_matched = preg_match('!^(//|/|)(([^@]+)@)*(.*)$!iu', $a, $sub_matches); - - if ($sub_matched === FALSE || !isset($sub_matches[4])) { - $result['invalid'] = TRUE; - - unset($sub_matches); - unset($sub_matched); - continue; - } - - - // process user information. - $information_matches = array(); - if (preg_match('@^([=|!|$|&|\'|(|)|\*|\+|,|;|\w|\d|-|\.|_|~|%|\s]*)(:|)$@iu', $sub_matches[3], $information_matches) === FALSE || !isset($information_matches[1])) { - $result['invalid'] = TRUE; - - unset($information_matches); - unset($sub_matches); - unset($sub_matched); - continue; - } - - $authority_setting = array( - 'type_path' => self::URI_PATH_THIS, - 'type_host' => 0, - 'user' => urldecode($information_matches[1]), - 'host' => NULL, - 'port' => NULL, - ); - unset($information_matches); - - - // process host information. - if ($sub_matches[1] == '//') { - $authority_setting['type_path'] = self::URI_PATH_SITE; - } - elseif ($sub_matches[1] == '/') { - $authority_setting['type_path'] = self::URI_PATH_BASE; - } - - $ipv6_matches = array(); - if (preg_match('@^\[([^\]]+)\](:\d+$|$)@iu', $sub_matches[4], $ipv6_matches) !== FALSE && isset($ipv6_matches[1])) { - $authority_setting['type_host'] = self::URI_HOST_IPV6; - - $ip = inet_pton($ipv6_matches[1]); - if ($ip === FALSE) { - $result['invalid'] = TRUE; - - unset($ip); - unset($ipv6_matches); - continue; - } - - $authority_setting['host'] = inet_ntop($ip); - unset($ip); - - if (isset($ipv6_matches[2]) && self::p_length_string($ipv6_matches[2]) > 0) { - $authority_setting['port'] = (int) $ipv6_matches[2]; - } - - // @todo: ipvfuture is actually embedded inside of the the double brackets used by ipv6. - // to support this, the ipv6 regex must be modified to check for the ipvfuture parameters. - // $authority_setting['type_host'] = self::URI_HOST_IPVX; - // '@v[\d|a|b|c|d|e|f]\.([=|!|$|&|\'|(|)|\*|\+|,|;|\w|\d|-|\.|_|~|%|:]*)@i' - } - unset($ipv6_matches); - - $ipv4_matches = array(); - if (is_null($authority_setting['host']) && preg_match('@(\d+\.\d+\.d+\.d+)(:(\d+)|)$@iu', $sub_matches[4], $ipv4_matches) !== FALSE && isset($ipv4_matches[1])) { - $authority_setting['type_host'] = self::URI_HOST_IPV4; - - $ip = inet_pton($ipv4_matches[1]); - if ($ip === FALSE) { - $result['invalid'] = TRUE; - - unset($ip); - unset($ipv4_matches); - continue; - } - - $authority_setting['host'] = inet_ntop($ip); - unset($ip); - - if (isset($ipv4_matches[3]) && self::p_length_string($ipv4_matches[3]) > 0) { - $authority_setting['port'] = (int) $ipv4_matches[3]; - } - } - unset($ipv4_matches); - - $ipv4_matches = array(); - if (is_null($authority_setting['host']) && preg_match('@(\d+\.\d+\.d+\.d+)(:(\d+)|)$@iu', $sub_matches[4], $ipv4_matches) !== FALSE && isset($ipv4_matches[1])) { - $authority_setting['type_host'] = self::URI_HOST_IPV4; - - $ip = inet_pton($ipv4_matches[1]); - if ($ip === FALSE) { - $result['invalid'] = TRUE; - - unset($ip); - unset($ipv4_matches); - continue; - } - - $authority_setting['host'] = inet_ntop($ip); - unset($ip); - - if (isset($ipv4_matches[3]) && self::p_length_string($ipv4_matches[3]) > 0) { - $authority_setting['port'] = (int) $ipv4_matches[3]; - } - } - unset($ipv4_matches); - - $name_matches = array(); - if (is_null($authority_setting['host']) && preg_match('@((=|\w|\d|-|\.|_|~|\!|$|&|\'|(|)|\*|\+|,|;)+)(:(\d+)|)$@iu', $sub_matches[4], $name_matches) !== FALSE && isset($name_matches[1])) { - $authority_setting['type_host'] = self::URI_HOST_NAME; - $authority_setting['host'] = $name_matches[2]; - - if (isset($name_matches[4]) && self::p_length_string($name_matches[4]) > 0) { - $authority_setting['port'] = (int) $name_matches[4]; - } - } - unset($name_matches); - - $result['authority'][] = $authority_setting; - - unset($authority_setting); - unset($sub_matches); - unset($sub_matched); - } - - unset($a); - unset($authority); - } - - - // process query. - if (array_key_exists(5, $matches) && self::p_length_string($matches[5]) > 0) { - $query_parts = mb_split(',', $matches[5]); - - foreach ($query_parts as $qp) { - $qp_parts = mb_split('=', $qp, 2); - - if (is_array($qp_parts) && isset($qp_parts[0])) { - $decoded = urldecode($qp_parts[0]); - if (isset($qp_parts[1])) { - $result['query'][$decoded] = urldecode($qp_parts[1]); - } - else { - $result['query'][$decoded] = NULL; - } - unset($decoded); - } - } - unset($qp); - unset($query_parts); - } - - - // process fragment. - if (array_key_exists(7, $matches) && self::p_length_string($matches[7]) > 0) { - $result['fragment'][] = urldecode($matches[7]); - } - - unset($matches); -*/ - return $result; - } - - /** * Decode and check that the given string is a valid entity tag such as with if-match (but do not test for weakness). * * Validation is done according to rfc7232. @@ -7784,16 +7978,29 @@ class c_base_http extends c_base_rfc_string { * * @param string $token_name * The string to sanitize as a token name. + * @param bool $lower_case + * (optional) Force token to be lower-case. + * There are some cases where the token case may need to remain untouched. + * In such cases, set this to FALSE. * * @return string|bool * A sanitized string is return on success. * FALSE is returned on error or if the header name is invalid. */ - private function p_prepare_token($token_name) { - $trimmed = preg_replace('/(^\s+)|(\s+$)/us', '', mb_strtolower($token_name)); - if ($trimmed === FALSE) { - unset($trimmed); - return FALSE; + private function p_prepare_token($token_name, $lower_case = TRUE) { + if ($lower_case) { + $trimmed = preg_replace('/(^\s+)|(\s+$)/us', '', mb_strtolower($token_name)); + if ($trimmed === FALSE) { + unset($trimmed); + return FALSE; + } + } + else { + $trimmed = preg_replace('/(^\s+)|(\s+$)/us', '', $token_name); + if ($trimmed === FALSE) { + unset($trimmed); + return FALSE; + } } $text = $this->pr_rfc_string_prepare($trimmed); @@ -7824,13 +8031,24 @@ class c_base_http extends c_base_rfc_string { * The header output array to make changes to. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Origin */ private function p_prepare_header_response_access_control_allow_origin($header_name, &$header_output) { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_ALLOW_ORIGIN, $this->response)) { return; } - // @todo + if ($this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_ORIGIN]['wildcard']) { + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_ORIGIN] = $header_name . self::SEPARATOR_HEADER_NAME . '*'; + return; + } + + $combined = pr_rfc_string_combine_uri_array($this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_ORIGIN]); + if (is_string($combined)) { + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_ORIGIN] = $header_name . self::SEPARATOR_HEADER_NAME . $combined; + } + unset($combined); } /** @@ -7842,13 +8060,29 @@ class c_base_http extends c_base_rfc_string { * The header output array to make changes to. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Expose-Headers */ private function p_prepare_header_response_access_control_expose_headers($header_name, &$header_output) { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_EXPOSE_HEADERS, $this->response)) { return; } - // @todo + $header_output[self::RESPONSE_ACCESS_CONTROL_EXPOSE_HEADERS] = $header_name . self::SEPARATOR_HEADER_NAME; + + if (!empty($this->response[self::RESPONSE_ACCESS_CONTROL_EXPOSE_HEADERS])) { + $exposed_headers_array = $this->response[self::RESPONSE_ACCESS_CONTROL_EXPOSE_HEADERS]; + + reset($exposed_headers_array); + $exposed_header_name = array_shift($exposed_headers_array); + $header_output[self::RESPONSE_ACCESS_CONTROL_EXPOSE_HEADERS] .= $exposed_header_name; + + foreach ($exposed_headers_array as $exposed_header_name) { + $header_output[self::RESPONSE_ACCESS_CONTROL_EXPOSE_HEADERS] .= ', ' . $exposed_header_name; + } + unset($exposed_headers_array); + unset($exposed_header_name); + } } /** @@ -7860,13 +8094,72 @@ class c_base_http extends c_base_rfc_string { * The header output array to make changes to. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Methods */ private function p_prepare_header_response_access_control_allow_methods($header_name, &$header_output) { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS, $this->response)) { return; } - // @todo + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] = $header_name . self::SEPARATOR_HEADER_NAME; + + $allow_methods_array = $this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS]; + + reset($allow_methods_array); + $methods = array_shift($allow_methods_array); + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= $methods; + + foreach ($allow_methods_array as $methods) { + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= ', ' ; + + switch ($methods) { + case self::HTTP_METHOD_HEAD: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'HEAD'; + break; + + case self::HTTP_METHOD_POST: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'POST'; + break; + + case self::HTTP_METHOD_PUT: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'PUT'; + break; + + case self::HTTP_METHOD_DELETE: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'DELETE'; + break; + + case self::HTTP_METHOD_TRACE: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'TRACE'; + break; + + case self::HTTP_METHOD_OPTIONS: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'OPTIONS'; + break; + + case self::HTTP_METHOD_CONNECT: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'CONNECT'; + break; + + case self::HTTP_METHOD_PATCH: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'PATCH'; + break; + + case self::HTTP_METHOD_TRACK: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'TRACK'; + break; + + case self::HTTP_METHOD_DEBUG: + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= 'DEBUG'; + break; + + } + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_METHODS] .= ', ' . $methods; + } + unset($allow_methods_array); + unset($methods); + } /** @@ -7878,13 +8171,31 @@ class c_base_http extends c_base_rfc_string { * The header output array to make changes to. * * @see: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing + * @see: https://www.w3.org/TR/CSP2/ + * @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Headers */ private function p_prepare_header_response_access_control_allow_headers($header_name, &$header_output) { if (!array_key_exists(self::RESPONSE_ACCESS_CONTROL_ALLOW_HEADERS, $this->response)) { return; } - // @todo + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_HEADERS] = $header_name . self::SEPARATOR_HEADER_NAME; + + if (empty($this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_HEADERS])) { + return; + } + + $allowed_headers_array = $this->response[self::RESPONSE_ACCESS_CONTROL_ALLOW_HEADERS]; + + reset($allowed_headers_array); + $allowed_header_name = array_shift($allowed_headers_array); + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_HEADERS] .= $allowed_header_name; + + foreach ($allowed_headers_array as $allowed_header_name) { + $header_output[self::RESPONSE_ACCESS_CONTROL_ALLOW_HEADERS] .= ', ' . $allowed_header_name; + } + unset($allowed_headers_array); + unset($allowed_header_name); } /** @@ -8285,71 +8596,53 @@ class c_base_http extends c_base_rfc_string { return; } - $header_output[self::RESPONSE_LINK] = $header_name . self::SEPARATOR_HEADER_NAME; - - $uri = NULL; - if ($this->response[self::RESPONSE_LINK]['uri']['url']) { - if (!is_null($this->response[self::RESPONSE_LINK]['uri']['scheme'])) { - $uri .= $this->response[self::RESPONSE_LINK]['uri']['scheme'] . '://'; - } - - if (!is_null($this->response[self::RESPONSE_LINK]['uri']['authority'])) { - $uri .= $this->response[self::RESPONSE_LINK]['uri']['authority'] . '/'; - } - - if (!is_null($this->response[self::RESPONSE_LINK]['uri']['path'])) { - $uri .= $this->response[self::RESPONSE_LINK]['uri']['path']; - } - - if (!is_null($this->response[self::RESPONSE_LINK]['uri']['query'])) { - $uri .= '?' . $this->response[self::RESPONSE_LINK]['uri']['query']; - } - - if (!is_null($this->response[self::RESPONSE_LINK]['uri']['fragment'])) { - $uri .= '#' . $this->response[self::RESPONSE_LINK]['uri']['fragment']; - } - } - else { - if (!is_null($this->response[self::RESPONSE_LINK]['uri']['scheme'])) { - $uri .= $this->response[self::RESPONSE_LINK]['uri']['scheme'] . ':'; + foreach ($this->response[self::RESPONSE_LINK] as $uris) { + $uri = $this->pr_rfc_string_combine_uri_array($uris['uri']); + if ($uri === FALSE) { + unset($uri); + unset($uris); + return; } - if (!is_null($this->response[self::RESPONSE_LINK]['uri']['path'])) { - $uri .= $this->response[self::RESPONSE_LINK]['uri']['path']; + if (!isset($header_output[self::RESPONSE_LINK])) { + $header_output[self::RESPONSE_LINK] = ''; } - } - - $header_output[self::RESPONSE_LINK] .= '<' . $uri . '>'; - unset($uri); - if (!empty($this->response[self::RESPONSE_LINK]['parameters'])) { - $parameter_value = reset($this->response[self::RESPONSE_LINK]['parameters']); - $parameter_name = key($this->response[self::RESPONSE_LINK]['parameters']); - unset($this->response[self::RESPONSE_LINK]['parameters'][$parameter_name]); - - if (is_null($parameter_value)) { - $parameters_string = $parameter_name; - } - else { - $parameters_string = $parameter_name . '=' . $parameter_value; - } + $header_output[self::RESPONSE_LINK] .= $header_name . self::SEPARATOR_HEADER_NAME; + $header_output[self::RESPONSE_LINK] .= '<' . $uri . '>'; + unset($uri); - foreach($this->response[self::RESPONSE_LINK]['parameters'] as $parameter_name => $parameter_value) { - $parameters_string .= '; '; + if (!empty($uris['parameters'])) { + $parameter_value = reset($uris['parameters']); + $parameter_name = key($uris['parameters']); + unset($uris['parameters'][$parameter_name]); + $parameters_string = '; '; if (is_null($parameter_value)) { $parameters_string .= $parameter_name; } else { $parameters_string .= $parameter_name . '=' . $parameter_value; } - } - unset($parameter_name); - unset($parameter_value); - $header_output[self::RESPONSE_LINK] .= $parameters_string; - unset($parameters_string); + foreach($uris['parameters'] as $parameter_name => $parameter_value) { + $parameters_string .= '; '; + + if (is_null($parameter_value)) { + $parameters_string .= $parameter_name; + } + else { + $parameters_string .= $parameter_name . '=' . $parameter_value; + } + } + unset($parameter_name); + unset($parameter_value); + + $header_output[self::RESPONSE_LINK] .= $parameters_string; + unset($parameters_string); + } } + unset($uris); } /** @@ -8787,9 +9080,9 @@ class c_base_http extends c_base_rfc_string { return; } - // in this case, a new header is created for every single entry.. + // in this case, a new header is created for every single entry. $header_output[self::RESPONSE_X_UA_COMPATIBLE] = array(); - foreach($header_output[self::RESPONSE_X_UA_COMPATIBLE] as $browser_name => $compatible_version) { + foreach($this->response[self::RESPONSE_X_UA_COMPATIBLE] as $browser_name => $compatible_version) { $header_output[self::RESPONSE_X_UA_COMPATIBLE][] = $browser_name . '=' . $compatible_version; } unset($browser_name); @@ -9203,6 +9496,31 @@ class c_base_http extends c_base_rfc_string { } /** + * Prepare HTTP response headers that are boolean values represented by the words true/false. + * + * @param string $header_name + * The HTTP header name, such as: 'Age'. + * @param array $header_output + * The header output array to make changes to. + * @param int $code + * The HTTP header code, such as: self::RESPONSE_AGE. + */ + private function p_prepare_header_response_boolean_value($header_name, &$header_output, $code) { + if (!array_key_exists($code, $this->response)) { + return; + } + + $header_output[$code] = $header_name . self::SEPARATOR_HEADER_NAME; + + if ($this->response[$code]) { + $header_output[$code] .= 'true'; + } + else { + $header_output[$code] .= 'false'; + } + } + + /** * Prepare HTTP response header: date * * @param string $header_name diff --git a/common/base/classes/base_rfc_char.php b/common/base/classes/base_rfc_char.php index f163bb2..122e578 100644 --- a/common/base/classes/base_rfc_char.php +++ b/common/base/classes/base_rfc_char.php @@ -332,10 +332,9 @@ abstract class c_base_rfc_char extends c_base_return { /** * Check to see if character is: tchar68. * - * This doesn't directly exist, but is supplied for use with token68 in the same way tchar is used by token. - * - * tchar = Visible ASCII character codes: 43, 45->57, 65->90, 95, 97->122, 126 + * tchar = Visible ASCII character codes: 43, 45->57, 65->90, 95, 97->122, 126. * tchar = UTF_8-2, UTF_8-3, UTF_8-4. + * tchar = ALPHA, DIGIT, '-', '.', '_', '~', '+', '/'. * * @param int $ordinal * A code representing a single character to test. @@ -770,7 +769,8 @@ abstract class c_base_rfc_char extends c_base_return { /** * Check to see if character is: unreserved. * - * unreserved = ASCII character codes 45, 46, 48->57, 65->90, 95, 97->122, 126 + * unreserved = ASCII character codes 45, 46, 48->57, 65->90, 95, 97->122, 126. + * = ALPHA, DIGIT, '-', '.', '_', '~'. * * @param int $ordinal * A code representing a single character to test. @@ -793,9 +793,10 @@ abstract class c_base_rfc_char extends c_base_return { } /** - * Check to see if character is: unreserved. + * Check to see if character is: reserved. * - * reserved = ASCII character codes 33, 35, 36, 38->44, 47, 58, 59, 61, 63, 64, 91, 93 + * reserved = ASCII character codes 33, 35, 36, 38->44, 47, 58, 59, 61, 63, 64, 91, 93. + * = ':', '/', '?', '#', '[', ']', '@', '!', '$', '&', ''', '(', ')', '*', '+', ',', ';', '='. * * @param int $ordinal * A code representing a single character to test. @@ -828,7 +829,8 @@ abstract class c_base_rfc_char extends c_base_return { /** * Check to see if character is: gen-delims. * - * gen-delims = ASCII character codes 35, 47, 58, 63, 64, 91, 93 + * gen-delims = ASCII character codes 35, 47, 58, 63, 64, 91, 93. + * = ':', '/', '?', '#', '[', ']', '@'. * * @param int $ordinal * A code representing a single character to test. @@ -853,7 +855,8 @@ abstract class c_base_rfc_char extends c_base_return { /** * Check to see if character is: sub-delims. * - * sub-delims = ASCII character codes 33, 36, 38->44, 59, 61 + * sub-delims = ASCII character codes 33, 36, 38->44, 59, 61. + * = '!', '$', '&', ''', '(', ')', '*', '+', ',', ';', '='. * * @param int $ordinal * A code representing a single character to test. @@ -880,6 +883,102 @@ abstract class c_base_rfc_char extends c_base_return { } /** + * Check to see if character is: pchar. + * + * sub-delims = ASCII character codes 33, 36->46, 48->59, 61, 64->90, 95, 97->122, 126. + * = ALPHA, DIGIT, '-', '.', '_', '~', '!', '$', '&', ''', '(', ')', '*', '+', ',', ';', '='. ':', '@', '%'. + * + * @param int $ordinal + * A code representing a single character to test. + * + * @return bool + * TRUE on match, FALSE otherwise. + * + * @see: https://tools.ietf.org/html/rfc3986#appendix-A + */ + protected function pr_rfc_char_is_pchar($ordinal) { + if ($ordinal == c_base_ascii::EXCLAMATION) { + return TRUE; + } + + if ($ordinal > c_base_ascii::HASH && $ordinal < c_base_ascii::SLASH_FORWARD) { + return TRUE; + } + + if ($ordinal > c_base_ascii::SLASH_FORWARD && $ordinal < c_base_ascii::LESS_THAN) { + return TRUE; + } + + if ($ordinal == c_base_ascii::EQUAL) { + return TRUE; + } + + if ($ordinal > c_base_ascii::QUESTION_MARK && $ordinal < c_base_ascii::BRACKET_OPEN) { + return TRUE; + } + + if ($ordinal == c_base_ascii::UNDERSCORE) { + return TRUE; + } + + if ($ordinal > c_base_ascii::GRAVE && $ordinal < c_base_ascii::BRACE_OPEN) { + return TRUE; + } + + if ($ordinal == c_base_ascii::TILDE) { + return TRUE; + } + + return FALSE; + } + + /** + * Check to see if character is: query (or fragment). + * + * query = ASCII character codes 33, 36->59, 61, 63->90, 95, 97->122, 126. + * = ALPHA, DIGIT, '-', '.', '_', '~', '!', '$', '&', ''', '(', ')', '*', '+', ',', ';', '='. ':', '@', '%', '?', '/'. + * + * @param int $ordinal + * A code representing a single character to test. + * + * @return bool + * TRUE on match, FALSE otherwise. + * + * @see: https://tools.ietf.org/html/rfc3986#appendix-A + */ + protected function pr_rfc_char_is_query($ordinal) { + if ($ordinal == c_base_ascii::EXCLAMATION) { + return TRUE; + } + + if ($ordinal > c_base_ascii::HASH && $ordinal < c_base_ascii::LESS_THAN) { + return TRUE; + } + + if ($ordinal == c_base_ascii::EQUAL) { + return TRUE; + } + + if ($ordinal > c_base_ascii::GREATER_THAN && $ordinal < c_base_ascii::BRACKET_OPEN) { + return TRUE; + } + + if ($ordinal == c_base_ascii::UNDERSCORE) { + return TRUE; + } + + if ($ordinal > c_base_ascii::GRAVE && $ordinal < c_base_ascii::BRACE_OPEN) { + return TRUE; + } + + if ($ordinal == c_base_ascii::TILDE) { + return TRUE; + } + + return FALSE; + } + + /** * Check to see if character is: UTF_8-1 (ASCII). * * The standard claims the following ranges: diff --git a/common/base/classes/base_rfc_string.php b/common/base/classes/base_rfc_string.php index 870e084..be25771 100644 --- a/common/base/classes/base_rfc_string.php +++ b/common/base/classes/base_rfc_string.php @@ -349,8 +349,6 @@ abstract class c_base_rfc_string extends c_base_rfc_char { * - 'text': A string containing the processed entity tag. * - 'current': an integer representing the position the counter stopped at. * - 'invalid': a boolean representing whether or not this string is valid or if an error occurred. - * - * @see: base_rfc_char::pr_rfc_char_qtext() */ protected function pr_rfc_string_is_entity_tag($ordinals, $characters, $start = 0, $stop = NULL) { $result = array( @@ -773,8 +771,10 @@ abstract class c_base_rfc_string extends c_base_rfc_char { /** * Processes a string based on the rfc syntax: credentials. * - * A string that is a digit has the following syntax: - * - token [ 1*(ws) ( token68 / [ ( "," / token *(ws) "=" *(ws) ( token / quoted-string ) ) *( *(ws) "," [ *(ws) token *(ws) "=" *(ws) ( token / quoted-string ) ] ) ] ) ] + * A string that has the following syntax: + * - 1*(tchar) 1*(wsp) 1*(tchar) *(wsp) "=" *(wsp) ( 1*(tchar) / quoted-string ) *( *(wsp) "," *(wsp) 1*(tchar) *(wsp) "=" *(wsp) ( 1*(tchar) / quoted-string ) ). + * + * @todo: this entire function is incomplete, finish writing it. * * @param array $ordinals * An array of integers representing each character of the string. @@ -796,14 +796,12 @@ abstract class c_base_rfc_string extends c_base_rfc_char { */ protected function pr_rfc_string_is_credentials($ordinals, $characters, $start = 0, $stop = NULL) { $result = array( - 'text' => NULL, + 'scheme' => NULL, + 'parameters' => array(), 'current' => $start, 'invalid' => FALSE, ); - // @todo: this looks like a lot of work, so deal with this at some point in the future because this is a very low priority function. - $result['invalid'] = TRUE; -/* if (is_null($stop)) { $stop = count($ordinals); } @@ -812,6 +810,8 @@ abstract class c_base_rfc_string extends c_base_rfc_char { return $result; } + + // load the scheme. for (; $result['current'] < $stop; $result['current']++) { if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { // @fixme: should error be reported? do some debugging with this. @@ -819,17 +819,146 @@ abstract class c_base_rfc_string extends c_base_rfc_char { break; } - $code = $ordinals[$result['current']]; + if ($this->pr_rfc_char_is_wsp($ordinals[$result['current']])) { + // reached end of the scheme. + $result['current']++; - if (!$this->pr_rfc_char_is_digit($code)) { + $this->p_rfc_string_skip_past_whitespace($ordinals, $characters, $stop, $result); + if ($result['invalid']) { + return $result; + } + + break; + } + elseif (!$this->pr_rfc_char_is_tchar($ordinals[$result['current']])) { + $result['invalid'] = TRUE; + return $result; + } + + $result['scheme'] .= $characters[$result['current']]; + } + + if ($result['current'] >= $stop) { + $result['invalid'] = TRUE; + return $result; + } + + + // load the parameters. + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + return $result; + } + + $this->p_rfc_string_skip_past_whitespace($ordinals, $characters, $stop, $result); + if ($result['invalid']) { + return $result; + } + + // load the parameter name. + $parameter_name = NULL; + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + return $result; + } + + elseif (!$this->pr_rfc_char_is_tchar($ordinals[$result['current']])) { + $result['invalid'] = TRUE; + return $result; + } + + $parameter_name .= $characters[$result['current']]; + } + + $this->p_rfc_string_skip_past_whitespace($ordinals, $characters, $stop, $result); + if ($result['invalid']) { + return $result; + } + + if ($result['current'] >= $stop) { + $result['invalid'] = TRUE; + return $result; + } + + // a parameter name must be followed by an equal sign and then the parameter value.. + if ($ordinals[$result['current']] != c_base_ascii::EQUAL) { + $result['invalid'] = TRUE; + return $result; + } + + $this->p_rfc_string_skip_past_whitespace($ordinals, $characters, $stop, $result); + if ($result['invalid']) { + return $result; + } + + if ($result['current'] >= $stop) { $result['invalid'] = TRUE; + return $result; + } + + // load the parameter value. + if ($ordinals[$result['current']] == c_base_ascii::QUOTE_DOUBLE) { + $result['current']++; + if ($result['current'] >= $stop) { + $result['invalid'] = TRUE; + return $result; + } + + $parsed = $this->pr_rfc_string_is_quoted_string($ordinals, $characters, $result['current'], self::STOP_AT_CLOSING_CHARACTER); + $result['current'] = $parsed['current']; + + if ($parsed['invalid']) { + unset($parsed); + + $result['invalid'] = TRUE; + return $result; + } + + $result['parameters'][$parameter_name] = $parsed['text']; + unset($parsed); + } + else { + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + return $result; + } + + if (!$this->pr_rfc_char_is_tchar($ordinals[$result['current']])) { + break; + } + + $result['parameters'][$parameter_name] .= $characters[$result['current']]; + } + } + + $this->p_rfc_string_skip_past_whitespace($ordinals, $characters, $stop, $result); + if ($result['invalid']) { + return $result; + } + + if ($result['current'] >= $stop) { break; } - $result['text'] .= $characters[$result['current']]; + + // A comma designates a new entry + if ($ordinals[$result['current']] == c_base_ascii::COMMA) { + $result['current']++; + if ($result['current'] >= $stop) { + $result['invalid'] = TRUE; + return $result; + } + } + + $parameter_name = NULL; } - unset($code); -*/ + return $result; } @@ -1514,7 +1643,7 @@ abstract class c_base_rfc_string extends c_base_rfc_char { krsort($result['choices']); // The NULL key should be the first key in the weight. - $this->p_prepend_array_value(NULL, $result['choices']); + $this->pr_prepend_array_value(NULL, $result['choices']); return $result; } @@ -1545,7 +1674,6 @@ abstract class c_base_rfc_string extends c_base_rfc_char { * - 'current': an integer representing the position the counter stopped at. * - 'invalid': a boolean representing whether or not this string is valid or if an error occurred. * - * @see: base_rfc_char::pr_rfc_char_tchar() * @see: https://tools.ietf.org/html/rfc2616#section-3.7 */ protected function pr_rfc_string_is_media_type($ordinals, $characters, $start = 0, $stop = NULL) { @@ -1824,7 +1952,6 @@ abstract class c_base_rfc_string extends c_base_rfc_char { * - 'invalid': a boolean representing whether or not this string is valid or if an error occurred. * * @see: self::pr_rfc_string_is_valued_token_comma() - * @see: base_rfc_char::pr_rfc_char_tchar() * @see: base_rfc_char::pr_rfc_string_is_media_type() * @see: https://tools.ietf.org/html/rfc2616#section-3.7 */ @@ -1850,7 +1977,6 @@ abstract class c_base_rfc_string extends c_base_rfc_char { $code = $ordinals[$result['current']]; if ($this->pr_rfc_char_is_wsp($code)) { - // @todo: handle whitespace between '='. if (is_null($token_name)) { continue; } @@ -2026,7 +2152,6 @@ abstract class c_base_rfc_string extends c_base_rfc_char { * - 'invalid': a boolean representing whether or not this string is valid or if an error occurred. * * @see: self::pr_rfc_string_is_valued_token() - * @see: base_rfc_char::pr_rfc_char_tchar() * @see: base_rfc_char::pr_rfc_string_is_media_type() * @see: https://tools.ietf.org/html/rfc2616#section-3.7 */ @@ -2052,7 +2177,6 @@ abstract class c_base_rfc_string extends c_base_rfc_char { $code = $ordinals[$result['current']]; if ($this->pr_rfc_char_is_wsp($code)) { - // @todo: handle whitespace between '='. if (is_null($token_name)) { continue; } @@ -2203,10 +2327,10 @@ abstract class c_base_rfc_string extends c_base_rfc_char { * - A simpler syntax will therefore be used. * * A valued_token has the following syntax: - * - 1*(*(ws) "," *(ws) token) + * - 1*(*(wsp) "," *(wsp) token) * * Original valued_token standard syntax: - * - *("," *(ws)) token *(*(ws) "," *(ws) token) + * - *("," *(wsp)) token *(*(wsp) "," *(wsp) token) * * @param array $ordinals * An array of integers representing each character of the string. @@ -2225,7 +2349,6 @@ abstract class c_base_rfc_string extends c_base_rfc_char { * - 'current': an integer representing the position the counter stopped at. * - 'invalid': a boolean representing whether or not this string is valid or if an error occurred. * - * @see: base_rfc_char::pr_rfc_char_tchar() * @see: https://tools.ietf.org/html/rfc7230#appendix-B */ protected function pr_rfc_string_is_commad_token($ordinals, $characters, $start = 0, $stop = NULL) { @@ -2307,6 +2430,94 @@ abstract class c_base_rfc_string extends c_base_rfc_char { } /** + * Processes a string based on the rfc syntax: path. + * + * A path has the following syntax: + * - *(ALPHA / DIGIT / "-" / "." / "_" / "~" / "%" HEXDIG HEXDIG / "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" / ":" / "@" / "/" / "?") + * + * @param array $ordinals + * An array of integers representing each character of the string. + * @param array $characters + * An array of characters representing the string. + * @param int $start + * (optional) The position in the arrays to start checking. + * @param int|null $stop + * (optional) The position in the arrays to stop checking. + * If NULL, then the entire string is processed or the closing character is found. + * + * @return array + * The processed information: + * - 'text': A string containing the validated path. + * - 'current': an integer representing the position the counter stopped at. + * - 'invalid': a boolean representing whether or not this string is valid or if an error occurred. + * + * @see: https://tools.ietf.org/html/rfc3986#section-3.4 + */ + protected function pr_rfc_string_is_path($ordinals, $characters, $start = 0, $stop = NULL) { + $result = array( + 'text' => NULL, + 'current' => $start, + 'invalid' => FALSE, + ); + + if (is_null($stop)) { + $stop = count($ordinals); + } + + if ($start >= $stop) { + return $result; + } + + for (; $result['current'] < $stop; $result['current']++) { + $code = $ordinals[$result['current']]; + + if ($code == c_base_ascii::PERCENT) { + // valid only if two hex digits immediately follow. + $result['current']++; + if ($result['current'] >= $stop) { + // this is invalid because it is cut off before 2 hex digits are required and none is found. + $result['invalid'] = TRUE; + break; + } + + $code = $ordinals[$result['current']]; + if (!$this->pr_rfc_char_is_hexdigit($ordinals[$result['current']])) { + $result['invalid'] = TRUE; + break; + } + + $result['current']++; + if ($result['current'] >= $stop) { + // this is invalid because it is cut off before 2 hex digits are required and only one is found. + $result['invalid'] = TRUE; + break; + } + + $code = $ordinals[$result['current']]; + if (!$this->pr_rfc_char_is_hexdigit($ordinals[$result['current']])) { + $result['invalid'] = TRUE; + break; + } + } + elseif (self::pr_rfc_char_is_pchar($code)) { + // do nothing, valid. + } + elseif ($code == c_base_asccii::SLASH_FORWARD) { + // do nothing, valid. + } + else { + $result['invalid'] = TRUE; + break; + } + + $result['text'] .= $characters[$result['current']]; + } + unset($code); + + return $result; + } + + /** * Processes a string based on the rfc syntax: query. * * This is also used by the rfc syntax: fragment. @@ -2326,21 +2537,15 @@ abstract class c_base_rfc_string extends c_base_rfc_char { * * @return array * The processed information: - * - 'address': A string containing the processed ip address. - * When is_future is TRUE, this is an array containing: - * - 'version': The ipfuture version. - * - 'ip': The ip address. - * - 'is_future': A boolean that when TRUE represents an ipvfuture address and when FALSE represents an ipv6 address. + * - 'text': A string containing the validated query. * - 'current': an integer representing the position the counter stopped at. * - 'invalid': a boolean representing whether or not this string is valid or if an error occurred. * - * @see: base_rfc_char::pr_rfc_char_tchar() - * @see: https://tools.ietf.org/html/rfc2616#section-3.7 + * @see: https://tools.ietf.org/html/rfc3986#section-3.4 */ protected function pr_rfc_string_is_query($ordinals, $characters, $start = 0, $stop = NULL) { $result = array( - 'address' => NULL, - 'is_future' => FALSE, + 'text' => NULL, 'current' => $start, 'invalid' => FALSE, ); @@ -2353,7 +2558,53 @@ abstract class c_base_rfc_string extends c_base_rfc_char { return $result; } - // @todo: finish writing this! + for (; $result['current'] < $stop; $result['current']++) { + $code = $ordinals[$result['current']]; + + if ($code == c_base_ascii::PERCENT) { + // valid only if two hex digits immediately follow. + $result['current']++; + if ($result['current'] >= $stop) { + // this is invalid because it is cut off before 2 hex digits are required and none is found. + $result['invalid'] = TRUE; + break; + } + + $code = $ordinals[$result['current']]; + if (!$this->pr_rfc_char_is_hexdigit($ordinals[$result['current']])) { + $result['invalid'] = TRUE; + break; + } + + $result['current']++; + if ($result['current'] >= $stop) { + // this is invalid because it is cut off before 2 hex digits are required and only one is found. + $result['invalid'] = TRUE; + break; + } + + $code = $ordinals[$result['current']]; + if (!$this->pr_rfc_char_is_hexdigit($ordinals[$result['current']])) { + $result['invalid'] = TRUE; + break; + } + } + elseif (self::pr_rfc_char_is_pchar($code)) { + // do nothing, valid. + } + elseif ($code == c_base_asccii::SLASH_FORWARD || $code == c_base_asccii::QUESTION_MARK) { + // do nothing, valid. + } + else { + $result['invalid'] = TRUE; + break; + } + + $result['text'] .= $characters[$result['current']]; + } + unset($code); + + return $result; } /** @@ -2385,7 +2636,6 @@ abstract class c_base_rfc_string extends c_base_rfc_char { * - 'current': an integer representing the position the counter stopped at. * - 'invalid': a boolean representing whether or not this string is valid or if an error occurred. * - * @see: base_rfc_char::pr_rfc_char_tchar() * @see: https://tools.ietf.org/html/rfc2616#section-3.7 */ protected function pr_rfc_string_is_ip_literal($ordinals, $characters, $start = 0, $stop = NULL) { @@ -2457,20 +2707,14 @@ abstract class c_base_rfc_string extends c_base_rfc_char { for (; $result['current'] < $stop; $result['current']++) { $code = $ordinals[$result['current']]; - if (self::pr_rfc_char_is_digit($code)) { - // do nothing, valid - } - elseif (self::pr_rfc_char_is_alpha($code)) { - // do nothing, valid - } - elseif (self::pr_rfc_char_is_unreserved($code)) { - // do nothing, valid + if (self::pr_rfc_char_is_unreserved($code)) { + // do nothing, valid. } elseif (self::pr_rfc_char_is_sub_delims($code)) { - // do nothing, valid + // do nothing, valid. } elseif ($code == c_base_ascii::COLON) { - // do nothing, valid + // do nothing, valid. } elseif ($code == c_base_ascii::BRACKET_CLOSE) { break; @@ -2634,23 +2878,606 @@ abstract class c_base_rfc_string extends c_base_rfc_char { } /** - * Effectively unshift a value onto a given array with a specified index. + * Decode and check that the given uri is valid. + * + * This does not decode the uri, it separates it into its individual parts. + * + * Validation is done according to rfc3986. + * + * foo://example.com:8042/over/there?name=ferret#nose + * \_/ \______________/\_________/ \_________/ \__/ + * | | | | | + * scheme authority path query fragment + * | _____________________|__ + * / \ / \ + * foo:example:animal:ferret:nose + * + * The standard is misleading in its definition of path. + * First it says path can be path-abempty, path-absolute, path-noscheme, path-rootless, or path-empty. + * it then defines those as follows: + * path-abempty: begins with "/" or is empty + * path-absolute: begins with "/" but not "//" + * path-noscheme: begins with a non-colon segment + * path-rootless: begins with a segment + * path-empty: zero characters + * + * path-abempty's definition includes a '//', making path-absolute irrelevant/redundant. + * path-rootless's definition includes a colon, making path-noscheme irrelevant/redundant. + * path-empty's definition is inconsistent, why not say 'is empty' as with path-abempty? + * + * I am going to assume that path-abempty is meant to be defined as 'begins with "/" but not "//"' or 'is empty'. + * + * The standard also provides a regex example that violates their own rules: + * - Regex: ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + * - Their URI: http://www.ics.uci.edu/pub/ietf/uri/#Related + * - Example URI: example:relative:url/that:is:not/a:urn + * + * That example is syntatically valid for 'path-absolute', but has part of it turned into a scheme. + * While it is currently not popular to use ':' in a url path, it is valid according to the standard. + * Therefore, their example regex is non-conforming. + * + * In fact, as-written, the standard provides no means to distinguish between a scheme and a path. + * - Example Scheme: my_scheme: + * - Example Path: my_path: + * Both of those are valid for their appropriate syntax. + * + * I believe that the way they wrote the standard is very mis-leading. + * path = path-abempty / path-absolute / path-noscheme / path-rootless / path-empty + * means that any one of those could be valid, thats an OR. + * but they are likely interpretting the '/' to be a separation betweem if/then conditionals that are not clearly defined. + * + * I am going to assume that what they mean is ':' is supported only in path IF a scheme is supplied. + * In this case the first colon separates the schema from everything else. + * Even with this interpretation, there are still problems because the following would still be syntatically valid: + * - Example URI: http://www.example.com/a:syntatically:valid:path + * If that example is what is necessary, then how does one make a valid relative uri for paths on that site!? + * - schemes are not allowed in relative paths, but that path still exists! + * - The standard supports these paths as absolute but does not support them as relative. + * + * For simplicity purposes, this function will violate the literal standard to follow what I am guessing to be the intended standard. + * If a colon is to appear in the path, it must be a URN and if so then it must have a scheme. * - * The NULL key should be the first key in the weight. - * PHP does not provide a way to preserve keys when merging arrays nor does PHP provide a way to unshift a value onto an array with a specific key. + * @param array $ordinals + * An array of integers representing each character of the string. + * @param array $characters + * An array of characters representing the string. + * @param int $start + * (optional) The position in the arrays to start checking. + * @param int|null $stop + * (optional) The position in the arrays to stop checking. + * If NULL, then the entire string is processed. * - * @param $key - * The index name to unshift the value onto the array as. - * @param array $array - * The array to unshift onto. + * @return array + * The processed information: + * - 'scheme': The protocol string. + * - 'authority': The domain string. + * - 'path': The path string. + * - 'query': An array of url arguments. + * - 'fragment': The id string. + * - 'url': A boolean that when TRUE means the string is a url and when FALSE the string is a urn. + * - 'invalid': a boolean representing whether or not this string is valid or if an error occurred. + * + * @see: self::p_combine_uri_array() + * @see: self::pr_rfc_string_is_scheme() + * @see: self::pr_rfc_string_is_authority() + * @see: self::pr_rfc_string_is_path() + * @see: self::pr_rfc_string_is_query() + * @see: https://tools.ietf.org/html/rfc3986 */ - protected function p_prepend_array_value($key, &$array) { - if (!array_key_exists($key, $array)) { - return; - } - - $value = $array[$key]; - unset($array[$key]); + protected function pr_rfc_string_is_uri($ordinals, $characters, $start = 0, $stop = NULL) { + $result = array( + 'scheme' => NULL, + 'authority' => NULL, + 'path' => NULL, + 'query' => NULL, + 'fragment' => NULL, + 'url' => TRUE, + 'current' => $start, + 'invalid' => FALSE, + ); + + if (is_null($stop)) { + $stop = count($ordinals); + } + + if ($start >= $stop) { + return $result; + } + + + // handle path cases that begin with a forward slash because they are easy to identify. + if ($ordinals[$result['current']] == c_base_ascii::SLASH_FORWARD) { + $this->p_rfc_string_is_uri_path($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + + + // check for query. + if ($ordinals[$result['current']] == c_base_ascii::QUESTION_MARK) { + // the first question mark is not recorded so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_query($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + + + // check for fragment. + if ($ordinals[$result['current']] == c_base_ascii::HASH) { + // only the first hash is supported in the fragment (and it is not recorded) so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_fragment($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + + return $result; + } + + + // handle fragment cases first because they are easy to identify. + if ($ordinals[$result['current']] == c_base_ascii::HASH) { + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + break; + } + + $code = $ordinals[$result['current']]; + + // the syntax for query is identical to fragment. + if (!$this->pr_rfc_char_is_query($code)) { + $result['invalid'] = TRUE; + break; + } + + $result['fragment'] .= $characters[$result['current']]; + } + unset($code); + + return $result; + } + + + $not_scheme = FALSE; + $not_authority = FALSE; + $not_path = FALSE; + $processed_string = ''; + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + break; + } + + $code = $ordinals[$result['current']]; + + if ($this->pr_rfc_char_is_alpha($code)) { + // allowed in: scheme, authority, path + } + elseif ($this->pr_rfc_char_is_digit($code)) { + // allowed in: scheme, authority, path + } + elseif ($code == c_base_ascii::COLON) { + $not_path = TRUE; + + if ($not_scheme) { + // must be an authority as per notes in the comments section of the function. + if ($not_authority) { + unset($not_scheme); + unset($not_authority); + unset($not_path); + unset($processed_string); + + $result['invalid'] = TRUE; + return $result; + } + } + else { + // must be an scheme as per notes in the comments section of the function. + $not_authority = TRUE; + } + } + elseif ($code == c_base_ascii::PLUS || $code == c_base_ascii::MINUS || $code == c_base_ascii::PERIOD) { + // allowed in: scheme, authority, path + } + elseif ($code == c_base_ascii::AT || $code == c_base_ascii::SLASH_FORWARD) { + // allowed in: authority, path + + $not_scheme = TRUE; + } + elseif ($this->pr_rfc_char_is_unreserved($code)) { + // allowed in: authority, path + + $not_scheme = TRUE; + } + elseif ($code == c_base_ascii::BRACKET_OPEN) { + // allowed in: authority + + $not_scheme = TRUE; + $not_path = TRUE; + } + else { + unset($not_scheme); + unset($not_authority); + unset($not_path); + unset($processed_string); + + $result['invalid'] = TRUE; + return $result; + } + + if (($not_scheme && $not_path) || ($not_scheme && $not_authority) || ($not_authority && $not_path)) { + break; + } + + $processed_string .= $characters[$result['current']]; + } + unset($code); + + if ($result['current'] >= $stop) { + unset($not_scheme); + unset($not_authority); + unset($not_path); + unset($processed_string); + + return $result; + } + + if ($not_authority && $not_path) { + unset($not_scheme); + unset($not_authority); + unset($not_path); + + $result['scheme'] = $processed_string; + unset($processed_string); + + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + // check to see if '/' immediately follows, if not then this is a urn. + $code = $ordinals[$result['current']]; + if ($code == c_base_ascii::SLASH_FORWARD) { + unset($code); + + // at this point it is known that this is a url instead of a urn. + $this->p_rfc_string_is_uri_path($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + + // check for query. + if ($ordinals[$result['current']] == c_base_ascii::QUESTION_MARK) { + // the first question mark is not recorded so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_query($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + + // check for fragment. + if ($ordinals[$result['current']] == c_base_ascii::HASH) { + // only the first hash is supported in the fragment (and it is not recorded) so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_fragment($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + + return $result; + } + unset($code); + + // process path argument and if a single ':' is found, then this is a urn. + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + unset($code); + + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + return $result; + } + + $code = $ordinals[$result['current']]; + + if ($code == c_base_ascii::HASH || $code == c_base_ascii::QUESTION_MARK) { + // found possible query or fragment. + $result['url'] = TRUE; + break; + } + elseif ($code == c_base_ascii::COLON) { + $result['url'] = FALSE; + } + elseif (!$this->pr_rfc_char_is_pchar($code)) { + unset($code); + + $result['invalid'] = TRUE; + return $result; + } + + $result['path'] .= $characters[$result['current']]; + } + unset($code); + + if ($result['current'] >= $stop) { + return $result; + } + + // check for query. + if ($ordinals[$result['current']] == c_base_ascii::QUESTION_MARK) { + // the first question mark is not recorded so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_query($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + + // check for fragment. + if ($ordinals[$result['current']] == c_base_ascii::HASH) { + // only the first hash is supported in the fragment (and it is not recorded) so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_fragment($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + + return $result; + } + elseif ($not_scheme && $not_path) { + unset($not_scheme); + unset($not_authority); + unset($not_path); + + $result['authority'] = $processed_string; + unset($processed_string); + + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + // check for authority. + $this->p_rfc_string_is_uri_authority($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + + // check for path. + $this->p_rfc_string_is_uri_path($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + + // check for query. + if ($ordinals[$result['current']] == c_base_ascii::QUESTION_MARK) { + // the first question mark is not recorded so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_query($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + + // check for fragment. + if ($ordinals[$result['current']] == c_base_ascii::HASH) { + // only the first hash is supported in the fragment (and it is not recorded) so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_fragment($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + } + elseif ($not_scheme && $not_authority) { + unset($not_scheme); + unset($not_authority); + unset($not_path); + + $result['path'] = $processed_string; + unset($processed_string); + + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + // check for path. + $this->p_rfc_string_is_uri_path($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + + // check for query. + if ($ordinals[$result['current']] == c_base_ascii::QUESTION_MARK) { + // the first question mark is not recorded so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_query($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + + // check for fragment. + if ($ordinals[$result['current']] == c_base_ascii::HASH) { + // only the first hash is supported in the fragment (and it is not recorded) so skip past it before validating the fragment. + $result['current']++; + if ($result['current'] >= $stop) { + return $result; + } + + $this->p_rfc_string_is_uri_fragment($ordinals, $characters, $stop, $result); + if ($result['invalid'] || $result['current'] >= $stop) { + return $result; + } + } + } + unset($not_scheme); + unset($not_authority); + unset($not_path); + unset($processed_string); + + $result['invalid'] = TRUE; + + return $result; + } + + /** + * Combine a uri array into a single uri. + * + * This does not validate the uri, it simply merges an array. + * + * foo://example.com:8042/over/there?name=ferret#nose + * \_/ \______________/\_________/ \_________/ \__/ + * | | | | | + * scheme authority path query fragment + * | _____________________|__ + * / \ / \ + * urn:example:animal:ferret:nose + * + * + * @param array $uri_array + * A url array with the following structure: + * - 'scheme': The protocol string. + * - 'authority': The domain string. + * - 'path': The path string. + * - 'query': An array of url arguments. + * - 'fragment': The id string. + * + * @return string|bool + * A combined url array on success. + * FALSE is returned on error. + * + * @see: self::pr_rfc_string_is_uri() + * @see: https://tools.ietf.org/html/rfc3986 + */ + protected function pr_rfc_string_combine_uri_array($uri_array) { + if (!$uri_array['url']) { + // both scheme and path are required for urn. + if (!isset($uri_array['scheme']) || !isset($uri_array['path'])) { + return FALSE; + } + + $combined .= $uri_array['scheme'] . ':' . $uri_array['path']; + + return $combined; + } + + $combined = NULL; + if (isset($uri_array['scheme'])) { + $combined .= $uri_array['scheme'] . ':'; + } + + if (isset($uri_array['authority'])) { + $combined .= $uri_array['authority']; + } + + if (isset($uri_array['path'])) { + $combined .= $uri_array['path']; + } + + if (!empty($uri_array['query'])) { + if (is_string($uri_array['query'])) { + $combined .= '?' . $uri_array['query']; + } + elseif (is_array($uri_array['query'])) { + $combined .= '?'; + + reset($uri_array['query']); + $query_name = key($uri_array['query']); + $query_value = $uri_array['query'][$query_name]; + unset($uri_array['query'][$query_name]); + + if (is_null($query_value)) { + $combined .= $query_name; + } + else { + $combined .= $query_name . '=' . $query_value; + } + + foreach ($uri_array['query'] as $query_name => $query_value) { + if (is_null($query_value)) { + $combined .= $query_name; + continue; + } + + $combined .= '&' . $query_name . '=' . $query_value; + } + unset($query_name); + unset($query_value); + } + } + + if (isset($uri_array['fragment'])) { + $combined .= '#' . $uri_array['fragment']; + } + + return $combined; + } + + /** + * Effectively unshift a value onto a given array with a specified index. + * + * The NULL key should be the first key in the weight. + * PHP does not provide a way to preserve keys when merging arrays nor does PHP provide a way to unshift a value onto an array with a specific key. + * + * @param $key + * The index name to unshift the value onto the array as. + * @param array $array + * The array to unshift onto. + */ + protected function pr_prepend_array_value($key, &$array) { + if (!array_key_exists($key, $array)) { + return; + } + + $value = $array[$key]; + unset($array[$key]); $new_array = array( $key => $value, @@ -2664,4 +3491,279 @@ abstract class c_base_rfc_string extends c_base_rfc_char { $array = $new_array; unset($new_array); } + + /** + * Helper function for pr_rfc_string_is_uri() to process: authority. + * + * @param array $ordinals + * An array of integers representing each character of the string. + * @param array $characters + * An array of characters representing the string. + * @param int $stop + * The position in the arrays to stop checking. + * @param array $result + * An array of return results used by the pr_rfc_string_is_uri(). + * + * @see: self::pr_rfc_string_is_uri() + */ + private function p_rfc_string_is_uri_authority(&$ordinals, &$characters, &$stop, &$result) { + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + unset($code); + + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + return $result; + } + + $code = $ordinals[$result['current']]; + + if ($code == c_base_ascii::PERCENT) { + // valid only if two hex digits immediately follow. + $result['current']++; + if ($result['current'] >= $stop) { + // this is invalid because it is cut off before 2 hex digits are required and none is found. + $result['invalid'] = TRUE; + break; + } + + $code = $ordinals[$result['current']]; + if (!$this->pr_rfc_char_is_hexdigit($ordinals[$result['current']])) { + $result['invalid'] = TRUE; + break; + } + + $result['current']++; + if ($result['current'] >= $stop) { + // this is invalid because it is cut off before 2 hex digits are required and only one is found. + $result['invalid'] = TRUE; + break; + } + + $code = $ordinals[$result['current']]; + if (!$this->pr_rfc_char_is_hexdigit($ordinals[$result['current']])) { + $result['invalid'] = TRUE; + break; + } + } + elseif ($code == c_base_ascii::AT || $code == c_base_ascii::COLON) { + // this is valid. + } + elseif ($code == c_base_ascii::BRACKET_OPEN || $code == c_base_ascii::BRACKET_CLOSE) { + // this is valid. + } + elseif ($this->pr_rfc_char_is_unreserved($code)) { + // this is valid. + } + elseif ($this->pr_rfc_char_is_sub_delims($code)) { + // this is valid. + } + else { + unset($code); + + $result['invalid'] = TRUE; + return $result; + } + + $result['authority'] .= $characters[$result['current']]; + } + unset($code); + } + + /** + * Helper function for pr_rfc_string_is_uri() to process: path. + * + * @param array $ordinals + * An array of integers representing each character of the string. + * @param array $characters + * An array of characters representing the string. + * @param int $stop + * The position in the arrays to stop checking. + * @param array $result + * An array of return results used by the pr_rfc_string_is_uri(). + * + * @see: self::pr_rfc_string_is_uri() + */ + private function p_rfc_string_is_uri_path(&$ordinals, &$characters, &$stop, &$result) { + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + unset($code); + + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + return $result; + } + + $code = $ordinals[$result['current']]; + + if ($code == c_base_ascii::HASH || $code == c_base_ascii::QUESTION_MARK) { + // found possible query or fragment. + break; + } + elseif ($code == c_base_ascii::SLASH_FORWARD) { + // this is valid. + } + elseif (!$this->pr_rfc_char_is_pchar($code)) { + unset($code); + + $result['invalid'] = TRUE; + return $result; + } + + $result['path'] .= $characters[$result['current']]; + } + unset($code); + } + + /** + * Helper function for pr_rfc_string_is_uri() to process: query. + * + * @param array $ordinals + * An array of integers representing each character of the string. + * @param array $characters + * An array of characters representing the string. + * @param int $stop + * The position in the arrays to stop checking. + * @param array $result + * An array of return results used by the pr_rfc_string_is_uri(). + * + * @see: self::pr_rfc_string_is_uri() + */ + private function p_rfc_string_is_uri_query(&$ordinals, &$characters, &$stop, &$result) { + $query_name = NULL; + $query_value = NULL; + $no_value = FALSE; + + $result['query'] = array(); + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + unset($code); + unset($query_name); + unset($query_value); + unset($no_value); + + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + return $result; + } + + $code = $ordinals[$result['current']]; + + if ($code == c_base_ascii::HASH) { + // hash is not part of the query but does mark the end of the query as it is the start of the fragment. + break; + } + elseif ($code == c_base_ascii::AMPERSAND) { + // The '&' designates a new name and value, separate each individual value inside the array. + $result['query'][$query_name] = $query_value; + + $query_name = NULL; + $query_value = NULL; + $no_value = FALSE; + + continue; + } + elseif ($code == c_base_ascii::EQUAL) { + // The '=' designates a value for the current name. + if ($no_value || is_null($query_name)) { + $query_name .= $characters[$result['current']]; + $no_value = TRUE; + continue; + } + + $query_value = ''; + continue; + } + elseif (!$this->pr_rfc_char_is_query($code)) { + unset($code); + unset($query_name); + unset($query_value); + unset($no_value); + + $result['invalid'] = TRUE; + return $result; + } + + if (is_null($query_value)) { + $query_name .= $characters[$result['current']]; + } + else { + $query_value .= $characters[$result['current']]; + } + } + unset($code); + unset($no_value); + + $result['query'][$query_name] = $query_value; + + unset($query_name); + unset($query_value); + } + + /** + * Helper function for pr_rfc_string_is_uri() to process: fragment. + * + * @param array $ordinals + * An array of integers representing each character of the string. + * @param array $characters + * An array of characters representing the string. + * @param int $stop + * The position in the arrays to stop checking. + * @param array $result + * An array of return results used by the pr_rfc_string_is_uri(). + * + * @see: self::pr_rfc_string_is_uri() + */ + private function p_rfc_string_is_uri_fragment(&$ordinals, &$characters, &$stop, &$result) { + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + unset($code); + + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + return $result; + } + + $code = $ordinals[$result['current']]; + + // the syntax for query is identical to fragment. + if (!$this->pr_rfc_char_is_query($code)) { + unset($code); + + $result['invalid'] = TRUE; + return $result; + } + + $result['fragment'] .= $characters[$result['current']]; + } + unset($code); + } + + /** + * Helper function for bypassing whitespaces. + * + * @param array $ordinals + * An array of integers representing each character of the string. + * @param array $characters + * An array of characters representing the string. + * @param int $stop + * The position in the arrays to stop checking. + * @param array $result + * An array of return results used by the pr_rfc_string_is_uri(). + * + * @see: self::pr_rfc_string_is_uri() + */ + private function p_rfc_string_skip_past_whitespace(&$ordinals, &$characters, &$stop, &$result) { + for (; $result['current'] < $stop; $result['current']++) { + if (!array_key_exists($result['current'], $ordinals) || !array_key_exists($result['current'], $characters)) { + // @fixme: should error be reported? do some debugging with this. + $result['invalid'] = TRUE; + break; + } + + if (!$this->pr_rfc_char_is_wsp($ordinals[$result['current']])) { + break; + } + } + } } diff --git a/examples/test.php b/examples/test.php index a5a76ad..c935ce0 100755 --- a/examples/test.php +++ b/examples/test.php @@ -349,7 +349,7 @@ $stuff['login'] = ''; } - $user_data = get_user_data($database, $session->get_name()->get_value_exact(), $ldap_data); + $user_data = get_user_data($database, $session->get_name()->get_value_exact()); $stuff['login'] .= ' - You are logged in as: ' . $session->get_name()->get_value_exact() . '
' . "\n"; $stuff['login'] .= ' - Your user id is: ' . $session->get_id_user()->get_value_exact() . '
' . "\n"; @@ -654,7 +654,7 @@ $connection_string = new c_base_connection_string(); $connection_string->set_host('127.0.0.1'); - $connection_string->set_port(5095); + $connection_string->set_port(5432); $connection_string->set_database('reservation'); $connection_string->set_user($username); $connection_string->set_password($password); @@ -1121,11 +1121,11 @@ unset($response); return FALSE; } - unset($response); // an integer is expected to be returned by the socket. $response_packet = unpack('C', $response); $response_value = (int) $response_packet[1]; + unset($response); // response codes as defined in the c source file: // 0 = no problems detected. diff --git a/program/reservation/index.php b/program/reservation/index.php index 113f265..d1d5b25 100644 --- a/program/reservation/index.php +++ b/program/reservation/index.php @@ -95,7 +95,8 @@ #$http->set_response_last_modified(strtotime('now')); #$http->set_response_expires(strtotime('+30 minutes')); $http->set_response_pragma('no-cache'); - #$http->set_response_vary('Date'); + $http->set_response_vary('Host'); + $http->set_response_vary('User-Agent'); #$http->set_response_warning('1234 This site is under active development.'); // finalize the content prior to sending headers to ensure header accuracy. -- 1.8.3.1