让PHP停止替换“。” $ _GET或$ _POST数组中的字符?


76

如果我.通过$ _GET传递名称中带有PHP变量的PHP,则会自动用_字符替换它们。例如:

<?php
echo "url is ".$_SERVER['REQUEST_URI']."<p>";
echo "x.y is ".$_GET['x.y'].".<p>";
echo "x_y is ".$_GET['x_y'].".<p>";

...输出以下内容:

url is /SpShipTool/php/testGetUrl.php?x.y=a.b
x.y is .
x_y is a.b.

...我的问题是:有什么办法可以阻止我?无法为我的一生弄清我应做的一切

我运行的PHP版本是5.2.4-2ubuntu5.3。


..为什么不将所有点都转换为某种标记(例如,转换为(〜#〜))然后发布呢?当收到var时,您可以将其重新转换为..这是因为有时我们需要发布下划线..如果将所有的“ _”转换为“。”,我们将丢失它们。–
Fernando

从检索查询本身,您可以将user_name(例如“ concat(firstname,'_',lastname))连接为user_name。–
Kaspar Mary

@Kaspar Mary ...数据库设置为具有用户名和状态列,并且用户名存储为firstname.lastname,因此我无法在sql中使用任何concat,因为它们通常与a相连。
罗布

@Crisp感谢您的评论!(at)Rob有趣的问题
hek2mgl

为什么没有删除评论?:)
克里斯·普林斯

Answers:


66

这是PHP.net对其执行原因的解释:

传入变量名称中的点

通常,当将变量传递到脚本中时,PHP不会更改变量的名称。但是,应注意,点(句点,句号)不是PHP变量名称中的有效字符。由于这个原因,请看一下:

<?php
$varname.ext;  /* invalid variable name */
?>

现在,解析器看到的是一个名为$ varname的变量,其后是字符串串联运算符,其后是裸字符串(即与任何已知关键字或保留字都不匹配的未引用字符串)“ ext”。显然,这没有预期的结果。

因此,必须注意PHP将自动用下划线替换输入变量名称中的任何点。

那是来自http://ca.php.net/variables.external

另外,根据此注释,其他字符也将转换为下划线:

以下是PHP转换为_(下划线)的字段名称字符的完整列表(不仅仅是点):

  • chr(32)()(空格)
  • chr(46)(。)(点)
  • chr(91)([](方括号)
  • chr(128)-chr(159)(多种)

因此,看起来好像您坚持使用它,因此您必须使用awnerd的建议将下划线转换回脚本中的点(不过我只是使用str_replace。)


20
这很好地解释了为什么,但是没有回答最初的问题“是否有任何方法可以阻止它”;下面的其他答案确实提供了原始问题的答案。
El Yobo 2013年

1
@ElYobo,@JeremyRuten; 为什么很好的解释?我正在使用PHP 5.4,PHP仍在这样做。我也想知道为什么它不被淘汰。我只能看到保留它的两个原因。register_globals(从5.3版开始不推荐使用),并且为了方便起见而手动执行register globals所做的操作(在这种情况下,应该由这样做的人来负担var名称以使其适合IMO的映射)。
spinkus 2014年

1
我认为是向后兼容吗?很好的一点是,随着寄存器全局变量成为dodo的方式,这种奇怪的“功能”也可能同样发生。
El Yobo 2014年

使用php7时,register globals已经进入日落,但是问题仍然存在。
magallanes

59

回答时间长的问题,但实际上有更好的答案(或解决方法)。PHP使您可以进入原始输入流,因此您可以执行以下操作:

$query_string = file_get_contents('php://input');

这将为您提供查询字符串格式的$ _POST数组,其句点应为应有的句点。

然后,您可以根据需要对其进行解析(根据POSTer的注释

<?php
// Function to fix up PHP's messing up input containing dots, etc.
// `$source` can be either 'POST' or 'GET'
function getRealInput($source) {
    $pairs = explode("&", $source == 'POST' ? file_get_contents("php://input") : $_SERVER['QUERY_STRING']);
    $vars = array();
    foreach ($pairs as $pair) {
        $nv = explode("=", $pair);
        $name = urldecode($nv[0]);
        $value = urldecode($nv[1]);
        $vars[$name] = $value;
    }
    return $vars;
}

// Wrapper functions specifically for GET and POST:
function getRealGET() { return getRealInput('GET'); }
function getRealPOST() { return getRealInput('POST'); }
?>

对于同时包含'。'的OpenID参数非常有用。和“ _”,每个都有特定含义!


5
要使此参数与GET参数一起使用,请替换file_get_contents("php://input")$_SERVER['QUERY_STRING']
Sarel Botha 2012年

您也可以使用$_SERVER['COOKIES']
Marcin

4
这是一个好的开始,但是有一些问题。它不处理数组值(例如foo.bar [] = blarg不会以数组形式出现,它将以标量变量foo.bar []结尾)。由于重新处理所有值,无论它们中是否有句点,它也有很多开销。
El Yobo

请参阅下面的解决方案,该解决方案解决了Rok实施中的问题。
El Yobo 2013年

由于某种原因$ query_string = file_get_contents('php:// input'); 为我返回一个空字符串。
克里斯·普林斯

27

在上面的评论中强调了Johan的实际答案-我只是将我的整个帖子包装在一个顶级数组中,从而完全绕开了该问题,而无需进行大量处理。

在你做的形式

<input name="data[database.username]">  
<input name="data[database.password]">  
<input name="data[something.else.really.deep]">  

代替

<input name="database.username"> 
<input name="database.password"> 
<input name="something.else.really.deep">  

在后处理程序中,只需将其解包即可:

$posdata = $_POST['data'];

对我来说,这是两行更改,因为我的观点完全是模板化的。

仅供参考。我在字段名称中使用点来编辑分组数据树。


4
确实,这是一种非常优雅且实用的解决方案,其好处是保留了表单数据的良好命名空间。
robinmitra

1
这完全解决了问题,应该已经接受了答案。
布莱恩·克鲁格

20

我在2013年暑假期间提出了这个功能,这是一个很好的主意。

例如,它符合标准并具有深层阵列支持a.a[x][b.a]=10。它parse_str()在后台使用了一些有针对性的预处理。

function fix($source) {
    $source = preg_replace_callback(
        '/(^|(?<=&))[^=[&]+/',
        function($key) { return bin2hex(urldecode($key[0])); },
        $source
    );

    parse_str($source, $post);

    $result = array();
    foreach ($post as $key => $val) {
        $result[hex2bin($key)] = $val;
    }
    return $result;
}

然后可以根据来源来像这样应用它:

$_POST   = fix(file_get_contents('php://input'));
$_GET    = fix($_SERVER['QUERY_STRING']);
$_COOKIE = fix($_SERVER['HTTP_COOKIE']);

对于低于5.4的PHP:使用base64_encode代替bin2hexbase64_decode代替hex2bin


谢谢你 如果有时间,也请为深数组a [2] [5]更新它。
Johan 2013年

@Johan,深层数组确实起作用。a[2][5]=10产生array(1) { ["a"]=> array(1) { [2]=> array(1) { [5]=> string(2) "10" } } }
Rok Kralj 2013年

1
哦,我确实知道了,只是做了测试。PHP不会在数组索引内转换点等,只有麻烦的是数组名称的顶层:php_touches_this [nochangeshere] [nochangeshere]。大。谢谢。
Johan 2013年

我希望看到您的基准测试,因为这与我几个月前所做的测试相冲突。另外,我遇​​到了需要处理已发布文件字段中的句点的情况,目前尚无答案。有任何想法吗?
El Yobo

您很快就会看到它们,目前没有时间,但是您可以展示自己的时间。*文件上传需要multipart / form-data类型,该类型不会传递给php:// input。因此,这仍然是非常棘手的事情。请参阅:stackoverflow.com/questions/1361673/get-raw-post-data
Rok Kralj 2013年

7

发生这种情况是因为句点在变量名称中是无效字符,原因是在PHP的实现中非常深入,因此尚无简单的解决方法(尚未解决)。

同时,您可以通过以下方法解决此问题:

  1. 通过php://inputPOST数据或$_SERVER['QUERY_STRING']GET数据访问原始查询数据
  2. 使用转换功能。

下面的转换函数(PHP> = 5.4)将每个键值对的名称编码为十六进制表示形式,然后执行常规parse_str();完成后,它将十六进制名称还原回其原始形式:

function parse_qs($data)
{
    $data = preg_replace_callback('/(?:^|(?<=&))[^=[]+/', function($match) {
        return bin2hex(urldecode($match[0]));
    }, $data);

    parse_str($data, $values);

    return array_combine(array_map('hex2bin', array_keys($values)), $values);
}

// work with the raw query string
$data = parse_qs($_SERVER['QUERY_STRING']);

要么:

// handle posted data (this only works with application/x-www-form-urlencoded)
$data = parse_qs(file_get_contents('php://input'));

但是,如果需要将其用于通过发送的其他内容,而我实际上需要变量中的_怎么办?
罗布

@Rob我根据您的问题添加了输出;它可以按预期工作,因为我不触碰下划线。
2013年

注意:这是一个经过编辑的解决方案,后来复制了我的代码和我的想法(请参阅更改日志)。主持人应将其删除。
Rok Kralj 2014年

显然,您bin2hex()从我这里接受这个想法就足够了,所以我们可以放弃这个毫无意义的争执吗?
2014年

好吧,我只是将其替换为base64编码。效益?除了一点加速之外,什么都没有。为什么要编辑一个完美的解决方案来复制别人的?
Rok Kralj 2014年

5

这种方法是Rok Kralj的改进版本,但需要进行一些调整,以提高效率(避免不必要的回调,对不受影响的键进行编码和解码)并正确处理数组键。

一个与测试要点是可用的和任何意见或建议,欢迎在这里或那里。

public function fix(&$target, $source, $keep = false) {                        
    if (!$source) {                                                            
        return;                                                                
    }                                                                          
    $keys = array();                                                           

    $source = preg_replace_callback(                                           
        '/                                                                     
        # Match at start of string or &                                        
        (?:^|(?<=&))                                                           
        # Exclude cases where the period is in brackets, e.g. foo[bar.blarg]
        [^=&\[]*                                                               
        # Affected cases: periods and spaces                                   
        (?:\.|%20)                                                             
        # Keep matching until assignment, next variable, end of string or   
        # start of an array                                                    
        [^=&\[]*                                                               
        /x',                                                                   
        function ($key) use (&$keys) {                                         
            $keys[] = $key = base64_encode(urldecode($key[0]));                
            return urlencode($key);                                            
        },                                                                     
    $source                                                                    
    );                                                                         

    if (!$keep) {                                                              
        $target = array();                                                     
    }                                                                          

    parse_str($source, $data);                                                 
    foreach ($data as $key => $val) {                                          
        // Only unprocess encoded keys                                      
        if (!in_array($key, $keys)) {                                          
            $target[$key] = $val;                                              
            continue;                                                          
        }                                                                      

        $key = base64_decode($key);                                            
        $target[$key] = $val;                                                  

        if ($keep) {                                                           
            // Keep a copy in the underscore key version                       
            $key = preg_replace('/(\.| )/', '_', $key);                        
            $target[$key] = $val;                                              
        }                                                                      
    }                                                                          
}                                                                              

繁荣对我来说非常有效,谢谢El Yobo / Rok。在CodeIgniter 2.1.3项目中使用它。
xref

我要指出的是,如果传入的值中还没有%20实体,例如“ Some Key = Some Value”,那么此函数的输出为“ Some_Key = Some Value”,则可能对正则表达式进行了调整?
xref

可以对正则表达式进行调整,以捕获未URL编码的空格...但是,如果您的源代码尚未经过URL编码,则可能还会有其他问题,因为处理总是对字符串进行解码和编码,因此parse_str调用将再次进行urldecode。您正在尝试尚未解析的ot解析吗?
El Yobo

感谢您的署名。不过,我可能会警告您,您的代码可能会更糟,因为POST通常只有几百个字节。我喜欢这里的简单性。
Rok Kralj 2013年

1
您是否将这些基准提高了?我很想知道它在哪些情况下变慢,因为我测试过的所有东西的速度都在与您相同的速度之间,而在两倍的速度之间。我怀疑差异在于所测试的事物的类型:)您可以轻松地对我的要旨添加一些时间检查,以查看结果如何,为什么不将您的时间与相同的输入进行比较并发布结果和时间呢?
El Yobo

4

发生这种情况的原因是由于PHP的旧register_globals功能。的。character不是变量名称中的有效字符,因此PHP会将其隐藏在下划线中,以确保兼容性。

简而言之,在URL变量中加句点不是一个好习惯。


1
启用register_globals也不是一个好主意。实际上,如果可能,应立即将其关闭。
08年

1
实际上,register_globals是关闭的,PHP5中的默认设置是关闭的。>。字符不是变量名中的有效字符不幸的是,我不想将其用作变量名(我将其保留为$ _GET词典中的键),因此PHP中的这种“体贴”没有增加任何价值:-(嗯...

register_globals是打开还是关闭都没有关系。PHP仍然执行替换。
杰里米·普里维特

3

如果寻找任何方式 从字面上让PHP停止更换“” $ _GET或$ _POST数组中的字符,那么一种这样的方法是修改PHP的源代码(在这种情况下,它相对简单)。

警告:修改PHP C源代码是一个高级选项!

另请参阅此PHP错误报告,其中建议进行相同的修改。

要进行探索,您需要:

  • 下载 PHP的C源代码
  • 禁用 .更换检查
  • ./configure制作和部署您的自定义PHP版本

源代码更改本身是微不足道的,仅涉及更新一行一半的内容main/php_variables.c

....
/* ensure that we don't have spaces or dots in the variable name (not binary safe) */
for (p = var; *p; p++) {
    if (*p == ' ' /*|| *p == '.'*/) {
        *p='_';
....

注意:与原版相比|| *p == '.' 已被注释掉


示例输出:

给定QUERY_STRING为a.a[]=bb&a.a[]=BB&c%20c=dd<?php print_r($_GET);现在运行将产生:

数组
(
    [aa] =>数组
        (
            [0] => bb
            [1] => BB
        )

    [c_c] => dd
)

笔记:

  • 该补丁仅解决原始问题(它停止点的替换,而不是空格的替换)。
  • 在此修补程序上运行将比脚本级解决方案更快,但是那些纯-.php答案通常还是可取的(因为它们避免更改PHP本身)。
  • 从理论上讲,这里可以使用polyfill方法,并且可以将这些方法组合在一起-使用Cparse_str()和(如果不可用)回退到较慢的方法来测试C级更改。

1
您不应该像现在这样进行操作,但是,您需要为此+1。
Rok Kralj 2013年

2

我对这个问题的解决方案既快速又肮脏,但是我仍然喜欢它。我只是想发布在表单上检查过的文件名列表。我曾经base64_encode在标记中对文件名进行编码,然后base64_decode在使用它们之前对其进行了解码。


2

在查看了Rok的解决方案之后,我想出了一个版本,该版本解决了我下面的答案,上面的crb以及Rok的解决方案的局限性。看到我的改进版本


上面@crb的答案是一个好的开始,但是有两个问题。

  • 它会重新处理所有东西,这是过大的;仅那些带有“。”的字段 名称中的字母需要重新处理。
  • 它无法以与本机PHP处理相同的方式处理数组,例如,对于像“ foo.bar []”这样的键。

下面的解决方案现在解决了这两个问题(请注意,自最初发布以来已进行了更新)。这比我在测试中给出的答案快50%,但是将无法处理数据具有相同键(或提取相同键的键,例如foo.bar和foo_bar都提取为foo_bar)的情况。

<?php

public function fix2(&$target, $source, $keep = false) {                       
    if (!$source) {                                                            
        return;                                                                
    }                                                                          
    preg_match_all(                                                            
        '/                                                                     
        # Match at start of string or &                                        
        (?:^|(?<=&))                                                           
        # Exclude cases where the period is in brackets, e.g. foo[bar.blarg]
        [^=&\[]*                                                               
        # Affected cases: periods and spaces                                   
        (?:\.|%20)                                                             
        # Keep matching until assignment, next variable, end of string or   
        # start of an array                                                    
        [^=&\[]*                                                               
        /x',                                                                   
        $source,                                                               
        $matches                                                               
    );                                                                         

    foreach (current($matches) as $key) {                                      
        $key    = urldecode($key);                                             
        $badKey = preg_replace('/(\.| )/', '_', $key);                         

        if (isset($target[$badKey])) {                                         
            // Duplicate values may have already unset this                    
            $target[$key] = $target[$badKey];                                  

            if (!$keep) {                                                      
                unset($target[$badKey]);                                       
            }                                                                  
        }                                                                      
    }                                                                          
}                                                                              

-1。为什么?1.空格%20也是转换为下划线的特殊字符。2.您的代码会预先处理所有数据,因为preg_match_all必须扫描所有数据,即使您说不这样做也是如此。3.你的代码没有在这样的例子:a.b[10]=11
Rok Kralj

您对空间的要求是正确的,谢谢。我的解释已经指出,我的方法无法处理数组,因此我不确定您为什么要指出这一点。 preg_match_all必须“处理”一个字符串,而不是提取并重新处理所有不受影响的键和值,因此您在那里也有些偏离轨道。就是说,您的方法parse_string看起来很有趣,可能会稍作调整:)
El Yobo 2013年

您说您仅提取受影响的键,但是就计算复杂度而言,您没有。您的意思是说,您具有某种随机访问权限,只能提取受影响的密钥,但是即使不存在受影响的密钥,也必须访问整个内存。如果您的帖子中包含100兆的数据,则提取的内容无关紧要,两种方法都是线性的O(n)。实际上,in_array()如上所述,使用该函数会使复杂性变差。
Rok Kralj

我正在查看一次100兆字节,而不是将其拆分(这将使内存立即加倍),然后再次将其拆分(再次加倍),就像在crb的方法中一样。大O表示法根本不考虑内存使用情况,并且此实现也不使用in_array。另外,如果您想运行一些测试,您会注意到上述步骤仍然明显更快;不是O(n)vs O(n ^ 2),但是一种线性方法仍然可以比另一种方法快...而这是;)
El Yobo 2013年

这种方法的另一个主要优点是,当根本不需要完成任何工作时,即在没有键或空格的情况下,速度优势最大。这意味着,如果将其放入以处理所有请求,则开销很小,因为与多次提取和编码所有密钥相比,它几乎没有任何作用(一个正则表达式)。
El Yobo

0

好吧,我下面包含的函数“ getRealPostArray()”并不是一个不错的解决方案,但是它处理数组并支持两个名称:“ alpha_beta”和“ alpha.beta”:

  <input type='text' value='First-.' name='alpha.beta[a.b][]' /><br>
  <input type='text' value='Second-.' name='alpha.beta[a.b][]' /><br>
  <input type='text' value='First-_' name='alpha_beta[a.b][]' /><br>
  <input type='text' value='Second-_' name='alpha_beta[a.b][]' /><br>

而var_dump($ _ POST)产生:

  'alpha_beta' => 
    array (size=1)
      'a.b' => 
        array (size=4)
          0 => string 'First-.' (length=7)
          1 => string 'Second-.' (length=8)
          2 => string 'First-_' (length=7)
          3 => string 'Second-_' (length=8)

var_dump(getRealPostArray())产生:

  'alpha.beta' => 
    array (size=1)
      'a.b' => 
        array (size=2)
          0 => string 'First-.' (length=7)
          1 => string 'Second-.' (length=8)
  'alpha_beta' => 
    array (size=1)
      'a.b' => 
        array (size=2)
          0 => string 'First-_' (length=7)
          1 => string 'Second-_' (length=8)

该函数的价值:

function getRealPostArray() {
  if ($_SERVER['REQUEST_METHOD'] !== 'POST') {#Nothing to do
      return null;
  }
  $neverANamePart = '~#~'; #Any arbitrary string never expected in a 'name'
  $postdata = file_get_contents("php://input");
  $post = [];
  $rebuiltpairs = [];
  $postraws = explode('&', $postdata);
  foreach ($postraws as $postraw) { #Each is a string like: 'xxxx=yyyy'
    $keyvalpair = explode('=',$postraw);
    if (empty($keyvalpair[1])) {
      $keyvalpair[1] = '';
    }
    $pos = strpos($keyvalpair[0],'%5B');
    if ($pos !== false) {
      $str1 = substr($keyvalpair[0], 0, $pos);
      $str2 = substr($keyvalpair[0], $pos);
      $str1 = str_replace('.',$neverANamePart,$str1);
      $keyvalpair[0] = $str1.$str2;
    } else {
      $keyvalpair[0] = str_replace('.',$neverANamePart,$keyvalpair[0]);
    }
    $rebuiltpair = implode('=',$keyvalpair);
    $rebuiltpairs[]=$rebuiltpair;
  }
  $rebuiltpostdata = implode('&',$rebuiltpairs);
  parse_str($rebuiltpostdata, $post);
  $fixedpost = [];
  foreach ($post as $key => $val) {
    $fixedpost[str_replace($neverANamePart,'.',$key)] = $val;
  }
  return $fixedpost;
}

0

我想使用crb来重新创建整个$_POST数组,但是请记住,您仍然必须确保在客户端和服务器端都正确地编码和解码。重要的是要了解何时字符真正无效并且真正有效。另外的人应该还是总是使用它与之前逃脱客户端数据的任何数据库命令,无一例外

<?php
unset($_POST);
$_POST = array();
$p0 = explode('&',file_get_contents('php://input'));
foreach ($p0 as $key => $value)
{
 $p1 = explode('=',$value);
 $_POST[$p1[0]] = $p1[1];
 //OR...
 //$_POST[urldecode($p1[0])] = urldecode($p1[1]);
}
print_r($_POST);
?>

我建议仅将其用于个别情况,但我不确定将其放在主头文件顶部的不利之处。


0

我当前的解决方案(基于上一主题的答复):

function parseQueryString($data)
{
    $data = rawurldecode($data);   
    $pattern = '/(?:^|(?<=&))[^=&\[]*[^=&\[]*/';       
    $data = preg_replace_callback($pattern, function ($match){
        return bin2hex(urldecode($match[0]));
    }, $data);
    parse_str($data, $values);

    return array_combine(array_map('hex2bin', array_keys($values)), $values);
}

$_GET = parseQueryString($_SERVER['QUERY_STRING']);

请添加一些说明,这些说明将对每个阅读您的答案的人都有帮助。
Prafulla Kumar Sahu 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.