Hi Tim,

Thanks for the detailed review! Let me share my answers inline:

1. I don't think `QueryParamParsingOptions::$maxQueryStringLength` is
> necessary or useful to have. Checking the input length is easy to do
> manually (it's just a `strlen()` call) and I don't see a practical way
> to abuse the API with large input lengths alone: The API will not “blow
> up” inputs into considerably larger data structures, the input strings
> are mostly just “copied” over, not too different from e.g.
> `str_replace()`.
>

I was also thinking a lot about whether this option is necessary to have.
My main reason for including it was that people would otherwise easily
forget
to limit the input length, whereas the dedicated option enforces the limit
everywhere.

So the real question is whether QueryParams really cannot be abused via
long strings, and whether $maxParamCount alone could mitigate those risks?

And now by thinking about it more, neither I could identify any specific
vulnerability that
would become possible by not enforcing the upper limit. In the previous
iteration
of the RFC, limiting the length was more useful, because parsing would have
been
a more resource intensive operation due to the requirements of the old
"array API".
The operations that are performed now during parsing are cheap, so as far
as I can see,
it's possible to get rid of this option if no one can come up with a good
enough use-case.


> 3. If `$maxQueryStringLength` goes away, the `QueryParamParsingOptions`
> would be a single property object. Maybe the param count limit should
> become a regular parameter then, since I can't think of any other useful
> parse-time options.
>

Yes, without trying to future proof the API too much, I guess this is the
only sensible
choice, unless someone finds any other option which would be needed.

4. For the `QueryParamBuildingOptions` I don't think they should be
> passed as parameter to the parsing functions (and thus not be stored as
> an object property). Instead they should be passed to the `->to*()`
> functions just when they are needed. That keeps the QueryParams class’
> state focused on the actual parameters.
>
> Edit after getting further down the RFC: This choice is explained
> further below, because it immediately affects the getters. This makes
> sense, but I still believe this is a less than ideal API design. But I
> don't have better proposals other than “force the user to make a
> decision and only allow passing in `string`”. For `int` the implicit
> conversion to string would just work and making an explicit choice for
> `bool` is easy enough.
>

Yes, it was another inner debate for me whether relying on explicit casting
to a string
by the user or supporting automatic conversion is a better approach. I
believe
offering some kind of automatic casting approach is more ergonomic, so I
think
it's useful to try to keep this mechanism if possible.

Moreover, the fromArray() method reads the input array recursively, which
means
that in the absence of building options, users would need to make sure that
each
parameter value is casted to a supported type, even the nested ones. For
most legal cases,
the conversion is trivial (int, float, backed enum), so why require an
explicit conversion?

On the other hand, the query param representation of boolean or null values
may vary
case by case, and as I've just realized it's not as simple as I previously
thought:

Let's take a Consul API endpoint for example (this is just a random example
which I
found first):
https://developer.hashicorp.com/consul/api-docs/operator/usage#global
The "global" query parameter represents a bool value, but only the query
parameter name needs to be passed, so an example request URL looks like
this:
"http://127.0.0.1:8500/v1/operator/usage?global";

If one would build the query parameters, probably the following code would
be used:

$params = new Uri\QueryParams();
if ($global) {
    $params = $params->set("global", null);
}

This example made me realize that neither the modification methods should
respect
the $nullPolicy (in addition to hasValue() and deleteValue()), because
otherwise
one couldn't be sure if "global" would be included in the request -- it
would be omitted
if $nullPolicy is "SkipTuple".

As a result, I'm fine with changing the input param type for append() and
set():
instead of "mixed", maybe we could accept
"string|int|float|BackedEnum|null"? I still
think that using a "?string" type would be too pedantic. Going with the
above signature
would mean that one would need to cast a bool input to any of the supported
ones (or
omit them e.g. in case of false, as seen in the example above), and they
would also need
to handle null values on a case by case basis, since passing a null value
would always
add the parameter without a value (in line with
QueryParamNullPolicy::KeyOnly),
and the behavior of QueryParamNullPolicy::SkipTuple could be emulated by
simply skipping
calling append() or set() on the input. I don't know if it makes sense to
you,
but that's the idea which currently sounds the best for me.

However, I'm still unsure in case of fromArray() what would be the correct
solution that is
also as ergonomic as possible.


> 5. Is having a separate `parseRfc3986()` useful when RFC 3986 doesn't
> specify the handling of query parameters and just delegates to RFC 1866
> instead? A big benefit of the URI extension for me is that it clearly
> references the relevant standards in its naming and strictly implements
> them, making it easy to discover and allowing the use of primary
> references to understand what they are supposed to do. The
> `parseRfc3986()` method would muddy this straight-forward standard’s
> compliant implementation, particularly since it is defined to accept
> malformed inputs such as `#foo=bar`, which I would find unexpected from
> a method referencing RFC 3986 in its name.
>

As far as I can see, supporting RFC 3986 parsing is necessary to keep full
compatibility with the URI class so that the meaning of literal +
characters in
the query string wouldn't change during query parameter parsing.
Let's consider this code:

$uri = new Uri\Rfc3986\Uri("?q=C++");

$rfc1866Params = Uri\QueryParams::parseRfc1866($uri->getQuery());
$rfc3986Params = Uri\QueryParams::parseRfc3986($uri->getQuery());

var_dump($rfc1866Params->getFirst("q"));                    // string(3)
"C  "
var_dump($rfc3986Params->getFirst("q"));                    // string(3)
"C++"

Unfortunately, WHATWG URL's URL and URLSearchParams classes have different
query parameter parsing algorithms (see the "Percent-Encoding and Decoding"
section),
so the same issue is already present there, and unavoidable. I'd like not
to repeat it in
case of RFC 3986, that's why I think parseRfc3986() is needed. But let me
know if
I miss something.

Regarding accepting malformed input: uriparser's own query parameter
handling
functions (uriDissectQueryMalloc*()) behave the same way as the RFC currently
proposes.
Unfortunately, I could not really find any reasoning behind this choice of
uriparser.

To be clear, I don't insist on following uriparser's behavior for RFC 1866
and RFC 3986 (it's
not an option for WHATWG URL, since it specifies what needs to be done in
every detail),
and probably strict parsing is what is more consistent with the Uri class
indeed.

Then passing an invalid query string syntax would throw. However, incorrect
UTF-8 sequences
would still be allowed, since neither RFC 1866, nor RFC 3986 require a
syntactically
correct query string to be valid UTF-8.

7. I cannot describe an actual issue, but I find accepting invalid
> percent-encoding sequences to be dangerous. It would definitely be
> unexpected that `%6f=x' would be `o=x`, but `%6g=y` would be `%6g=y`
> (which would then become `%256g=y`).
>

Answered above. :)

8. The naming of `hasValue()` sounds like it would check if there is a
> `=` sign for the given key somewhere, not that it would check that there
> is a specific key-value pair. I'm also not sure if it is necessary.
> Duplicated keys are comparatively rare and for the use cases where this
> is a thing, `in_array()` on the result of `getAll()` would just work (or
> `array_any()` for more complex checks).
>

This method is required by WHATWG URL:
https://url.spec.whatwg.org/#dom-urlsearchparams-has
It's specified as an overloaded method, so I had to introduce a separate
signature for this use-case, as per the policy accepted in
https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#policy_about_new_functions#policy_about_new_functions
.

TBH, I find it useful (more convenient and more efficient than getAll() +
in_array()).
The name itself may be slightly confusing at the first sight, but I think
the
$value parameter which must be passed should clarify its purpose. But in
any case,
I'm open to improving its name.


> 9. I'm not sure I like the name `->list()`. Technically it's also
> redundant with `iterator_to_array($queryParams)`, but having a dedicated
> method probably makes sense nevertheless.
>

Yes, I'm fine with renaming this method to toList(). It would go well with
toArray()
IMO. It's more ergonomic and more efficient than the alternative, so I agree
that it's useful.

10. Would it make sense to rename `parseWhatWg()` to
> `parseUrlSearchParams()` or similar to make it clear that this
> constructor implements the URLSearchParams specification. I'm asking due
> to the section that mentioned how `$whatWgUrl->getQuery()` is not the
> correct value to pass there (due to the questionmark handling). Renaming
> this method would also make sense when removing the RFC 3986 parser (as
> suggested in (5)).
>

I think it's a good idea. Maybe people using it would be less likely to be
surprised by
its behavior (how it handles the leading ? character) if the name referred
to
URLSearchParams. However, I think including WHATWG in the method name
would still have its purpose (people with less familiarity with WHATWG URL
would know immediately that this method is related to the WHATWG URL
specification),
so my preferred name would be parseWhatWgUrlSearchParams(), even
if it's somewhat verbose.

11. Bullet point 8 also applies to `deleteValue()`. Is there an actual
> use case for this method?
>

Yes, it has the same reason:
https://url.spec.whatwg.org/#dom-urlsearchparams-delete

Also, this function may come handy in case of filtering. For example, let's
consider the
"tag=php&tag=web&sort=date" query string: deleteValue("tag", "php") would
remove one
selected filter while keeping the other parameters intact.


> 12. Is having the `withArray()` method useful? It does the same as
> `QueryParams::fromArray()`, no? It feels confusing to have a “replace
> everything” method when one could just create something entirely new.
> Technically one could even call `$queryParams->fromArray()`, since
> calling static methods on instances is legal.
>

Currently, the only description about this method is the following:

It replaces the whole query list with the input, but clones the current
> instance rather than creating a completely new one.


So it's a more efficient form of instantiating a class based on an array
input than fromArray().
Additionally, it was designed to also keep the original building options
(if they are going to be kept
in the final proposal).

$queryParams = Uri\QueryParams::parseRfc3986("foo=bar", 1000, new
Uri\QueryParamBuildingOptions("true", "false"));
$array = [...];
// Currently preferred solution:
$queryParams = $queryParams->withArray($array);
// The same solution using fromArray():
$queryParams = Uri\QueryParams::fromArray($array,
$queryParams->getBuildingOptions());

P.S. The getBuildingOptions() doesn't exist yet in the proposal, and it
should be added if building options remain in the RFC at last.

13. “doesn't start with a “with” suffix” - that should read “prefix”.
> Prefix is “start with”, suffix is “end with”.
>

 Yeah, thanks for noticing it. It was an unfortunate typo, and I had
already fixed it in my draft version when you were reading it.

14. The “Backward Incompatible Changes” section should mention that
> adding a new class is technically backwards incompatible, but an allowed
> break according to policy and that the Uri namespace is reserved for
> ext/uri since 8.5.
>

Fixed.

One more thing which I'm probably going to change: it turned out that
WHATWG URL specifies
the sort() method (https://url.spec.whatwg.org/#dom-urlsearchparams-sort)
which must use UTF-16 based
comparisons (using 2 bytes). It is really not obvious from the WHATWG URL,
only the WHATWG Infra
specification defines it: https://infra.spec.whatwg.org/#code-unit-less-than

In order to restore compatibility with this requirement, I'd define two
sorting algorithms:
- sortByBytes(): the currently specified one which would use bytes as the
base of comparison
- sortByUtf16(): the one comparing UTF-16 sequences at once which would be
compatible with WHATWG URL

Again, thank you for the review!

Máté

Reply via email to