On my Drupal 8 site I have a custom content type that contains, among other fields, a link field. When I create a node and add the following external URL as one of the values of the link field:

http://www.isfdb.org/cgi-bin/title.cgi?1580

the rendered page contains this mangled query string:

http://www.isfdb.org/cgi-bin/title.cgi?0=

Is this the result of some misconfiguration on my part? How can I get the rendered page to use the correct URL?

I should probably mention that the D8 site is the result of a migration from a D7 site. On the old D7 site, the same URL was rendered correctly. The new Drupal 8 site uses Drupal version 8.6.1.

up vote 1 down vote accepted

I have tracked down the problem deep into Drupal Core itself. The problem actually consists of two bugs, not just one!

EDIT: For the first bug I have submitted a new Drupal Core issue (issue 3007243). For the second bug I have contributed my analysis to the discussions of two existing Drupal Core issues (issue 2987114 and issue 1464244).


Let's start at the beginning. The Link module works with the Drupal Core class Url. At the appropriate time, the Link module gets the URI from the Url object by invoking its toString() method. For external links, this method makes use of the Drupal Core class UnroutedUrlAssembler, by invoking its assemble() method, which in turn invokes another method buildExternalUrl.

Bug #1 is in buildExternalUrl: The method contains this line:

$options['query'] = NestedArray::mergeDeep($parsed['query'], $options['query']);

This is wrong because it doesn't preserve integer keys! Remember that the query parameter in my URL is numeric (1580, see the OP)? The above line causes the 1580 to be discarded, instead the result is the 0 (zero) that we have seen in the mangled query string. The correct line would be this. The all-important thing is the TRUE parameter which tells NestedArray::mergeDeepArray() to preserve integer keys.

$options['query'] = NestedArray::mergeDeepArray(array($parsed['query'], $options['query']), TRUE);

Unfortunately this is not the end of the road. Where does the "=" character in the mangled query string come from? The assemble() method of the UnroutedUrlAssembler class first breaks down the URI into its individual parts like this:

$parsed = UrlHelper::parse($uri);

Then it reassembles the URI, piece by piece. The query string gets reassembled with this line:

$uri .= '?' . UrlHelper::buildQuery($options['query']);

The UrlHelper::buildQuery function generates "key=value" pairs from the entries in the supplied array ($options['query']). It omits the "=value" part if - and only if - the value of an array entry is NULL (the check is made with the PHP function isset()). As I have found out, for my example URL the value of the array entry is NOT NULL, it's an empty string - and that's what causes the "=" character to be generated.

But where is the empty string coming from? The function UrlHelper::parse() uses the PHP built-in function parse_str() to break down the query string into its parts, and that PHP function is the culprit! Here's the proof:

php -r '$query="1580"; $arr = array(); parse_str($query, $arr); echo "isset: " . isset($arr[1580]) . "\n"; echo "+" . $arr[1580] . "+\n"; echo "gettype: " . gettype($arr[1580]) . "\n";'
isset: 1
++
gettype: string

So to ultimately fix the problem, either parse_str() would have to change its buggy query string parsing, or the Drupal function UrlHelper::parse() would have to use some other function to parse query strings.

  • Nice findings! Unfortunately can't upvote twice. – leymannx Oct 17 at 9:26

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.