Problem/Motivation

Calling drupal_http_build_query() --or another function like url() which calls drupal_http_build_query-- with some empty params adds extra ampersand signs. Take a look at next code:

$q = array(
  'action' => 'search',
  'filter' => array(
    'name' => array(),
    'city' => array(),
    'location' => array(),
  ),
  'page' => 10,
);

echo drupal_http_build_query($q);

++++++ sample for for Drupal 11

use Drupal\Component\Utility\UrlHelper;
echo UrlHelper::buildQuery($q);

This displays

action=search&&&&page=10

and you notice that 3 extra amps are added. I know this is because name, city and location are empty but this can happen often if you are making some complex filters.
PHP http_build_query() handles well these empty arrays but drupal don't. With PHP http_build_query we get

action=search&page=10

which is correct.

Steps to reproduce

Proposed resolution

Modify buildQuery to ignore empty arrays and NULL valiues.

--- original proposal ---
To fix this we have to check if $params is empty or not before implode. Please check patch:

--- a/includes/common.inc
+++ b/includes/common.inc
@@ -489,7 +489,8 @@ function drupal_http_build_query(array $query, $parent = '') {
     }
   }
 
-  return implode('&', $params);
+  $params = array_filter($params);
+  return empty($params) ? NULL : implode('&', $params);
 }

Remaining tasks

Review
Commit

User interface changes

API changes

Data model changes

Release notes snippet

Issue fork drupal-1425588

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

ptocheia’s picture

Status: Active » Needs work

I've verified that this is a bug. The patch looks promising, please attach it for testing!

webkenny’s picture

Version: 7.12 » 7.x-dev
Assigned: Sorin Sarca » Unassigned
Status: Needs work » Needs review
StatusFileSize
new370 bytes

Nice patch. I'm merely converting the code to patch format and queuing for testing. Also confirming this works but I won't RTBC it since I'm the one submitting it.

lslinnet’s picture

Status: Needs review » Reviewed & tested by the community

Have reviewed the code and tested the implementation, it works as expected.

Also good to see that it doesn't strip out empty variables like

<?php
$q = array(
  'action' => 'search',
  'filter' => array(
    'name' => array(),
    'city' => array(),
    'location' => array(),
  ),
  'test' => '',
  'page' => 10,
);
?>

Reason for this is that some web services require that you pass all parameters even if they are empty, taking an example "p1=&q=123213" is valid and is in some cases required by the receiving server.

webchick’s picture

Version: 7.x-dev » 8.x-dev
Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

Thanks for the fix! Let's add a test here to prove the fix works.

Also, patches go into D8 first, then are backported to D7, so bumping to D8.

webkenny’s picture

Right, right. Forgot about that piece. :) I haven't the slightest idea how to code a test. Sounds like I have a project to do. I'll work on it.

webkenny’s picture

Ok, so I am attaching this here so I don't lose it but here's the D8 version. Still needs a test. Working on that now.

webkenny’s picture

So far the following test has failed but i am continuing to check into it:

$this->assertEqual(drupal_http_build_query(array('a' => '1', 'b' => NULL, 'c' => '3')), 'a=1&c=3', t('Multiple values were properly concatenated when null values were passed.'));

I have to assume this is just my newness to writing tests. I'll post back when I have something.

lslinnet’s picture

Your test assertion assumes that it should be empty correct output would be a=1&b=&c=3

Sorin Sarca’s picture

For array('a' => '1', 'b' => NULL, 'c' => '3') the correct output is a=1&b&c=3 which I thinnk is equivalent with a=1&b=&c=3 since for both the output of the following code is the same:

var_dump($_GET);

Output:

array
'a' => string '1' (length=1)
'b' => string '' (length=0)
'c' => string '3' (length=1)

Because of this I thinks we should also change the line

elseif (!isset($value)) {

with

elseif (!isset($value) || $value === '') {

to remove "=" when no value OR
we should modify the line

$params[] = $key;

with

$params[] = $key . '=';

to always have "=".

(OR of course we can leave it as is because there is no difference between them)

cweagans’s picture

Issue tags: +Needs backport to D7
cgreaten’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new1.91 KB
new1.12 KB

Spoke with chx in IRC and he confirmed this is the desired behavior. See the attached test and fix files.

Status: Needs review » Needs work

The last submitted patch, drupal-1425588-11-combined.patch, failed testing.

tim.plunkett’s picture

Reading http://en.wikipedia.org/wiki/Query_string:

Within each pair, the field name and value are separated by an equals sign. The equals sign may be omitted if the value is an empty string.

So it looks like we got this one wrong. Either ?a=1&b=&c=3 or ?a=1&b&c=3 are valid, but ?a=1&c=3 is not valid.

Melissamcewen’s picture

So in the test that is in the newest patch for

$query = array(
      'a' => '1',
      'b' => array(
        'c' => array(),
        'd' => NULL,
      ),
      'e' => '2',
      'f' => '',

the result should be a=1&b[c]&b[d]&e=2&f?

cgreaten’s picture

Both having an equals sign and not having an equals sign for empty string parameters seem to be valid. Uploading both variations of the patch to determine which will break the least amount of code in core.

cgreaten’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, drupal-1425588-15-without-equals-combined.patch, failed testing.

tim.plunkett’s picture

http://example.com/node/123?foo= is VERY weird.

Comparing a=1&e&f=2 and a=1&e=&f=2, I think we definitely want the former.

Unfortunately, that means debugging why _openid_get_params() is blowing up.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new4.09 KB
new3.18 KB

chx demonstrated that a pure HTML form includes the = sign, and it's required for OpenID.

Status: Needs review » Needs work

The last submitted patch, drupal-1425588-18-combined.patch, failed testing.

cgreaten’s picture

Status: Needs work » Needs review
StatusFileSize
new1.12 KB

Alright. I think I have the test created and bug fixed. I have attached the test for this bug and also the bug fix as well as the test fixes.

cgreaten’s picture

StatusFileSize
new4.12 KB
new1.12 KB

Alright. I think I have the test created and bug fixed. I have attached the test for this bug and also the bug fix as well as the test fixes.

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

This had chx's input at MWDS, and I think Chris nailed it here.

jcfiala’s picture

Just a note...

I'm actually working on a link bug http://drupal.org/node/1710578 where the user has a url he's entering - 'http://www.example.com/?something' and I'm outputting 'http://www.example.com/?something=', the latter of which breaks what he's trying to point at. I'm currently able to fix this by setting the query array so that it's array('something' => NULL), but this change would break that. I realize that for PHP, the querystrings are equivalent, but that may not be true in all languages, would it?

tim.plunkett’s picture

Then he can parse it differently. They are equivalent in HTTP.

jcfiala’s picture

He doesn't have any control over the target's processing in this case, but if they're equivalent in HTTP, then that's fine. Just pointing out that it could cause trouble for someone else.

damien tournoud’s picture

Status: Reviewed & tested by the community » Needs work
-  return implode('&', $params);
+  $params = array_filter($params);
+  return !empty($params) ? implode('&', $params) : NULL;
 }

This array_filter() feels sloppy (it removes any falsish value by default, aka NULL, '', 0, '0', etc.). If I understand this correctly, we are just removing the special case elseif (!isset($value)) and checking that we don't add an entry if the sub-query is empty. So the whole function could read:

function drupal_http_build_query(array $query, $parent = '') {
  $params = array();

  foreach ($query as $key => $value) {
    $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));

    // Recurse into children.
    if (is_array($value)) {
      $children_query = drupal_http_build_query($value, $key);
      if ($children_query !== '') {
        $params[] = $children_query;
      }
    }
    else {
      // For better readability of paths in query strings, we decode slashes.
      $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
    }
  }

  return implode('&', $params);
}
     // Verify that the current path is used as destination.
-    $this->drupalGet('common-test/destination', array('query' => array($query => NULL)));
+    $this->drupalGet('common-test/destination', array('query' => array($query => '')));
     $url = 'common-test/destination?' . $query;
     $this->assertText('The destination: ' . $url, t('The current path is determined as destination.'));

That cannot possibly be the intention of the test? This clearly only passes by chance (because it does a assertText() which is a substring match).

We likely want something like:

    // Verify that the current path is used as destination.
    $this->drupalGet('common-test/destination', array('query' => array('additional_argument' => $query)));
    $url = 'common-test/destination?additional_argument=' . $query;
    $this->assertText('The destination: ' . $url, t('The current path is determined as destination.'));
Sorin Sarca’s picture

If I understand this correctly, we are just removing the special case elseif (!isset($value)) and checking that we don't add an entry if the sub-query is empty.

That's true.
But can be done without auxiliary variable:

    // Recurse into children.
    if (is_array($value)) {
      if ($value) {
        $params[] = drupal_http_build_query($value, $key);
      }
    }
damien tournoud’s picture

But can be done without auxiliary variable:

No, because the sub-array could contain empty sub-sub-arrays.

Melissamcewen’s picture

But can be done without auxiliary variable:

No, because the sub-array could contain empty sub-sub-arrays.

Yes, the test written by cgreaten contains an array with an empty array and I don't think it would pass without the auxiliary variable.

cgreaten’s picture

This evening I applied the test I originally wrote to prove there was a bug and then applied Damien's proposed code. When I ran the test, it failed. The reason it failed is that '&d=' was added to the query string for the d => NULL portion of the array in the test. In the patch I submitted, that portion of the query string is left out (and I think correctly).

I am happy to re-roll my patches again or make changes, if needed. Sorry I'm not quite sure what the process is from here (it's my first attempt at contributing to the community).

cgreaten’s picture

Status: Needs work » Needs review
StatusFileSize
new4.62 KB
new1.12 KB

Alright, I took a bit of time tonight and I tweaked Damien's code so it passes the test. I also used his feedback to modify the test in the GotoTest to be better. With the tests that are in UrlTest tests, I added a test for '' along side the NULL so that both are there instead of just changing the NULLs to '' so that the tests pass.

This patch can be reviewed and then compared to my first one to see which is preferable (or whether we want some kind of hybrid of the two).

The last submitted patch, 32: drupal-1425588-32-combined.patch, failed testing.

mgifford’s picture

Issue summary: View changes
Status: Needs review » Needs work

GotoTest.php is no longer there and patch no longer applies.

Sawascwoolf’s picture

Hi,

I don't know how to create a patch, but here is my code suggestion:

function drupal_http_build_query(array $query, $parent = '') {
  $params = array();
  
  foreach ($query as $key => $value) {
    $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));

    // Recurse into children.
    if (is_array($value)) {
      $children_query = drupal_http_build_query($value, $key);
      if ($children_query) {
        $params[] = $children_query;
      }
    } elseif ($value === NULL ) {
        
    } elseif (!isset($value) || $value === FALSE || $value === '') {
      $params[] = $key;
    }
    else {
      // For better readability of paths in query strings, we decode slashes.
      $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
    }
  }
  return implode('&', $params);
}

In my tests this function returns the expected Results.

tim.plunkett’s picture

Title: drupal_http_build_query() adds extra ampersands » \Drupal\Component\Utility\UrlHelper::buildQuery() adds extra ampersands
Status: Needs work » Needs review
StatusFileSize
new2.13 KB

Creating a patch built on #36 just to show which unit tests fail.

@Sawascwoolf, https://www.drupal.org/patch should help with patch creation if you'd like to learn!

Status: Needs review » Needs work

The last submitted patch, 37: 1425588-ampersands-37.patch, failed testing.

The last submitted patch, 37: 1425588-ampersands-37.patch, failed testing.

Mixologic’s picture

StatusFileSize
new2.92 KB

The phpunit test that failed might have more information with this patch - its identical to #37 with the exception to the change to the simpletest module, but probably with more information

Mixologic’s picture

Status: Needs work » Needs review

How come everybody but me remembers to mark it needs work review?

marvin_b8’s picture

StatusFileSize
new2.58 KB

A problem is that

array(array('a' => '1', 'b' => array('c' => array()), 'd' => NULL, 'e' => '', 'f' => '2'), 'a=1&e=&f=2', 'Empty query parameter values are properly ignored.'),

and
array(array('foo' => NULL), 'foo', 'Simple parameters are properly added.')

this two tests are not right.

see:
'd' => NULL = ''
'foo' => NULL = foo

and this can not work.

We need a clear behavior for this function .

tim.plunkett’s picture

StatusFileSize
new2.21 KB
new953 bytes
http_build_query(array('a' => '1', 'b' => array('c' => array()), 'd' => NULL, 'e' => '', 'f' => '2'));
=> "a=1&e=&f=2"

http_build_query(array('foo' => NULL));
=> ""

So there should be no 'd'

@marvin_B8, it's really hard to see what changed from one patch to the other without an interdiff

The last submitted patch, 40: 1425588-ampersands-40.patch, failed testing.

The last submitted patch, 40: 1425588-ampersands-40.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 43: 1425588-ampersands-43.patch, failed testing.

Sawascwoolf’s picture

I think we should consider the testcases a bit more precisely.

We should use http_build_query to generate test cases!

With the following code I've created the test cases:

//TestCase Array
$array = array('a' => array('b' => '2', 'c' => '3'), 'd' => 'foo');

$phpFunctionQuery = http_build_query($array,'','&',PHP_QUERY_RFC3986);
// Decode [ and ]
$result = str_replace(array('%5B','%5D'),array('[',']'), $phpFunctionQuery);

The test-cases

  • array(' &#//+%20@۞' => 'a') ---> '%20%26%23%2F%2F%2B%2520%40%DB%9E=a'
  • array('a' => '1', 'b' => '2', 'c' => '3') ---> 'a=1&b=2&c=3'
  • array('a' => array('b' => '2', 'c' => '3')) ---> 'a[b]=2&a[c]=3'
  • array('a' => '1', 'b' => array('c' => array()), 'd' => NULL, 'e' => '', 'f' => '2') ---> 'a=1&e=&f=2'
  • array('foo' => NULL) ---> ''

And here is my current version:

function drupal_http_build_query(array $query, $parent = '') {
  $params = array();
  
  foreach ($query as $key => $value) {
    $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));
    
    if (!$value && $value !== '') {
     
    } elseif (is_array($value)) {
      // Recurse into children.
      $children_query = drupal_http_build_query($value, $key);
      if ($children_query) {
        $params[] = $children_query;
      }
    } else {
      // For better readability of paths in query strings, we decode slashes.
      $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
    }
  }
  return implode('&', $params);
}

I don't have much time right now to learn the patch-system, if I'm going on to contribute I will learn it.

Sawascwoolf’s picture

I'm not sure right now if it is really desired to use the http_build_query() as reference.

Is there a need for passing:

  • (bool) FALSE values?
  • empty strings
  • empty array

Assume a form with a huge amount of checkboxes, with the current version every checkbox-value is added to the query string with value=0, this results in really long query-strings which don't hold information.

I suggest to ignore the values (see list above) and don't append anything at all for this case.

As this could cause notices of type Undefined index: it must be discussed if it should be backported to D7 in this way.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

quietone’s picture

Version: 8.9.x-dev » 9.3.x-dev
Issue summary: View changes
Status: Needs work » Needs review
Issue tags: +Bug Smash Initiative
StatusFileSize
new3.15 KB
new973 bytes
new2.67 KB

Updated IS and rerolled patch.

The last submitted patch, 57: 1425588-57-fail.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 57: 1425588-57.patch, failed testing. View results

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

abhaypai made their first commit to this issue’s fork.

abhaypai’s picture

Issue summary: View changes
Issue tags: +Needs tests
StatusFileSize
new1.71 KB
new1.87 KB

Landing here from Bug smash initiative.

Few updates from end:

  1. I am still able to replicate this issue on D11.
  2. Added patch compatible with 11.x-dev version with interdiff from #57
  3. Adding Needs Tests tag for help in solving failures in test
  4. Did initial level of testing and works as expected.
  5. Updated IS by adding sample for for Drupal 11
abhaypai’s picture

Status: Needs work » Needs review

Updating status to Needs Review after test run:

Need some advice or suggestion for following doubts.

  1. Wondering why phpstan is throwing error elseif (!isset($value)) { i didnt change line for this and its giving error
  2. Its weird that 1806 test run failed and at the same time it also shows core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php passed

Will create MR, once above issue is addressed.

smustgrave’s picture

Status: Needs review » Needs work

Think it's saying your change caused that check to be redundant. So may need to look at the solution and make sure.

dcam made their first commit to this issue’s fork.

dcam’s picture

I added #65 to the MR.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.