Access multiple GET parameters in PHP without bracket notation - php

PHP automatically creates arrays in $_GET, when the parameter name is followed by [] or [keyname].
However for a public API I'd love to have the same behaviour without explicit brackets in the URL. For example, the query
?foo=bar&foo=baz
should result in a $_GET (or similar) like this:
$_GET["foo"] == array("bar", "baz");
Is there any possibility to get this behaviour in PHP easily? I.e., not parsing $_SERVER['QUERY_STRING'] myself or preg_replacing = with []= in the query string before feeding it to parse_str()?

There's no built in way to support ?foo=bar&foo=baz.
Daniel Morell proposed a solution which manually parses the URL string and iteratively builds up an array when multiple instances of the parameter exist, or returns a string when only one parameter exists (ie; matches the default behaviour).
It supports both types of URLs, with and without a bracket:
?foo=bar&foo=baz // works
?foo[]=bar&foo[]=baz // works
/**
* Parses GET and POST form input like $_GET and $_POST, but without requiring multiple select inputs to end the name
* in a pair of brackets.
*
* #param string $method The input method to use 'GET' or 'POST'.
* #param string $querystring A custom form input in the query string format.
* #return array $output Returns an array containing the input keys and values.
*/
function bracketless_input( $method, $querystring=null ) {
// Create empty array to
$output = array();
// Get query string from function call
if( $querystring !== null ) {
$query = $querystring;
// Get raw POST data
} elseif ($method == 'POST') {
$query = file_get_contents('php://input');
// Get raw GET data
} elseif ($method == 'GET') {
$query = $_SERVER['QUERY_STRING'];
}
// Separerate each parameter into key value pairs
foreach( explode( '&', $query ) as $params ) {
$parts = explode( '=', $params );
// Remove any existing brackets and clean up key and value
$parts[0] = trim(preg_replace( '(\%5B|\%5D|[\[\]])', '', $parts[0] ) );
$parts[0] = preg_replace( '([^0-9a-zA-Z])', '_', urldecode($parts[0]) );
$parts[1] = urldecode($parts[1]);
// Create new key in $output array if param does not exist.
if( !key_exists( $parts[0], $output ) ) {
$output[$parts[0]] = $parts[1];
// Add param to array if param key already exists in $output
} elseif( is_array( $output[$parts[0]] ) ) {
array_push( $output[$parts[0]], $parts[1] );
// Otherwise turn $output param into array and append current param
} else {
$output[$parts[0]] = array( $output[$parts[0]], $parts[1] );
}
}
return $output;
}

you can try something like this:
foreach($_GET as $slug => $value) {
#whatever you want to do, for example
print $_GET[$slug];
}

Related

How to get PHP value from input using Tagify

I have a problem with submitting a PHP form using jQuery Tagify.
If I add 2 tags like John and Thomas, then I'm getting $_POST['tag'] as:
'[{"value":"John"}, {"value":"Thomas"}]'
How I can change my $_POST['tag'] to get this POST as: John,Thomas?
var_dump(implode(', ', array_column(json_decode($_POST['tag']), 'value')));
First you decode the JSON coming in $_POST['tag'] into an array/object structure. array_column gives you the flat array with the values. Then you join it separated by commas (implode).
Yes, the square brackets is in the way. In fact, tagify-js outputs an array of json objects. So json_decode function doesn't work either. It is necessary to prepare the output.
Here is the function I implemented to save the input value. It converts them to an array of values.
function br_bookmarks_tagify_json_to_array( $value ) {
// Because the $value is an array of json objects
// we need this helper function.
// First check if is not empty
if( empty( $value ) ) {
return $output = array();
} else {
// Remove squarebrackets
$value = str_replace( array('[',']') , '' , $value );
// Fix escaped double quotes
$value = str_replace( '\"', "\"" , $value );
// Create an array of json objects
$value = explode(',', $value);
// Let's transform into an array of inputed values
// Create an array
$value_array = array();
// Check if is array and not empty
if ( is_array($value) && 0 !== count($value) ) {
foreach ($value as $value_inner) {
$value_array[] = json_decode( $value_inner );
}
// Convert object to array
// Note: function (array) not working.
// This is the trick: create a json of the values
// and then transform back to an array
$value_array = json_decode(json_encode($value_array), true);
// Create an array only with the values of the child array
$output = array();
foreach($value_array as $value_array_inner) {
foreach ($value_array_inner as $key=>$val) {
$output[] = $val;
}
}
}
return $output;
}
}
Usage:
br_bookmarks_tagify_json_to_array( $_POST['tag'] );
Hope it helps others.

Are two URLs identical? Ignore the param order

I have two URLs and am looking for the best way to decide if they are identical.
Example:
$url1 = 'http://example.com/page.php?tab=items&msg=3&sort=title';
$url2 = 'http://example.com/page.php?tab=items&sort=title&msg=3';
In the two URLs only the sort and msg param are switched, so I consider them equal.
However I cannot simply do if ( $url1 == $url2 ) { … }
I'm having a list of URLs and need to find duplicates, so the code should be fast as it is run inside a loop. (As a side note: The domain/page.php will always be same, it's only about finding URLs by params.)
Maybe like this?
function compare_url($url1, $url2){
return (parse_url($url1,PHP_URL_QUERY) == parse_url($url2,PHP_URL_QUERY));
}
It's not as easy as it might sound to find out if an URI is identical or not, especially as you take the query parameter into account here.
One common way to do this is to have a function that normalizes the URL and then compare the normalized URIs:
$url1 = 'http://example.com/page.php?tab=items&msg=3&sort=title';
$url2 = 'http://example.com/page.php?tab=items&sort=title&msg=3';
var_dump(url_nornalize($url1) == url_nornalize($url2)); # bool(true)
Into such a normalization function you put in your requirements. First of all the URL should be normalized according to the specs:
function url_nornalize($url, $separator = '&')
{
// normalize according RFC 3986
$url = new Net_URL2($url);
$url->normalize();
And then you can take care of additional normalization steps, for example, sorting the sub-parts of the query:
// normalize query if applicable
$query = $url->getQuery();
if (false !== $query) {
$params = explode($separator, $query);
sort($params);
$query = implode($separator, $params);
$url->setQuery($query);
}
Additional steps can be though of, like removing default parameters or not allowed ones, or duplicate ones and what not.
Finally the string of normalized URL is returned
return (string) $url;
}
Using an array/hash-map for the parameters isn't bad as well, I just wanted to show an alternative approach. Full example:
<?php
/**
* http://stackoverflow.com/questions/27667182/are-two-urls-identical-ignore-the-param-order
*/
require_once 'Net/URL2.php';
function url_nornalize($url, $separator = '&')
{
// normalize according RFC 3986
$url = new Net_URL2($url);
$url->normalize();
// normalize query if applicable
$query = $url->getQuery();
if (false !== $query) {
$params = explode($separator, $query);
// remove empty parameters
$params = array_filter($params, 'strlen');
// sort parameters
sort($params);
$query = implode($separator, $params);
$url->setQuery($query);
}
return (string)$url;
}
$url1 = 'http://EXAMPLE.com/p%61ge.php?tab=items&&&msg=3&sort=title';
$url2 = 'http://example.com:80/page.php?tab=items&sort=title&msg=3';
var_dump(url_nornalize($url1) == url_nornalize($url2)); # bool(true)
To make sure that both URLs are identical, we need to compare at least 4 elements:
The scheme(e.g. http, https, ftp)
The host, i.e. the domain name of the URL
The path, i.e. the "file" that was requested
Query parameters of the request.
Some notes:
(1) and (2) are case-insensitive, which means http://example.org is identical to HTTP://EXAMPLE.ORG.
(3) can have leading or trailing slashes, that should be ignored: example.org is identical to example.org/
(4) could include parameters in varying order.
We can safely ignore anchor text, or "fragment" (#anchor after the query parameters), as they are only parsed by the browser.
URLs can also include port-numbers, a username and password - I think we can ignore those elements, as they are used so rarely that they do not need to be checked here.
Solution:
Here's a complete function that checks all those details:
/**
* Check if two urls match while ignoring order of params
*
* #param string $url1
* #param string $url2
* #return bool
*/
function do_urls_match( $url1, $url2 ) {
// Parse urls
$parts1 = parse_url( $url1 );
$parts2 = parse_url( $url2 );
// Scheme and host are case-insensitive.
$scheme1 = strtolower( $parts1[ 'scheme' ] ?? '' );
$scheme2 = strtolower( $parts2[ 'scheme' ] ?? '' );
$host1 = strtolower( $parts1[ 'host' ] ?? '' );
$host2 = strtolower( $parts2[ 'host' ] ?? '' );
if ( $scheme1 !== $scheme2 ) {
// URL scheme mismatch (http <-> https): URLs are not identical.
return false;
}
if ( $host1 !== $host2 ) {
// Different host (domain name): Not identical.
return false;
}
// Remvoe leading/trailing slashes, url-decode special characters.
$path1 = trim( urldecode( $parts1[ 'path' ] ?? '' ), '/' );
$path2 = trim( urldecode( $parts2[ 'path' ] ?? '' ), '/' );
if ( $path1 !== $path2 ) {
// The request-path is different: Different URLs.
return false;
}
// Convert the query-params into arrays.
parse_str( $parts1['query'] ?? '', $query1 );
parse_str( $parts2['query'] ?? '', $query2 );
if ( count( $query1 ) !== count( $query2 ) ) {
// Both URLs have a differnt number of params: They cannot match.
return false;
}
// Only compare the query-arrays when params are present.
if (count( $query1 ) > 0 ) {
ksort( $query1 );
ksort( $query2 );
if ( array_diff( $query1, $query2 ) ) {
// Query arrays have differencs: URLs do not match.
return false;
}
}
// All checks passed, URLs are identical.
return true;
} // End do_urls_match()
Test cases:
$base_urls = [
'https://example.org/',
'https://example.org/index.php?sort=asc&field=id&filter=foo',
'http://EXAMPLE.com/p%61ge.php?tab=items&&&msg=3&sort=title',
];
$compare_urls = [
'https://example.org/',
'https://Example.Org',
'https://example.org/index.php?sort=asc&&field=id&filter=foo',
'http://example.org/index.php?sort=asc&field=id&filter=foo',
'https://company.net/page.php?sort=asc&field=id&filter=foo',
'https://example.org/index.php?sort=asc&&&field=id&filter=foo#anchor',
'https://example.org/index.php?field=id&filter=foo&sort=asc',
'http://example.com:80/page.php?tab=items&sort=title&msg=3',
];
foreach ( $base_urls as $url1 ) {
printf( "\n\n%s", $url1 );
foreach ( $compare_urls as $url2 ) {
if (do_urls_match( $url1, $url2 )) {
printf( "\n [MATCHES] %s", $url2 );
}
}
}
/* Output:
https://example.org/
[MATCHES] https://example.org/
[MATCHES] https://Example.Org
https://example.org/index.php?sort=asc&field=id&filter=foo
[MATCHES] https://example.org/index.php?sort=asc&&field=id&filter=foo
[MATCHES] https://example.org/index.php?sort=asc&&&field=id&filter=foo#anchor
[MATCHES] https://example.org/index.php?field=id&filter=foo&sort=asc
http://EXAMPLE.com/p%61ge.php?tab=items&&&msg=3&sort=title
[MATCHES] http://example.com:80/page.php?tab=items&sort=title&msg=3
*/

PHP truncates parameter name after squared brackets?

If the parameter name of an URL contains squared brackets (no matter if they are url-encoded or not), any following character is ignored by PHP and is not made available to the script (e.g. via $_GET).
Example request:
...showget.php?xxx%5B1%5Dyyy=42
$_GET:
Array
(
[xxx] => Array
(
[1] => 42
)
)
As you can see, "yyy" didn't made it. ^^
(tested in PHP 5.3.28 & 5.5.10)
Does somebody know if such URLs are even syntactically valid?
Is this behaviour intended and documented (could not find anything) or should it rather be considered as a bug within PHP?
If intended: Can i change the respective behaviour by changing a special setting or so?
Thanks!
This is intended behaviour. As you saw in your example, PHP builds arrays from GET parameters if it can, that is, if it finds square brackets in the variable name. There's a FAQ entry showing how that can sometimes be useful.
In your case, PHP sees xxx[1]yyy=42 as xxx[1]=42 which becomes an array.
As far as I know, PHP's query string parsing can not be changed, but you could use $_SERVER['QUERY_STRING'] and parse that yourself.
[] in query key names is a hint to PHP that you want an array, e.g.
example.com?foo[]=bar&foo[]=baz
produces
$_GET = array(
'foo' => array('bar', 'baz')
);
This notation also lets you specify keys in the url:
example.com?foo[bar]=baz
$_GET = array(
'foo' => array('bar' => 'baz')
);
But once you get into this array notation, you're not permitted to have anything in the keyname AFTER the [] portion:
example.com?foo[bar]baz=qux
$_GET = array(
'foo' => array('bar' => 'qux')
);
Basically it's related to PHP syntax, where somethign like
$foo['bar']baz
would be a syntax error.
Came across this myself earlier too, and wrote a function to handle it from POST data, but it shouldn't take much to get it to use GET data instead. Such a large amount of code simply to account for the fact that PHP doesn't account for nested square brackets ;-)
/**
* Gets the _POST data with correct handling of nested brackets:
* "path[to][data[nested]]=value"
* "path"
* -> "to"
* -> "data[nested]" = value
* #return array
*/
function get_real_post() {
function set_nested_value(&$arr, &$keys, &$value) {
$key = array_shift($keys);
if (count($keys)) {
// Got deeper to go
if (!array_key_exists($key, $arr)) {
// Make sure we can get deeper if we've not hit this key before
$arr[$key] = array();
} elseif (!is_array($arr[$key])) {
// This should never be relevant for well formed input data
throw new Exception("Setting a value and an array with the same key: $key");
}
set_nested_value($arr[$key], $keys, $value);
} elseif (empty($key)) {
// Setting an Array
$arr[] = $value;
} else {
// Setting an Object
$arr[$key] = $value;
}
}
$input = array();
$parts = array();
$pairs = explode("&", file_get_contents("php://input"));
foreach ($pairs as $pair) {
$key_value = explode("=", $pair, 2);
preg_match_all("/([a-zA-Z0-9]*)(?:\[([^\[\]]*(?:(?R)[^\[\]]*)*)\])?/", urldecode($key_value[0]), $parts);
$keys = array($parts[1][0]);
if (!empty($parts[2][0])) {
array_pop($parts[2]); // Remove the blank one on the end
$keys = array_merge($keys, $parts[2]);
}
$value = urldecode($key_value[1]);
if ($value == "true") {
$value = true;
} else if ($value == "false") {
$value = false;
} else if (is_numeric($value)) {
if (strpos($value, ".") !== false) {
$num = floatval($value);
} else {
$num = intval($value);
}
if (strval($num) === $value) {
$value = $num;
}
}
set_nested_value($input, $keys, $value);
}
return $input;
}

How to combine query strings in PHP

Given a url, and a query string, how can I get the url resulting from the combination of the query string with the url?
I'm looking for functionality similar to .htaccess's qsa. I realize this would be fairly trivial to implement completely by hand, however are there built-in functions that deal with query strings which could either simplify or completely solve this?
Example input/result sets:
Url="http://www.example.com/index.php/page?a=1"
QS ="?b=2"
Result="http://www.example.com/index.php/page?a=1&b=2"
-
Url="page.php"
QS ="?b=2"
Result="page.php?b=2"
How about something that uses no PECL extensions and isn't a huge set of copied-and-pasted functions? It's still a tad complex because you're splicing together two query strings and want to do it in a way that isn't just $old .= $new;
We'll use parse_url to extract the query string from the desired url, parse_str to parse the query strings you wish to join, array_merge to join them together, and http_build_query to create the new, combined string for us.
// Parse the URL into components
$url = 'http://...';
$url_parsed = parse_url($url);
$new_qs_parsed = array();
// Grab our first query string
parse_str($url_parsed['query'], $new_qs_parsed);
// Here's the other query string
$other_query_string = 'that=this&those=these';
$other_qs_parsed = array();
parse_str($other_query_string, $other_qs_parsed);
// Stitch the two query strings together
$final_query_string_array = array_merge($new_qs_parsed, $other_qs_parsed);
$final_query_string = http_build_query($final_query_string_array);
// Now, our final URL:
$new_url = $url_parsed['scheme']
. '://'
. $url_parsed['host']
. $url_parsed['path']
. '?'
. $final_query_string;
You can get the query string part from url using:
$_SERVER['QUERY_STRING']
and then append it to url normally.
If you want to specify your own custom variables in query string, have a look at:
http_build_query
This is a series of functions taken from the WordPress "framework" that will do it, but this could quite well be too much:
add_query_arg()
/**
* Retrieve a modified URL query string.
*
* You can rebuild the URL and append a new query variable to the URL query by
* using this function. You can also retrieve the full URL with query data.
*
* Adding a single key & value or an associative array. Setting a key value to
* emptystring removes the key. Omitting oldquery_or_uri uses the $_SERVER
* value.
*
* #since 1.0
*
* #param mixed $param1 Either newkey or an associative_array
* #param mixed $param2 Either newvalue or oldquery or uri
* #param mixed $param3 Optional. Old query or uri
* #return string New URL query string.
*/
public function add_query_arg() {
$ret = '';
if ( is_array( func_get_arg(0) ) ) {
$uri = ( #func_num_args() < 2 || false === #func_get_arg( 1 ) ) ? $_SERVER['REQUEST_URI'] : #func_get_arg( 1 );
} else {
$uri = ( #func_num_args() < 3 || false === #func_get_arg( 2 ) ) ? $_SERVER['REQUEST_URI'] : #func_get_arg( 2 );
}
if ( $frag = strstr( $uri, '#' ) ) {
$uri = substr( $uri, 0, -strlen( $frag ) );
} else {
$frag = '';
}
if ( preg_match( '|^https?://|i', $uri, $matches ) ) {
$protocol = $matches[0];
$uri = substr( $uri, strlen( $protocol ) );
} else {
$protocol = '';
}
if ( strpos( $uri, '?' ) !== false ) {
$parts = explode( '?', $uri, 2 );
if ( 1 == count( $parts ) ) {
$base = '?';
$query = $parts[0];
} else {
$base = $parts[0] . '?';
$query = $parts[1];
}
} elseif ( !empty( $protocol ) || strpos( $uri, '=' ) === false ) {
$base = $uri . '?';
$query = '';
} else {
$base = '';
$query = $uri;
}
parse_str( $query, $qs );
if ( get_magic_quotes_gpc() )
$qs = format::stripslashes_deep( $qs );
$qs = format::urlencode_deep( $qs ); // this re-URL-encodes things that were already in the query string
if ( is_array( func_get_arg( 0 ) ) ) {
$kayvees = func_get_arg( 0 );
$qs = array_merge( $qs, $kayvees );
} else {
$qs[func_get_arg( 0 )] = func_get_arg( 1 );
}
foreach ( ( array ) $qs as $k => $v ) {
if ( $v === false )
unset( $qs[$k] );
}
$ret = http_build_query( $qs, '', '&' );
$ret = trim( $ret, '?' );
$ret = preg_replace( '#=(&|$)#', '$1', $ret );
$ret = $protocol . $base . $ret . $frag;
$ret = rtrim( $ret, '?' );
return $ret;
}
stripslashes_deep()
/**
* Navigates through an array and removes slashes from the values.
*
* If an array is passed, the array_map() function causes a callback to pass the
* value back to the function. The slashes from this value will removed.
*
* #since 1.0
*
* #param array|string $value The array or string to be stripped
* #return array|string Stripped array (or string in the callback).
*/
function stripslashes_deep( $value ) {
return is_array( $value ) ? array_map( array('self', 'stripslashes_deep'), $value ) : stripslashes( $value );
}
urlencode_deep()
/**
* Navigates through an array and encodes the values to be used in a URL.
*
* Uses a callback to pass the value of the array back to the function as a
* string.
*
* #since 1.0
*
* #param array|string $value The array or string to be encoded.
* #return array|string $value The encoded array (or string from the callback).
*/
public function urlencode_deep( $value ) {
return is_array($value) ? array_map( array('self', 'urlencode_deep'), $value) : urlencode($value);
}
THere is no built-in function to do this. However, you can use this function from http PECL extension,
http://usphp.com/manual/en/function.http-build-url.php
For example,
$url = http_build_url("http://www.example.com/index.php/page?a=1",
array(
"b" => "2"
)
);
So what happens if the urls conflict? If both urls contain a b= component in the querystring? You'd need to decided which holds sway.
Here's a chunk of code that does what you want, parsing each string as a url, then extracting the query url part and implode() ing them back together.
$url="http://www.example.com/index.php/page?a=1";
$qs ="?b=2";
$url_parsed = parse_url($url);
$qs_parsed = parse_url($qs);
$args = array(
$url_parsed['query'],
$qs_parsed['query'],
);
$new_url = $url_parsed['scheme'];
$new_url .= '://';
$new_url .= $url_parsed['host'];
$new_url .= $url_parsed['path'];
$new_url .= '?';
$new_url .= implode('&', $args);
print $new_url;

trim url(query string)

I have a query string like the one given below:
http://localhost/project/viewMember.php?sort=Y2xhc3M=&class=Mw==&page=9
Now variable: page in query string can be anywhere within the query string either in beginning or middle or at end (like ?page=9 or &page=9& or &page=9).
Now, I need to remove page=9 from my query string and get a valid query string.
Lots of ways this could be done, including regex (as seen below). This is the most robust method I can think of, although it is more complex than the other methods.
Use parse_url to get the query string from the url (or write your own function).
Use parse_str to convert the query string into an array
unset the key that you don't want
Use http_build_query to reassemble the array into a query string
Then reconstruct the Url (if required)
Try:
preg_replace('/page=\d+/', '', $url);
Tried writing a function for this. Seems to work:
<?php
$url = "http://localhost/project/viewMember.php?sort=Y2xhc3M=&class=Mw==&page=9";
// prints http://localhost/project/viewMember.php?sort=Y2xhc3M=&class=Mw==
print changeURL($url) . "\n";
$url = "http://localhost/project/viewMember.php?sort=Y2xhc3M=&page=9&class=Mw==";
// prints http://localhost/project/viewMember.php?sort=Y2xhc3M=&class=Mw==
print changeURL($url) . "\n";
function changeURL($url)
{
$arr = parse_url($url);
$query = $arr['query'];
$pieces = explode('&',$query);
for($i=0;$i<count($pieces);$i++)
{
if(preg_match('/^page=\d+/',$pieces[$i]))
unset($pieces[$i]);
}
$query = implode('&',$pieces);
return "$arr[scheme]://$arr[host]$arr[user]$arr[pass]$arr[path]?$query$arr[fragment]";
}
?>
I created these two functions:
function cleanQuery($queryLabels){
// Filter all items in $_GET which are not in $queryLabels
if(!is_array($queryLabels)) return;
foreach($_GET as $queryLabel => $queryValue)
if(!in_array($queryLabel, $queryLabels) || ($queryValue == ''))
unset($_GET[$queryLabel]);
ksort($_GET);
}
function amendQuery($queryItems = array()){
$queryItems = array_merge($_GET, $queryItems);
ksort($queryItems);
return http_build_query($queryItems);
}
To remove the page part I would use
$_GET = amendQuery(array('page'=>null));
cleanQuery does the opposite. Pass in an array of the terms you want to keep.
function remove_part_of_qs($removeMe)
{
$qs = array();
foreach($_GET as $key => $value)
{
if($key != $removeMe)
{
$qs[$key] = $value;
}
}
return "?" . http_build_query($qs);
}
echo remove_part_of_qs("page");
This should do it, this is my first post on StackOverflow, so go easy!

Categories