Merge branch 'master' into phpng
authorMichael Wallner <mike@php.net>
Fri, 12 Jun 2015 08:30:13 +0000 (10:30 +0200)
committerMichael Wallner <mike@php.net>
Fri, 12 Jun 2015 08:30:13 +0000 (10:30 +0200)
24 files changed:
.gitignore
check_package-xml.php
config9.m4
php_http.c
php_http_client_curl.c
php_http_env_response.c
php_http_info.h
php_http_message_parser.c
php_http_misc.h
php_http_params.c
php_http_params.h
php_http_url.c
reflection2php.php
tests/bug69076.phpt [new file with mode: 0644]
tests/bug69313.phpt [new file with mode: 0644]
tests/bug69357.phpt [new file with mode: 0644]
tests/client019.phpt
tests/client020.phpt
tests/client025.phpt
tests/helper/dump.inc [new file with mode: 0644]
tests/helper/proxy.inc
tests/helper/upload.inc [new file with mode: 0644]
tests/params016.phpt [new file with mode: 0644]
tests/params017.phpt [new file with mode: 0644]

index 93781ff39705687030eaada19d3b577665056d59..c78e2b840ffa57fa6efbf5c83ce721171942ea1f 100644 (file)
@@ -39,3 +39,4 @@ tests/*.php
 tests/*.sh
 lcov_data
 *~
+*.phar
index 4000054a16755258b51c3aabaf0ed7bcc473a805..32a1e734da067cb8a9ac93cd41ee785e1826f23f 100755 (executable)
@@ -36,6 +36,11 @@ if (($xml = simplexml_load_file($file))) {
                        }
                }
        }
+       foreach ($xml_files as $file) {
+               if (!file_exists($file)) {
+                       echo "Extraneous file $file\n";
+               }
+       }
 }
 
 ###
index bd5ba2f08feb59dc9ee852cdfff7d16d15fe5947..b82a3a77a316234bf1767175314ab392ba71288a 100644 (file)
@@ -11,7 +11,7 @@ PHP_ARG_WITH([http-libcurl-dir], [],
 PHP_ARG_WITH([http-libevent-dir], [],
 [  --with-http-libevent-dir[=DIR] HTTP: where to find libevent], $PHP_HTTP_LIBCURL_DIR, "")
 PHP_ARG_WITH([http-libidn-dir], [],
-[  --with-http-libidn-dir=[=DIR]  HTTP: where to find libidn], $PHP_HTTP_LIBCURL_DIR, "")
+[  --with-http-libidn-dir[=DIR]   HTTP: where to find libidn], $PHP_HTTP_LIBCURL_DIR, "")
 
 if test "$PHP_HTTP" != "no"; then
 
@@ -120,18 +120,70 @@ dnl ----
                        break;
                fi
        done
-       if test "x$IDNA_DIR" = "x"; then
-               AC_MSG_RESULT([not found])
-               case $host_os in
-               darwin*)
-                       AC_CHECK_HEADERS(unicode/uidna.h)
-                       PHP_CHECK_FUNC(uidna_IDNToASCII, icucore);;
-               esac
-       else
+       if test "x$IDNA_DIR" != "x"; then
                AC_MSG_RESULT([found in $IDNA_DIR])
                AC_DEFINE([PHP_HTTP_HAVE_IDN], [1], [Have libidn support])
                PHP_ADD_INCLUDE($IDNA_DIR/include)
                PHP_ADD_LIBRARY_WITH_PATH(idn, $IDNA_DIR/$PHP_LIBDIR, HTTP_SHARED_LIBADD)
+               AC_MSG_CHECKING([for libidn version])
+               IDNA_VER=$(pkg-config --version libidn 2>/dev/null || echo unknown)
+               AC_MSG_RESULT([$IDNA_VER])
+               AC_DEFINE_UNQUOTED([PHP_HTTP_LIBIDN_VERSION], "$IDNA_VER", [ ])
+       else
+               AC_MSG_RESULT([not found])
+               AC_MSG_CHECKING([for idn2.h])
+               IDNA_DIR=
+               for i in "$PHP_HTTP_LIBIDN_DIR" "$IDN_DIR" /usr/local /usr /opt; do
+                       if test -f "$i/include/idn2.h"; then
+                               IDNA_DIR=$i
+                               break;
+                       fi
+               done
+               if test "x$IDNA_DIR" != "x"; then
+                       AC_MSG_RESULT([found in $IDNA_DIR])
+                       AC_DEFINE([PHP_HTTP_HAVE_IDN2], [1], [Have libidn2 support])
+                       PHP_ADD_INCLUDE($IDNA_DIR/include)
+                       PHP_ADD_LIBRARY_WITH_PATH(idn2, $IDNA_DIR/$PHP_LIBDIR, HTTP_SHARED_LIBADD)
+                       AC_MSG_CHECKING([for libidn2 version])
+                       IDNA_VER=`$EGREP "define IDN2_VERSION " $IDNA_DIR/include/idn2.h | $SED -e's/^.*VERSION //g' -e 's/[[^0-9\.]]//g'`
+                       AC_MSG_RESULT([$IDNA_VER])
+                       AC_DEFINE_UNQUOTED([PHP_HTTP_LIBIDN2_VERSION], "$IDNA_VER", [ ])
+               else
+                       AC_MSG_RESULT([not found])
+                       AC_CHECK_HEADERS([unicode/uidna.h])
+                       case $host_os in
+                       darwin*)
+                               PHP_CHECK_FUNC(uidna_IDNToASCII, icucore);;
+                       *)
+                               AC_PATH_PROG(ICU_CONFIG, icu-config, no, [$PATH:/usr/local/bin])
+                               if test ! -x "$ICU_CONFIG"; then
+                                       ICU_CONFIG="icu-config"
+                               fi
+                               AC_MSG_CHECKING([for uidna_IDNToASCII])
+                               if ! test -x "$ICU_CONFIG"; then
+                                       ICU_CONFIG=icu-config
+                               fi
+                               if $ICU_CONFIG --exists 2>/dev/null >&2; then
+                                       save_LIBS=$LIBS
+                                       LIBS=$($ICU_CONFIG --ldflags)
+                                       AC_TRY_RUN([
+                                               #include <unicode/uidna.h>
+                                               int main(int argc, char *argv[]) {
+                                                       return uidna_IDNToASCII(0, 0, 0, 0, 0, 0, 0);
+                                               }
+                                       ], [
+                                               AC_MSG_RESULT([yes])
+                                               AC_DEFINE([HAVE_UIDNA_IDNTOASCII], [1], [ ])
+                                               LIBS=$save_LIBS
+                                               PHP_EVAL_LIBLINE(`$ICU_CONFIG --ldflags`, HTTP_SHARED_LIBADD)
+                                       ], [
+                                               LIBS=$save_LIBS
+                                               AC_MSG_RESULT([no])
+                                       ])
+                               fi
+                               ;;
+                       esac
+               fi
        fi
 
 dnl ----
index d430e1f13538fce89cfb75a8005e18a0c5e6d6ad..6d4384d71133a22095cef60e3b5a1237057b7ac8 100644 (file)
 #              endif
 #      endif
 #endif
-#if PHP_HTTP_HAVE_SERF
-#      include <serf.h>
+#if PHP_HTTP_HAVE_IDN2
+#      include <idn2.h>
+#elif PHP_HTTP_HAVE_IDN
+#      include <idna.h>
 #endif
 
 ZEND_DECLARE_MODULE_GLOBALS(php_http);
@@ -220,6 +222,12 @@ PHP_MINFO_FUNCTION(http)
        php_info_print_table_row(3, "libevent", "disabled", "disabled");
 #endif
 
+#if PHP_HTTP_HAVE_IDN2
+       php_info_print_table_row(3, "libidn2 (IDNA2008)", IDN2_VERSION, idn2_check_version(NULL));
+#elif PHP_HTTP_HAVE_IDN
+       php_info_print_table_row(3, "libidn (IDNA2003)", PHP_HTTP_LIBIDN_VERSION, "unknown");
+#endif
+
        php_info_print_table_end();
        
        DISPLAY_INI_ENTRIES();
index ac464abc7a72ede81d764d4b9c0bfb36f94c6721..9b44aa00d1129b506d86787393e36735fa4f7749 100644 (file)
@@ -611,7 +611,14 @@ static php_http_message_t *php_http_curlm_responseparser(php_http_client_curl_ha
 
        response = php_http_message_init(NULL, 0, h->response.body TSRMLS_CC);
        php_http_header_parser_init(&parser TSRMLS_CC);
-       php_http_header_parser_parse(&parser, &h->response.headers, PHP_HTTP_HEADER_PARSER_CLEANUP, &response->hdrs, (php_http_info_callback_t) php_http_message_info_callback, (void *) &response);
+       while (h->response.headers.used) {
+               php_http_header_parser_state_t st = php_http_header_parser_parse(&parser,
+                               &h->response.headers, PHP_HTTP_HEADER_PARSER_CLEANUP, &response->hdrs,
+                               (php_http_info_callback_t) php_http_message_info_callback, (void *) &response);
+               if (PHP_HTTP_HEADER_PARSER_STATE_FAILURE == st) {
+                       break;
+               }
+       }
        php_http_header_parser_dtor(&parser);
 
        /* move body to right message */
@@ -621,6 +628,7 @@ static php_http_message_t *php_http_curlm_responseparser(php_http_client_curl_ha
                while (ptr->parent) {
                        ptr = ptr->parent;
                }
+               php_http_message_body_free(&response->body);
                response->body = ptr->body;
                ptr->body = NULL;
        }
@@ -653,7 +661,8 @@ static php_http_message_t *php_http_curlm_responseparser(php_http_client_curl_ha
 
 static void php_http_curlm_responsehandler(php_http_client_t *context)
 {
-       int remaining = 0;
+       int err_count = 0, remaining = 0;
+       php_http_curle_storage_t *st, *err = NULL;
        php_http_client_enqueue_t *enqueue;
        php_http_client_curl_t *curl = context->ctx;
 
@@ -662,8 +671,18 @@ static void php_http_curlm_responsehandler(php_http_client_t *context)
 
                if (msg && CURLMSG_DONE == msg->msg) {
                        if (CURLE_OK != msg->data.result) {
-                               php_http_curle_storage_t *st = php_http_curle_get_storage(msg->easy_handle);
-                               php_error_docref(NULL, E_WARNING, "%s; %s (%s)", curl_easy_strerror(st->errorcode = msg->data.result), STR_PTR(st->errorbuffer), STR_PTR(st->url));
+                               st = php_http_curle_get_storage(msg->easy_handle);
+                               st->errorcode = msg->data.result;
+
+                               /* defer the warnings/exceptions, so the callback is still called for this request */
+                               if (!err) {
+                                       err = ecalloc(remaining + 1, sizeof(*err));
+                               }
+                               memcpy(&err[err_count], st, sizeof(*st));
+                               if (st->url) {
+                                       err[err_count].url = estrdup(st->url);
+                               }
+                               err_count++;
                        }
 
                        if ((enqueue = php_http_client_enqueued(context, msg->easy_handle, compare_queue))) {
@@ -677,6 +696,19 @@ static void php_http_curlm_responsehandler(php_http_client_t *context)
                        }
                }
        } while (remaining);
+
+       if (err_count) {
+               int i = 0;
+
+               do {
+                       php_error_docref(NULL TSRMLS_CC, E_WARNING, "%s; %s (%s)", curl_easy_strerror(err[i].errorcode), err[i].errorbuffer, STR_PTR(err[i].url));
+                       if (err[i].url) {
+                               efree(err[i].url);
+                       }
+               } while (++i < err_count);
+
+               efree(err);
+       }
 }
 
 #if PHP_HTTP_HAVE_EVENT
@@ -1910,35 +1942,6 @@ static ZEND_RESULT_CODE php_http_client_curl_handler_prepare(php_http_client_cur
        php_http_url_to_string(PHP_HTTP_INFO(msg).request.url, &storage->url, NULL, 1);
        curl_easy_setopt(curl->handle, CURLOPT_URL, storage->url);
 
-       /* request method */
-       switch (php_http_select_str(PHP_HTTP_INFO(msg).request.method, 4, "GET", "HEAD", "POST", "PUT")) {
-               case 0:
-                       curl_easy_setopt(curl->handle, CURLOPT_HTTPGET, 1L);
-                       break;
-
-               case 1:
-                       curl_easy_setopt(curl->handle, CURLOPT_NOBODY, 1L);
-                       break;
-
-               case 2:
-                       curl_easy_setopt(curl->handle, CURLOPT_POST, 1L);
-                       break;
-
-               case 3:
-                       curl_easy_setopt(curl->handle, CURLOPT_UPLOAD, 1L);
-                       break;
-
-               default: {
-                       if (PHP_HTTP_INFO(msg).request.method) {
-                               curl_easy_setopt(curl->handle, CURLOPT_CUSTOMREQUEST, PHP_HTTP_INFO(msg).request.method);
-                       } else {
-                               php_error_docref(NULL, E_WARNING, "Cannot use empty request method");
-                               return FAILURE;
-                       }
-                       break;
-               }
-       }
-
        /* apply options */
        php_http_options_apply(&php_http_curle_options, enqueue->options, curl);
 
@@ -1989,6 +1992,7 @@ static ZEND_RESULT_CODE php_http_client_curl_handler_prepare(php_http_client_cur
                curl_easy_setopt(curl->handle, CURLOPT_READDATA, msg->body);
                curl_easy_setopt(curl->handle, CURLOPT_INFILESIZE, body_size);
                curl_easy_setopt(curl->handle, CURLOPT_POSTFIELDSIZE, body_size);
+               curl_easy_setopt(curl->handle, CURLOPT_POST, 1L);
        } else {
                curl_easy_setopt(curl->handle, CURLOPT_SEEKDATA, NULL);
                curl_easy_setopt(curl->handle, CURLOPT_READDATA, NULL);
@@ -1996,6 +2000,29 @@ static ZEND_RESULT_CODE php_http_client_curl_handler_prepare(php_http_client_cur
                curl_easy_setopt(curl->handle, CURLOPT_POSTFIELDSIZE, 0L);
        }
 
+       /*
+        * Always use CUSTOMREQUEST, else curl won't send any request body for GET etc.
+        * See e.g. bug #69313.
+        *
+        * Here's what curl does:
+        * - CURLOPT_HTTPGET: ignore request body
+        * - CURLOPT_UPLOAD: set "Expect: 100-continue" header
+        * - CURLOPT_POST: set "Content-Type: application/x-www-form-urlencoded" header
+        * Now select the least bad.
+        *
+        * See also https://tools.ietf.org/html/rfc7231#section-5.1.1
+        */
+       if (PHP_HTTP_INFO(msg).request.method) {
+               if (!strcasecmp("PUT", PHP_HTTP_INFO(msg).request.method)) {
+                       curl_easy_setopt(curl->handle, CURLOPT_UPLOAD, 1L);
+               } else {
+                       curl_easy_setopt(curl->handle, CURLOPT_CUSTOMREQUEST, PHP_HTTP_INFO(msg).request.method);
+               }
+       } else {
+               php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot use empty request method");
+               return FAILURE;
+       }
+
        return SUCCESS;
 }
 
@@ -2309,11 +2336,11 @@ static ZEND_RESULT_CODE php_http_client_curl_exec(php_http_client_t *h)
                                php_error_docref(NULL, E_ERROR, "Error in event_base_dispatch()");
                                return FAILURE;
                        }
-               } while (curl->unfinished);
+               } while (curl->unfinished && !EG(exception));
        } else
 #endif
        {
-               while (php_http_client_curl_once(h)) {
+               while (php_http_client_curl_once(h) && !EG(exception)) {
                        if (SUCCESS != php_http_client_curl_wait(h, NULL)) {
 #ifdef PHP_WIN32
                                /* see http://msdn.microsoft.com/library/en-us/winsock/winsock/windows_sockets_error_codes_2.asp */
index 5b4682084278d460c6672caafa745177fd289c9a..557bb1088b07849d6ac56855d62fb1db77dc05ed 100644 (file)
@@ -207,7 +207,9 @@ php_http_cache_status_t php_http_env_is_response_cached_by_last_modified(zval *o
 
 static zend_bool php_http_env_response_is_cacheable(php_http_env_response_t *r, php_http_message_t *request)
 {
-       if (r->ops->get_status(r) >= 400) {
+       long status = r->ops->get_status(r);
+
+       if (status && status / 100 != 2) {
                return 0;
        }
 
@@ -1157,7 +1159,12 @@ static PHP_METHOD(HttpEnvResponse, __invoke)
                PHP_HTTP_ENV_RESPONSE_OBJECT_INIT(obj);
 
                php_http_message_object_init_body_object(obj);
-               php_http_message_body_append(obj->message->body, ob_str, ob_len);
+
+               if (ob_flags & PHP_OUTPUT_HANDLER_CLEAN) {
+                       php_stream_truncate_set_size(php_http_message_body_stream(obj->message->body), 0);
+               } else {
+                       php_http_message_body_append(obj->message->body, ob_str, ob_len);
+               }
                RETURN_TRUE;
        }
 }
index b771c5cc40912cf0e0798e6f5119e8e18dc85b61..8a52ee2df19adf543a89e0be0b6adab47e42ed8a 100644 (file)
@@ -41,6 +41,8 @@ typedef struct php_http_info_data {
        php_http_version_t version;
 } php_http_info_data_t;
 
+#undef PHP_HTTP_REQUEST
+#undef PHP_HTTP_RESPONSE
 typedef enum php_http_info_type {
        PHP_HTTP_NONE = 0,
        PHP_HTTP_REQUEST,
index 7bf9a164f5a14cad90d42617cfb114fc7455ab80..92c8e4073aad391bd5f25bb9c2be32605167af88 100644 (file)
@@ -59,19 +59,21 @@ php_http_message_parser_t *php_http_message_parser_init(php_http_message_parser_
 
 php_http_message_parser_state_t php_http_message_parser_state_push(php_http_message_parser_t *parser, unsigned argc, ...)
 {
-       php_http_message_parser_state_t state;
+       php_http_message_parser_state_t state = PHP_HTTP_MESSAGE_PARSER_STATE_FAILURE;
        va_list va_args;
        unsigned i;
 
-       /* short circuit */
-       ZEND_PTR_STACK_RESIZE_IF_NEEDED((&parser->stack), argc);
+       if (argc > 0) {
+               /* short circuit */
+               ZEND_PTR_STACK_RESIZE_IF_NEEDED((&parser->stack), argc);
 
-       va_start(va_args, argc);
-       for (i = 0; i < argc; ++i) {
-               state  = va_arg(va_args, php_http_message_parser_state_t);
-               zend_ptr_stack_push(&parser->stack, (void *) state);
+               va_start(va_args, argc);
+               for (i = 0; i < argc; ++i) {
+                       state  = va_arg(va_args, php_http_message_parser_state_t);
+                       zend_ptr_stack_push(&parser->stack, (void *) state);
+               }
+               va_end(va_args);
        }
-       va_end(va_args);
 
        return state;
 }
index 421c9912cf57082283554c2c4ca759c82a098c4e..36f4020359877a501068eaa5dc359d67ed538b0c 100644 (file)
@@ -139,6 +139,11 @@ static inline const char *php_http_locate_bin_eol(const char *bin, size_t len, i
 
 /* ZEND */
 
+#ifdef PHP_DEBUG
+#      undef  HASH_OF
+#      define HASH_OF(p) ((HashTable*)(Z_TYPE_P(p)==IS_ARRAY ? Z_ARRVAL_P(p) : ((Z_TYPE_P(p)==IS_OBJECT ? Z_OBJ_HT_P(p)->get_properties((p)) : NULL))))
+#endif
+
 static inline void *PHP_HTTP_OBJ(zend_object *zo, zval *zv)
 {
        if (!zo) {
index 6e5dd4a0df442602ca6cd52e882d2a9231fc7086..ced2507aea8e470ebbfe7e7fc0585ab81f97b43e 100644 (file)
@@ -63,9 +63,27 @@ static inline void sanitize_escaped(zval *zv)
        php_stripcslashes(Z_STR_P(zv));
 }
 
-static inline void prepare_escaped(zval *zv)
+static inline void quote_string(zend_string **zs, zend_bool force)
 {
-       if (Z_TYPE_P(zv) == IS_STRING) {
+       int len = (*zs)->len;
+
+       *zs = php_addcslashes(*zs, 1, ZEND_STRL("\0..\37\173\\\""));
+
+       if (force || len != (*zs)->len || strpbrk((*zs)->val, "()<>@,;:\"[]?={} ")) {
+               int len = (*zs)->len + 2;
+
+               *zs = zend_string_extend(*zs, len, 0);
+
+               memmove(&(*zs)->val[1], (*zs)->val, (*zs)->len);
+               (*zs)->val[0] = '"';
+               (*zs)->val[len-1] = '"';
+               (*zs)->val[len] = '\0';
+
+               zend_string_forget_hash_val(*zs);
+       }
+}
+
+/*     if (Z_TYPE_P(zv) == IS_STRING) {
                size_t len = Z_STRLEN_P(zv);
                zend_string *stripped = php_addcslashes(Z_STR_P(zv), 0,
                                ZEND_STRL("\0..\37\173\\\""));
@@ -86,6 +104,12 @@ static inline void prepare_escaped(zval *zv)
                        zval_dtor(zv);
                        ZVAL_STR(zv, stripped);
                }
+*/
+
+static inline void prepare_escaped(zval *zv)
+{
+       if (Z_TYPE_P(zv) == IS_STRING) {
+               quote_string(&Z_STR_P(zv), 0);
        } else {
                zval_dtor(zv);
                ZVAL_EMPTY_STRING(zv);
@@ -291,6 +315,23 @@ static inline void sanitize_rfc5987(zval *zv, char **language, zend_bool *latin1
        }
 }
 
+static inline void sanitize_rfc5988(char *str, size_t len, zval *zv TSRMLS_DC)
+{
+       zend_string *zs = zend_string_init(str, len, 0);
+
+       zval_dtor(zv);
+       ZVAL_STR(zv, php_trim(zs, " ><", 3, 3));
+       zend_string_release(zs);
+}
+
+static inline void prepare_rfc5988(zval *zv TSRMLS_DC)
+{
+       if (Z_TYPE_P(zv) != IS_STRING) {
+               zval_dtor(zv);
+               ZVAL_EMPTY_STRING(zv);
+       }
+}
+
 static void utf8encode(zval *zv)
 {
        size_t pos, len = 0;
@@ -363,7 +404,11 @@ static inline void prepare_key(unsigned flags, char *old_key, size_t old_len, ch
        }
 
        if (flags & PHP_HTTP_PARAMS_ESCAPED) {
-               prepare_escaped(&zv);
+               if (flags & PHP_HTTP_PARAMS_RFC5988) {
+                       prepare_rfc5988(&zv);
+               } else {
+                       prepare_escaped(&zv);
+               }
        }
 
        *new_key = estrndup(Z_STRVAL(zv), Z_STRLEN(zv));
@@ -544,12 +589,16 @@ static void push_param(HashTable *params, php_http_params_state_t *state, const
                        zend_bool rfc5987 = 0;
 
                        ZVAL_NULL(&key);
-                       sanitize_key(opts->flags, state->param.str, state->param.len, &key, &rfc5987);
-                       state->rfc5987 = rfc5987;
+                       if (opts->flags & PHP_HTTP_PARAMS_RFC5988) {
+                               sanitize_rfc5988(state->param.str, state->param.len, &key);
+                       } else {
+                               sanitize_key(opts->flags, state->param.str, state->param.len, &key, &rfc5987);
+                               state->rfc5987 = rfc5987;
+                       }
                        if (Z_TYPE(key) == IS_ARRAY) {
                                merge_param(params, &key, &state->current.val, &state->current.args);
                        } else if (Z_TYPE(key) == IS_STRING && Z_STRLEN(key)) {
-                               //array_init_size(&prm, 2);
+                               // FIXME: array_init_size(&prm, 2);
                                array_init(&prm);
 
                                if (!Z_ISUNDEF(opts->defval)) {
@@ -563,7 +612,7 @@ static void push_param(HashTable *params, php_http_params_state_t *state, const
                                } else {
                                        state->current.val = zend_hash_str_update(Z_ARRVAL(prm), "value", lenof("value"), &val);
                                }
-                               //array_init_size(&arg, 3);
+                               // FIXME: array_init_size(&arg, 3);
                                array_init(&arg);
                                state->current.args = zend_hash_str_update(Z_ARRVAL(prm), "arguments", lenof("arguments"), &arg);
                                state->current.param = zend_symtable_str_update(params, Z_STRVAL(key), Z_STRLEN(key), &prm);
@@ -623,7 +672,13 @@ HashTable *php_http_params_parse(HashTable *params, const php_http_params_opts_t
        }
 
        while (state.input.len) {
-               if (*state.input.str == '"' && !state.escape) {
+               if ((opts->flags & PHP_HTTP_PARAMS_RFC5988) && !state.arg.str) {
+                       if (*state.input.str == '<') {
+                               state.quotes = 1;
+                       } else if (*state.input.str == '>') {
+                               state.quotes = 0;
+                       }
+               } else if (*state.input.str == '"' && !state.escape) {
                        state.quotes = !state.quotes;
                } else {
                        state.escape = (*state.input.str == '\\');
@@ -735,6 +790,33 @@ static inline void shift_rfc5987(php_http_buffer_t *buf, zval *zvalue, const cha
        }
 }
 
+static inline void shift_rfc5988(php_http_buffer_t *buf, char *key_str, size_t key_len, const char *ass, size_t asl, unsigned flags)
+{
+       char *str;
+       size_t len;
+
+       if (buf->used) {
+               php_http_buffer_append(buf, ass, asl);
+       }
+
+       prepare_key(flags, key_str, key_len, &str, &len);
+       php_http_buffer_appends(buf, "<");
+       php_http_buffer_append(buf, str, len);
+       php_http_buffer_appends(buf, ">");
+       efree(str);
+}
+
+static inline void shift_rfc5988_val(php_http_buffer_t *buf, zval *zv, const char *vss, size_t vsl, unsigned flags)
+{
+       zend_string *zs = zval_get_string(zv);
+
+       quote_string(&zs, 1);
+       php_http_buffer_append(buf, vss, vsl);
+       php_http_buffer_append(buf, zs->val, zs->len);
+
+       zend_string_release(zs);
+}
+
 static inline void shift_val(php_http_buffer_t *buf, zval *zvalue, const char *vss, size_t vsl, unsigned flags)
 {
        zval tmp;
@@ -788,6 +870,21 @@ static void shift_arg(php_http_buffer_t *buf, char *key_str, size_t key_len, zva
                ZEND_HASH_FOREACH_END();
        } else {
                shift_key(buf, key_str, key_len, ass, asl, flags);
+
+               if (flags & PHP_HTTP_PARAMS_RFC5988) {
+                       switch (key_len) {
+                       case lenof("rel"):
+                       case lenof("title"):
+                       case lenof("anchor"):
+                               /* some args must be quoted */
+                               if (0 <= php_http_select_str(key_str, 3, "rel", "title", "anchor")) {
+                                       shift_rfc5988_val(buf, zvalue, vss, vsl, flags);
+                                       return;
+                               }
+                               break;
+                       }
+               }
+
                shift_val(buf, zvalue, vss, vsl, flags);
        }
 }
@@ -808,6 +905,11 @@ static void shift_param(php_http_buffer_t *buf, char *key_str, size_t key_len, z
                }
        } else {
                shift_key(buf, key_str, key_len, pss, psl, flags);
+               if (flags & PHP_HTTP_PARAMS_RFC5988) {
+                       shift_rfc5988(buf, key_str, key_len, pss, psl, flags);
+               } else {
+                       shift_key(buf, key_str, key_len, pss, psl, flags);
+               }
                shift_val(buf, zvalue, vss, vsl, flags);
        }
 }
@@ -1201,6 +1303,7 @@ PHP_MINIT_FUNCTION(http_params)
        zend_declare_class_constant_long(php_http_params_class_entry, ZEND_STRL("PARSE_URLENCODED"), PHP_HTTP_PARAMS_URLENCODED);
        zend_declare_class_constant_long(php_http_params_class_entry, ZEND_STRL("PARSE_DIMENSION"), PHP_HTTP_PARAMS_DIMENSION);
        zend_declare_class_constant_long(php_http_params_class_entry, ZEND_STRL("PARSE_RFC5987"), PHP_HTTP_PARAMS_RFC5987);
+       zend_declare_class_constant_long(php_http_params_class_entry, ZEND_STRL("PARSE_RFC5988"), PHP_HTTP_PARAMS_RFC5988);
        zend_declare_class_constant_long(php_http_params_class_entry, ZEND_STRL("PARSE_DEFAULT"), PHP_HTTP_PARAMS_DEFAULT);
        zend_declare_class_constant_long(php_http_params_class_entry, ZEND_STRL("PARSE_QUERY"), PHP_HTTP_PARAMS_QUERY);
 
index 1803c8400e9b536f17b0650689cea82efd50c766..b8892101e15d28bc83d7590b34d6e3080e6127ab 100644 (file)
@@ -23,6 +23,7 @@ typedef struct php_http_params_token {
 #define PHP_HTTP_PARAMS_URLENCODED     0x04
 #define PHP_HTTP_PARAMS_DIMENSION      0x08
 #define PHP_HTTP_PARAMS_RFC5987                0x10
+#define PHP_HTTP_PARAMS_RFC5988                0x20
 #define PHP_HTTP_PARAMS_QUERY          (PHP_HTTP_PARAMS_URLENCODED|PHP_HTTP_PARAMS_DIMENSION)
 #define PHP_HTTP_PARAMS_DEFAULT                (PHP_HTTP_PARAMS_ESCAPED|PHP_HTTP_PARAMS_RFC5987)
 
index 5a267d898ee086fb3be58638e42c5d49f37cf840..18ac182d73f6088391b8a181acda1dff4553c7a1 100644 (file)
@@ -12,7 +12,9 @@
 
 #include "php_http_api.h"
 
-#ifdef PHP_HTTP_HAVE_IDN
+#if PHP_HTTP_HAVE_IDN2
+#      include <idn2.h>
+#elif PHP_HTTP_HAVE_IDN
 #      include <idna.h>
 #endif
 
@@ -567,8 +569,8 @@ HashTable *php_http_url_to_struct(const php_http_url_t *url, zval *strct)
 
 ZEND_RESULT_CODE php_http_url_encode_hash(HashTable *hash, const char *pre_encoded_str, size_t pre_encoded_len, char **encoded_str, size_t *encoded_len)
 {
-       const char *arg_sep_str;
-       size_t arg_sep_len;
+       const char *arg_sep_str = "&";
+       size_t arg_sep_len = 1;
        php_http_buffer_t *qstr = php_http_buffer_new();
 
        php_http_url_argsep(&arg_sep_str, &arg_sep_len);
@@ -837,7 +839,7 @@ static ZEND_RESULT_CODE parse_userinfo(struct parse_state *state, const char *pt
 
 #if defined(PHP_WIN32) || defined(HAVE_UIDNA_IDNTOASCII)
 typedef size_t (*parse_mb_func)(unsigned *wc, const char *ptr, const char *end);
-static ZEND_RESULT_CODE to_utf16(parse_mb_func fn, const char *u8, uint16_t **u16, size_t *len)
+static ZEND_RESULT_CODE to_utf16(parse_mb_func fn, const char *u8, uint16_t **u16, size_t *len TSRMLS_DC)
 {
        size_t offset = 0, u8_len = strlen(u8);
 
@@ -880,7 +882,33 @@ static ZEND_RESULT_CODE to_utf16(parse_mb_func fn, const char *u8, uint16_t **u1
 #      define MAXHOSTNAMELEN 256
 #endif
 
-#ifdef PHP_HTTP_HAVE_IDN
+#if PHP_HTTP_HAVE_IDN2
+static ZEND_RESULT_CODE parse_idn2(struct parse_state *state, size_t prev_len)
+{
+       char *idn = NULL;
+       int rv = -1;
+       TSRMLS_FETCH_FROM_CTX(state->ts);
+
+       if (state->flags & PHP_HTTP_URL_PARSE_MBUTF8) {
+               rv = idn2_lookup_u8((const unsigned char *) state->url.host, (unsigned char **) &idn, IDN2_NFC_INPUT);
+       }
+#      ifdef PHP_HTTP_HAVE_WCHAR
+       else if (state->flags & PHP_HTTP_URL_PARSE_MBLOC) {
+               rv = idn2_lookup_ul(state->url.host, &idn, 0);
+       }
+#      endif
+       if (rv != IDN2_OK) {
+               php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to parse IDN; %s", idn2_strerror(rv));
+               return FAILURE;
+       } else {
+               size_t idnlen = strlen(idn);
+               memcpy(state->url.host, idn, idnlen + 1);
+               free(idn);
+               state->offset += idnlen - prev_len;
+               return SUCCESS;
+       }
+}
+#elif PHP_HTTP_HAVE_IDN
 static ZEND_RESULT_CODE parse_idn(struct parse_state *state, size_t prev_len)
 {
        char *idn = NULL;
@@ -924,12 +952,12 @@ static ZEND_RESULT_CODE parse_uidn(struct parse_state *state)
        TSRMLS_FETCH_FROM_CTX(state->ts);
 
        if (state->flags & PHP_HTTP_URL_PARSE_MBUTF8) {
-               if (SUCCESS != to_utf16(parse_mb_utf8, state->url.host, &uhost_str, &uhost_len)) {
+               if (SUCCESS != to_utf16(parse_mb_utf8, state->url.host, &uhost_str, &uhost_len TSRMLS_CC)) {
                        return FAILURE;
                }
 #ifdef PHP_HTTP_HAVE_WCHAR
        } else if (state->flags & PHP_HTTP_URL_PARSE_MBLOC) {
-               if (SUCCESS != to_utf16(parse_mb_loc, state->url.host, &uhost_str, &uhost_len)) {
+               if (SUCCESS != to_utf16(parse_mb_loc, state->url.host, &uhost_str, &uhost_len TSRMLS_CC)) {
                        return FAILURE;
                }
 #endif
@@ -1113,7 +1141,9 @@ static ZEND_RESULT_CODE parse_hostinfo(struct parse_state *state, const char *pt
        }
 
        if (state->flags & PHP_HTTP_URL_PARSE_TOIDN) {
-#ifdef PHP_HTTP_HAVE_IDN
+#if PHP_HTTP_HAVE_IDN2
+               return parse_idn2(state, len);
+#elif PHP_HTTP_HAVE_IDN
                return parse_idn(state, len);
 #endif
 #ifdef HAVE_UIDNA_IDNTOASCII
@@ -1245,7 +1275,7 @@ static const char *parse_query(struct parse_state *state)
        tmp = ++state->ptr;
        state->url.query = &state->buffer[state->offset];
 
-       do {
+       while (state->ptr < state->end) {
                switch (*state->ptr) {
                case '#':
                        goto done;
@@ -1297,7 +1327,9 @@ static const char *parse_query(struct parse_state *state)
                        }
                        state->ptr += mb - 1;
                }
-       } while (++state->ptr < state->end);
+
+               ++state->ptr;
+       }
 
        done:
        state->buffer[state->offset++] = 0;
@@ -1541,7 +1573,7 @@ ZEND_END_ARG_INFO();
 PHP_METHOD(HttpUrl, mod)
 {
        zval *new_url = NULL;
-       zend_long flags = PHP_HTTP_URL_JOIN_PATH | PHP_HTTP_URL_JOIN_QUERY;
+       zend_long flags = PHP_HTTP_URL_JOIN_PATH | PHP_HTTP_URL_JOIN_QUERY | PHP_HTTP_URL_SANITIZE_PATH;
        zend_error_handling zeh;
 
        php_http_expect(SUCCESS == zend_parse_parameters(ZEND_NUM_ARGS(), "z!|l", &new_url, &flags), invalid_arg, return);
@@ -1656,7 +1688,7 @@ PHP_MINIT_FUNCTION(http_url)
        zend_declare_class_constant_long(php_http_url_class_entry, ZEND_STRL("PARSE_MBLOC"), PHP_HTTP_URL_PARSE_MBLOC);
 #endif
        zend_declare_class_constant_long(php_http_url_class_entry, ZEND_STRL("PARSE_MBUTF8"), PHP_HTTP_URL_PARSE_MBUTF8);
-#if defined(PHP_HTTP_HAVE_IDN) || defined(HAVE_UIDNA_IDNTOASCII)
+#if defined(PHP_HTTP_HAVE_IDN2) || defined(PHP_HTTP_HAVE_IDN) || defined(HAVE_UIDNA_IDNTOASCII)
        zend_declare_class_constant_long(php_http_url_class_entry, ZEND_STRL("PARSE_TOIDN"), PHP_HTTP_URL_PARSE_TOIDN);
 #endif
        zend_declare_class_constant_long(php_http_url_class_entry, ZEND_STRL("PARSE_TOPCT"), PHP_HTTP_URL_PARSE_TOPCT);
index e486012037fde36f229a296b06431137e7277a49..20a1f0fdd853b35d459090e1a43f33a3e4d6cbf2 100755 (executable)
@@ -98,11 +98,11 @@ foreach ($namespaces as $ns) {
             fprintf($out, "\n\tfunction %s(", $fn);
             $ps = array();
             foreach ($f->getParameters() as $p) {
-                $p1 = sfprintf($out, "%s%s\$%s", t($p), 
-                        $p->isPassedByReference()?"&":"", $p->getName());
+                $p1 = sprintf("%s%s\$%s", t($p), 
+                        $p->isPassedByReference()?"&":"", trim($p->getName(), "\""));
                 if ($p->isOptional()) {
                     if ($p->isDefaultValueAvailable()) {
-                        $p1 .= sfprintf($out, " = %s", 
+                        $p1 .= sprintf(" = %s", 
                                 var_export($p->getDefaultValue(), true));
                     } elseif (!($p->isArray() || $p->getClass()) || $p->allowsNull()) {
                         $p1 .= " = NULL";
diff --git a/tests/bug69076.phpt b/tests/bug69076.phpt
new file mode 100644 (file)
index 0000000..cd64958
--- /dev/null
@@ -0,0 +1,17 @@
+--TEST--
+Bug #69076 (URL parsing throws exception on empty query string)
+--SKIPIF--
+<?php 
+include "skipif.inc";
+?>
+--FILE--
+<?php 
+echo "Test\n";
+echo new http\Url("http://foo.bar/?");
+?>
+
+===DONE===
+--EXPECT--
+Test
+http://foo.bar/
+===DONE===
diff --git a/tests/bug69313.phpt b/tests/bug69313.phpt
new file mode 100644 (file)
index 0000000..c1d56ef
--- /dev/null
@@ -0,0 +1,46 @@
+--TEST--
+Bug #69313 (http\Client doesn't send GET body)
+--SKIPIF--
+<?php
+include "skipif.inc";
+skip_client_test();
+?>
+--FILE--
+<?php
+
+include "helper/dump.inc";
+include "helper/server.inc";
+
+echo "Test\n";
+
+server("proxy.inc", function($port, $stdin, $stdout, $stderr) {
+       $request = new http\Client\Request("GET", "http://localhost:$port/");
+       $request->setHeader("Content-Type", "text/plain");
+       $request->getBody()->append("foo");
+       $client = new http\Client();
+       $client->enqueue($request);
+       $client->send();
+       dump_message(null, $client->getResponse());
+});
+
+?>
+
+Done
+--EXPECTF--
+Test
+HTTP/1.1 200 OK
+Accept-Ranges: bytes
+Content-Length: %d
+Etag: "%s"
+X-Original-Transfer-Encoding: chunked
+
+GET / HTTP/1.1
+Accept: */*
+Content-Length: 3
+Content-Type: text/plain
+Host: localhost:%d
+User-Agent: %s
+X-Original-Content-Length: 3
+
+foo
+Done
diff --git a/tests/bug69357.phpt b/tests/bug69357.phpt
new file mode 100644 (file)
index 0000000..ac93baf
--- /dev/null
@@ -0,0 +1,41 @@
+--TEST--
+Bug #69357 (HTTP/1.1 100 Continue overriding subsequent 200 response code with PUT request)
+--SKIPIF--
+<?php
+include "skipif.inc";
+skip_client_test();
+?>
+--FILE--
+<?php 
+echo "Test\n";
+
+include "helper/server.inc";
+
+server("upload.inc", function($port) {
+       $r = new \http\Client\Request("PUT", "http://localhost:$port/", [],
+                       (new \http\Message\Body)->append("foo")
+       );
+       $c = new \http\Client;
+       $c->setOptions(["expect_100_timeout" => 0]);
+       $c->enqueue($r)->send();
+       
+       var_dump($c->getResponse($r)->getInfo());
+       var_dump($c->getResponse($r)->getHeaders());
+});
+
+?>
+===DONE===
+--EXPECTF--
+Test
+string(15) "HTTP/1.1 200 OK"
+array(4) {
+  ["Accept-Ranges"]=>
+  string(5) "bytes"
+  ["Etag"]=>
+  string(10) ""%x""
+  ["X-Original-Transfer-Encoding"]=>
+  string(7) "chunked"
+  ["Content-Length"]=>
+  int(%d)
+}
+===DONE===
index c41a260f0acff98b8e4d29167374ee97f5f018ae..9666b976a618a6f297df56448ac70df54ae18c58 100644 (file)
@@ -41,8 +41,9 @@ server("proxy.inc", function($port, $stdin, $stdout, $stderr) {
 Test
 Server on port %d
 CONNECT www.example.com:80 HTTP/1.1
+Hello: there!
 Host: www.example.com:80
-User-Agent: PECL_HTTP/%s PHP/%s libcurl/%s
 Proxy-Connection: Keep-Alive
-Hello: there!
+User-Agent: PECL_HTTP/%s PHP/%s libcurl/%s
+
 ===DONE===
index 7ea5d60d87eceee5eab42c7fd6cc0f32afe84c07..ed86f4a727e87d5b4520b3727efcdcf8010fc0d1 100644 (file)
@@ -35,7 +35,8 @@ server("proxy.inc", function($port, $stdin, $stdout, $stderr) {
 Test
 Server on port %d
 GET / HTTP/1.1
-User-Agent: PECL_HTTP/%s PHP/%s libcurl/%s
-Host: localhost:%d
 Accept: */*
+Host: localhost:%d
+User-Agent: PECL_HTTP/%s PHP/%s libcurl/%s
+
 ===DONE===
index 3c4793e96b5793a5a2b25f31fa1c80e4b59b7c7f..3f90cbd7b8314f2db4dd22551dab3c5c99bd7fa4 100644 (file)
@@ -7,6 +7,7 @@ include "skipif.inc";
 --FILE--
 <?php 
 
+include "helper/dump.inc";
 include "helper/server.inc";
 
 echo "Test\n";
@@ -16,7 +17,7 @@ server("proxy.inc", function($port) {
        $request = new http\Client\Request("PUT", "http://localhost:$port");
        $request->setOptions(array("resume" => 1, "expect_100_timeout" => 0));
        $request->getBody()->append("123");
-       echo $client->enqueue($request)->send()->getResponse();
+       dump_message(null, $client->enqueue($request)->send()->getResponse());
 });
 // Content-length is 2 instead of 3 in older libcurls
 ?>
@@ -25,17 +26,17 @@ server("proxy.inc", function($port) {
 Test
 HTTP/1.1 200 OK
 Accept-Ranges: bytes
+Content-Length: %d
 Etag: "%x"
 X-Original-Transfer-Encoding: chunked
-Content-Length: %d
 
 PUT / HTTP/1.1
-Content-Range: bytes 1-2/3
-User-Agent: %s
-Host: localhost:%d
 Accept: */*
 Content-Length: %d
+Content-Range: bytes 1-2/3
 Expect: 100-continue
+Host: localhost:%d
+User-Agent: %s
 X-Original-Content-Length: %d
 
 23===DONE===
diff --git a/tests/helper/dump.inc b/tests/helper/dump.inc
new file mode 100644 (file)
index 0000000..5f5f367
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+function dump_message($stream, http\Message $msg, $parent = false) {
+       if (!is_resource($stream)) {
+               $stream = fopen("php://output", "w");
+       }
+       fprintf($stream, "%s\n", $msg->getInfo());
+       $headers = $msg->getHeaders();
+       ksort($headers);
+       foreach ($headers as $key => $val) {
+               fprintf($stream, "%s: %s\n", $key, $val);
+       }
+       fprintf($stream, "\n");
+       $msg->getBody()->toStream($stream);
+       
+       if ($parent && ($msg = $msg->getParentMessage())) {
+               dump_message($stream, $msg, true);
+       }
+}
+
+?>
\ No newline at end of file
index 80a007353c4f81e32a924ea7fc955e47d8217074..f99dd97cf735575c0cbd7857d0ab77bae9ead7b6 100644 (file)
@@ -1,5 +1,6 @@
 <?php 
 
+include "dump.inc";
 include "server.inc";
 
 serve(function($client) {
@@ -18,6 +19,6 @@ serve(function($client) {
        /* return the initial message as response body */
        $response = new http\Env\Response;
        /* avoid OOM with $response->getBody()->append($request); */
-       $request->toStream($response->getBody()->getResource());
+       dump_message($response->getBody()->getResource(), $request);
        $response->send($client);
 });
diff --git a/tests/helper/upload.inc b/tests/helper/upload.inc
new file mode 100644 (file)
index 0000000..ddc06a8
--- /dev/null
@@ -0,0 +1,21 @@
+<?php 
+
+include "dump.inc";
+include "server.inc";
+
+serve(function($client) {
+       $request = new http\Message($client, false);
+       
+       if ($request->getHeader("Expect") === "100-continue") {
+               $response = new http\Env\Response;
+               $response->setEnvRequest($request);
+               $response->setResponseCode(100);
+               $response->send($client);
+       }
+       
+       /* return the initial message as response body */
+       $response = new http\Env\Response;
+       /* avoid OOM with $response->getBody()->append($request); */
+       dump_message($response->getBody()->getResource(), $request);
+       $response->send($client);
+});
diff --git a/tests/params016.phpt b/tests/params016.phpt
new file mode 100644 (file)
index 0000000..e5fbd97
--- /dev/null
@@ -0,0 +1,46 @@
+--TEST--
+header params rfc5988
+--SKIPIF--
+<?php
+include "skipif.inc";
+?>
+--FILE--
+<?php
+echo "Test\n";
+
+$link = <<<EOF
+<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>; rel="next", <https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>; rel="last"
+EOF;
+
+$p = new http\Params($link, ",", ";", "=",
+       http\Params::PARSE_RFC5988 | http\Params::PARSE_ESCAPED);
+var_dump($p->params);
+var_dump((string)$p);
+?>
+===DONE===
+--EXPECT--
+Test
+array(2) {
+  ["https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2"]=>
+  array(2) {
+    ["value"]=>
+    bool(true)
+    ["arguments"]=>
+    array(1) {
+      ["rel"]=>
+      string(4) "next"
+    }
+  }
+  ["https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34"]=>
+  array(2) {
+    ["value"]=>
+    bool(true)
+    ["arguments"]=>
+    array(1) {
+      ["rel"]=>
+      string(4) "last"
+    }
+  }
+}
+string(162) "<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=2>;rel="next",<https://api.github.com/search/code?q=addClass+user%3Amozilla&page=34>;rel="last""
+===DONE===
diff --git a/tests/params017.phpt b/tests/params017.phpt
new file mode 100644 (file)
index 0000000..b3587e5
--- /dev/null
@@ -0,0 +1,69 @@
+--TEST--
+header params rfc5988
+--SKIPIF--
+<?php
+include "skipif.inc";
+?>
+--FILE--
+<?php
+echo "Test\n";
+
+$link = <<<EOF
+Link: </TheBook/chapter2>;
+         rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
+         </TheBook/chapter4>;
+         rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel
+EOF;
+
+$p = current(http\Header::parse($link, "http\\Header"))->getParams(
+       http\Params::DEF_PARAM_SEP,
+       http\Params::DEF_ARG_SEP,
+       http\Params::DEF_VAL_SEP,
+       http\Params::PARSE_RFC5988 | http\Params::PARSE_ESCAPED
+);
+var_dump($p->params);
+var_dump((string)$p);
+?>
+===DONE===
+--EXPECTF--
+Test
+array(2) {
+  ["/TheBook/chapter2"]=>
+  array(2) {
+    ["value"]=>
+    bool(true)
+    ["arguments"]=>
+    array(2) {
+      ["rel"]=>
+      string(8) "previous"
+      ["*rfc5987*"]=>
+      array(1) {
+        ["title"]=>
+        array(1) {
+          ["de"]=>
+          string(15) "letztes Kapitel"
+        }
+      }
+    }
+  }
+  ["/TheBook/chapter4"]=>
+  array(2) {
+    ["value"]=>
+    bool(true)
+    ["arguments"]=>
+    array(2) {
+      ["rel"]=>
+      string(4) "next"
+      ["*rfc5987*"]=>
+      array(1) {
+        ["title"]=>
+        array(1) {
+          ["de"]=>
+          string(17) "nächstes Kapitel"
+        }
+      }
+    }
+  }
+}
+string(139) "</TheBook/chapter2>;rel="previous";title*=utf-8'de'letztes%20Kapitel,</TheBook/chapter4>;rel="next";title*=utf-8'de'n%C3%A4chstes%20Kapitel"
+===DONE===