Problem/Motivation

Right now there is no validation of ?filter params. The variety of filter keys and their combinations might be confusing for developers. All these filter variants are correct:

filter[uuid][value]=1234.
filter[0][condition][path]=uuid&filter[0][condition][value]=1234.
filter[uuid][condition][value]=1234.
filter[uuid][value]=1234&filter[uuid][group]=my_group.
filter[title-filter][condition][path]=title
filter[title-filter][condition][operator]=CONTAINS
filter[title-filter][condition][value]=Foo
# Create an AND and an OR GROUP
filter[and-group][group][conjunction]=AND
filter[or-group][group][conjunction]=OR

# Put the OR group into the AND GROUP
filter[or-group][group][memberOf]=and-group

# Create the admin filter and put it in the AND GROUP
filter[admin-filter][condition][path]=uid.name
filter[admin-filter][condition][value]=admin
filter[admin-filter][condition][memberOf]=and-group

# Create the sticky filter and put it in the OR GROUP
filter[sticky-filter][condition][path]=sticky
filter[sticky-filter][condition][value]=1
filter[sticky-filter][condition][memberOf]=or-group

# Create the promoted filter and put it in the OR GROUP
filter[promote-filter][condition][path]=promote
filter[promote-filter][condition][value]=1
filter[promote-filter][condition][memberOf]=or-group

And so on. The validation should help to avoid painful debugging of invalid filter request.

Proposed resolution

1. Add thorough validation to Drupal\jsonapi\Routing\Param\Filter.
2. Cover all validation cases with Kernel or Unit tests. Should be done in #2852259: Add comprehensive test coverage to \Drupal\jsonapi\Routing\Param\Filter

Comments

Spleshka created an issue. See original summary.

spleshka’s picture

Issue summary: View changes
spleshka’s picture

spleshka’s picture

Assigned: Unassigned » spleshka

Assigning this one to myself.

spleshka’s picture

Issue summary: View changes
clemens.tolboom’s picture

In trying to grasp https://www.drupal.org/docs/8/modules/json-api/collections-filtering-sor... I tried to translate that back into https://en.wikipedia.org/wiki/Backus–Naur_form

For me this helped a little. But I guess if we first spec the filter we can write tests for it easier.

Below a EBNF of the canonical version: filter[{filter id}][{filter type}][{filter property}]={filter value}


<filter> :== <filter-item> | <filter-item> '&' <filter>

<filter-item> :== <condition-filter> | <group-filter>

<condition-filter> :== <filter-id> '[condition]' <condition-filter-property>

<condition-filter-property> :== '[path]' | '[value]' | '[operator]' | '[group]' | '[memberOf]'

<group-filter> :== <filter-id> '[group]' <group-filter-property>

<filter-id> :== '[' <alpha-with-dashes-or-integer> ']'

<group-filter-property> :== '[conjunction]' | '[group]'

<alpha-with-dashes-or-integer> :== <letter-dash> | <integer>

<letter-dash> ::= <letter> | <letter> <letter-dash> | <letter> '-' <letter-dash>

<integer> ::= <digit> | <digit> <integer>

filter-id is not completely clear to me. "it can be a string or an integer". What are the string chars? How long can it be?

Both (optional) group is unclear to me in:

- Condition Properties: path, value, operator, (optional) group
- Group Properties: conjunction, (optional) group

memberOf is mentioned on doc page but not in the list of

clemens.tolboom’s picture

filter[filter-id][group][group] = "parent-group-id"

why not make that parent instead of that weird repeating [group][group]

filter[filter-id][group][parent] = "parent-group-id"
spleshka’s picture

Couple things to mention:

<condition-filter-property> :== '[path]' | '[value]' | '[operator]' | '[group]' | '[memberOf]'

I don't think that "group" belongs to conditions. It's essentially the same as "memberOf".

why not make that parent instead of that weird repeating [group][group]

It's a good point, but please open a follow-up issue.

- Condition Properties: path, value, operator, (optional) group
- Group Properties: conjunction, (optional) group

What I've found in docs is:
- Condition Properties: path, value, operator, (optional) memberOf
- Group Properties: conjunction, (optional) group, (optional) memberOf

What I've found interesting is that devs can make "condition" query without specifying the "condition" key in the filter query. So these filter queries are identical and both work:

filter[title-filter][condition][path]=title
filter[title-filter][condition][operator]=CONTAINS
filter[title-filter][condition][value]=Foo
filter[title-filter][path]=title
filter[title-filter][operator]=CONTAINS
filter[title-filter][value]=Foo
spleshka’s picture

filter-id is not completely clear to me. "it can be a string or an integer". What are the string chars? How long can it be?

Literally any scalar type will work: int, float, string. I'm assuming that with any set of chars as well, so no limitations here. I've also tried to set this value to a string of ~4k chars and it worked out. So the filter ID length is not limited as well (apart from usual server limitations).

clemens.tolboom’s picture

As http://jsonapi.org/format/#fetching-filtering is agnostic I'd love to have our specs for filtering in a rigid way.

I hope we can derive the short forms from the long version.

What I've found interesting is that devs can make "condition" query without specifying the "condition" key in the filter query.

Drupal core devs or JSON API devs?

spleshka’s picture

Drupal core devs or JSON API devs?

Anyone who implements the filtering with JSON API.

spleshka’s picture

Status: Active » Needs review
StatusFileSize
new7.21 KB

Attaching the first draft. Going to cover the functionality with tests soon. Just thought that someone might have the first round of feedback in the meantime.

Status: Needs review » Needs work
spleshka’s picture

Status: Needs work » Needs review
StatusFileSize
new7.17 KB
new441 bytes

Looks like "operator" key in "condition" query is NOT mandatory (worth updating the docs).

spleshka’s picture

StatusFileSize
new17.61 KB
new16.14 KB

Here's is the final version including tests. Several notes:
1. Slightly refactored dataProvider for valid tests. Hope it's okay :)
2. Treat usage of filter[group][group] or filter[condition][group] keys as invalid, because there is no legacy support of those keys in the code, so they are useless and I think frontend apps should know about this.
3. We do need to update the documentation to get rid of examples with keys mentioned in the previous item.
4. We do need to mark operator key as optional at https://www.drupal.org/docs/8/modules/json-api/collections-filtering-sor.... As well as that, the list of allowed operators is incomplete: missing "CONTAINS", etc. I'd change Can be =, <, >, <>, IN, NOT IN, IS NULL, IS NOT NULL or BETWEEN. to Supports any database-compliant operators, for example: =, <, >, <>, IN, NOT IN, IS NULL, IS NOT NULL, BETWEEN, etc.

spleshka’s picture

StatusFileSize
new21.57 KB
new4.49 KB

Over the night I've thought that it makes sense to add several more tests with valid filters to make sure that the validation doesn't cut them.

clemens.tolboom’s picture

So we are not define / specify the filter options? We could say the docs are the specs but my 'bnf' was just a first stab to a specification. I hoped someone would point to a PHP spec tool :p

  1. +++ b/tests/src/Unit/Routing/Param/FilterTest.php
    @@ -14,72 +14,110 @@ class FilterTest extends UnitTestCase {
    +   * @dataProvider validFiltersDataProvider
    

    I hoped for a canonical data provider and a function to create the short version(s) from it.

  2. +++ b/tests/src/Unit/Routing/Param/FilterTest.php
    @@ -14,72 +14,110 @@ class FilterTest extends UnitTestCase {
    +      // Test case:
    +      // filter[foo][value]=bar.
           [
             ['foo' => ['value' => 'bar']],
             ['foo' => ['condition' => ['path' => 'foo', 'value' => 'bar', 'operator' => '=']]],
           ],
    -      // Tests filter[0][path]=foo&filter[0][value]=bar.
    +      // Test case:
    +      // filter[0][path]=foo
    +      // filter[0][value]=bar.
           [
             [0 => ['path' => 'foo', 'value' => 'bar']],
             [0 => ['condition' => ['path' => 'foo', 'value' => 'bar', 'operator' => '=']]],
           ],
    

    Why not use the filter URL parameter strings instead of PHP arrays? Makes the comments obsolete.

spleshka’s picture

So we are not define / specify the filter options? We could say the docs are the specs but my 'bnf' was just a first stab to a specification.

Not sure I understand. Could you please expand?

I hoped for a canonical data provider and a function to create the short version(s) from it.

In some cases we test both short and canonical versions together. Not sure how it's going to work if we provide just a canonical data provider and then reuse it for shorthand version.

Why not use the filter URL parameter strings instead of PHP arrays? Makes the comments obsolete.

The class we're testing accepts only arrays. Conversion of URL strings into arrays is a job of another class and we're not testing it here.

spleshka’s picture

Any volunteers to review this? Or ask questions if there are any. I think this patch is important, as it brings in more clarity for contributors regarding filtering system. Additionally, it will be much easier to support backwards compatibility if there will be any changes in the future. So be bold and look into the patch :P

e0ipso’s picture

Status: Needs review » Needs work

I'm sorry it took too long for this review.


+++ b/src/Routing/Param/Filter.php
@@ -74,6 +75,7 @@ class Filter extends JsonApiParamBase {
     foreach ($this->original as $filter_index => $filter_item) {
+      $this->validateItem($filter_index, $filter_item);
       $expanded[$filter_index] = $this->expandItem($filter_index, $filter_item);
     }

Let's validate the expanded item instead so we can simplify the validation logic.

clemens.tolboom’s picture

Status: Needs work » Needs review

I took quite a tour d.o #2884913: Update link module for array query parameters and Link D7 #2333119: Output broken when using array parameters in query to answer my own question. Why not use parse_str to generate your arrays.

    $test = [
        'filter[foo][value]=bar',
        'filter[foo][condition][path]=foo&filter[foo][condition][value]=bar&filter[foo][condition][operator]=%23',
    ];
    
    parse_str($test[0], $input);
    parse_str($test[1], $expected);

The test code is long due to extra documentation and does not match potential reports submitting their query strings.

I cannot review this patch due to not enough experience yet with filters.

clemens.tolboom’s picture

I ran into D8 #2885351: Query string duplications wondering whether we should have tests for bad patterns too.

\Drupal\Tests\link\Functional\LinkFieldTest has also some $invalid_external_entries and $invalid_internal_entries.

And related to #21 they use \parse_str in core/lib/Drupal/Core/Url.php:298.

Sorry for the noise ;-)

e0ipso’s picture

@Spleshka do you think you'll have time to follow up on this soon? If not, there's no problem with that, please unassign the ticket and grab it back when you're available again.

spleshka’s picture

@e0ipso, I think I'll find the time on this weekend to get it done. Got extremely busy these days, sorry guys for delay.

e0ipso’s picture

@Spleshka I'm sorry to hear that. I'm cheering for you till the end of your project! I just wanted to make sure you had all you needed for this.

spleshka’s picture

StatusFileSize
new21.68 KB
new5.81 KB

Hi @e0ipso, thanks for your words :) Attaching the patch with updated validation behavior.

spleshka’s picture

I ran into D8 #2885351: Query string duplications wondering whether we should have tests for bad patterns too.

I think it should be a separate follow-up. Let's get the basic stuff done first, then I'm happy to enhance it. I'm a big fan of step-by-step improvements :)

e0ipso’s picture

Status: Needs review » Reviewed & tested by the community

This looks great @Spleshka! I wonder if we can have this kind of validation and test coverage for sort and page as well. Can you create follow up tickets for those?

e0ipso’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new23.28 KB
new13.79 KB

I ended up doing a refactor and catching a bug with the IS NULL filters (they don't require a value).

  • e0ipso committed 895112a on 8.x-1.x authored by Spleshka
    feature(Filter): Implement validation of filter params (#2878654 by...
e0ipso’s picture

Status: Needs review » Fixed

This is committed. Thanks!

spleshka’s picture

Thanks Mateu for the fast turn-around & commit!

FYI, added 2 follow-ups: #2887304: [TASK] Implement validation of page param, #2887302: [TASK] Implement validation of sort param

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.