【问题标题】:Get PHP to stop replacing '.' characters in $_GET or $_POST arrays?让 PHP 停止替换 '.' $_GET 或 $_POST 数组中的字符?
【发布时间】:2010-09-09 06:42:08
【问题描述】:

如果我通过 $_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。

【问题讨论】:

  • .. 为什么不将所有点转换为某种标记,例如转换为 (~#~) 然后发布呢?收到变量后,您可以将它们重新转换回来..这是因为有时我们需要发布下划线..如果将所有“_”重新转换为“.”s,我们会丢失它们......
  • 从检索查询本身中,您可以将 user_name 连接为“concat(firstname,'_',lastname) as user_name。
  • @Kaspar Mary ... 数据库设置为包含 username 和 status 列,并且用户名存储为 firstname.lastname 所以我不能在 sql 中使用任何 concat,因为它们已经 concat-ed带有 .
  • @Crisp 感谢您的评论! (at) Rob 有趣的问题
  • 为什么没有删除评论? :)

标签: php regex postback


【解决方案1】:

以下是 PHP.net 对其原因的解释:

传入变量名中的点

通常,PHP 不会更改 变量名 传递到脚本中。然而,它 应该注意的是点(句号, 句号)不是一个有效的字符 PHP 变量名。由于这个原因, 看看吧:

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

现在,什么 解析器看到的是一个名为的变量 $varname,后跟字符串 连接运算符,后跟 裸字符串(即不带引号的字符串 不匹配任何已知密钥或 保留字)'ext'。显然,这 没有达到预期的结果。

因此,重要的是 注意 PHP 会自动 替换传入变量中的任何点 带下划线的名称。

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

另外,根据this comment,这些其他字符被转换为下划线:

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

  • chr(32) ( )(空格)
  • chr(46) (.)(点)
  • chr(91) ([)(左方括号)
  • chr(128) - chr(159)(各种)

所以看起来你被它困住了,所以你必须使用dawnerd's suggestion 将脚本中的下划线转换回点(不过我只使用str_replace。)

【讨论】:

  • 这是对为什么的一个很好的解释,但没有回答“有没有办法让它停止”的原始问题;下面的其他答案确实提供了对原始问题的答案。
  • @ElYobo,@JeremyRuten;很好的解释为什么?我正在使用 PHP 5.4,而 PHP 仍在这样做。我也很想知道 为什么 它还没有被弃用。我只能看到保留它的两个原因; register_globals (自 5.3 起已弃用),并且为了方便手动执行 register globals 所做的事情(在这种情况下,执行此操作的人应该承担将 var 名称映射到他们认为适合 IMO 的方式)。
  • 我假设的向后兼容性?好点,随着寄存器全局变量的发展,这种奇怪的“功能”也可以发挥作用。
  • 在 php7 中,注册全局变量已经消失了,但问题仍然存在。
【解决方案2】:

很久以来回答的问题,但实际上有更好的答案(或解决方法)。 PHP 让你在raw input stream,所以你可以做这样的事情:

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

这将为您提供查询字符串格式的 $_POST 数组,它们应该是句点。

然后你可以根据需要解析它(根据POSTer's comment

<?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 参数非常有用和'_',每个都有一定的含义!

【讨论】:

  • 要使用 GET 参数进行这项工作,请将 file_get_contents("php://input") 替换为 $_SERVER['QUERY_STRING']
  • 你也可以使用$_SERVER['COOKIES']对cookies做同样的事情
  • 这是一个好的开始,但也存在一些问题。它不处理数组值(例如 foo.bar[]=blarg 不会以数组的形式结束,它会以名为 foo.bar[] 的标量变量的形式结束)。它在重新处理所有值时也有很多开销,无论它们中是否有句点。
  • 请参阅my solution below,它解决了 Rok 实施的问题。
  • 出于某种原因 $query_string = file_get_contents('php://input');为我返回一个空字符串。
【解决方案3】:

在上面的评论中突出显示 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">  

在 post 处理程序中,打开它:

$posdata = $_POST['data'];

对我来说,这是一个两行更改,因为我的视图完全是模板化的。

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

【讨论】:

  • 确实是非常优雅实用的解决方案,附带的好处是可以很好地命名表单数据。
  • 这完全解决了问题,应该已经接受了答案。
【解决方案4】:

您想要一个符合标准并适用于深度数组(例如:?param[2][5]=10)的解决方案吗?

要修复此问题的所有可能来源,您可以在 PHP 代码的最顶部应用:

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

这个函数的工作是我在 2013 年暑假期间提出的一个好主意。不要被一个简单的正则表达式气馁,它只是抓取所有查询名称,对它们进行编码(因此保留点),然后使用普通的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;
}

p.s.:如果您在项目中使用此解决方案,请在功能上注明@author Rok Kralj

【讨论】:

  • 谢谢。如果你有时间,也请为深度数组 a[2][5] 更新它。
  • @Johan,深度数组确实有效。 a[2][5]=10 产生 array(1) { ["a"]=&gt; array(1) { [2]=&gt; array(1) { [5]=&gt; string(2) "10" } } }
  • 哦,我知道了,确实可以,刚刚测试过。 php 不转换数组索引内的点等,只有数组名的顶层有问题:php_touches_this[nochangeshere][nochangeshere]。伟大的。谢谢。
  • 我很想看看你的基准测试,因为这与我几个月前所做的测试有冲突。另外,我刚刚遇到了需要处理已发布文件字段中的句点的情况,目前还没有答案;有什么想法吗?
  • 您很快就会看到它们,目前没有时间,但您可以展示自己的。 * 文件上传需要 multipart/form-data 类型,它不会传递给 php://input。因此,这仍然是非常骇人听闻的。见:stackoverflow.com/questions/1361673/get-raw-post-data
【解决方案5】:

发生这种情况是因为句点是变量名称中的无效字符,reason 在 PHP 的实现中非常深入,因此(目前)还没有简单的修复方法。

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

  1. 通过php://input 获取POST 数据或$_SERVER['QUERY_STRING'] 获取原始查询数据
  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 我已根据您的问题添加了输出;它按预期工作,因为我没有触摸下划线。
  • 注意:这是一个经过编辑的解决方案,后来复制了我的代码和我的想法(请参阅更改日志)。它应该由版主删除。
  • 显然你可以接受我的bin2hex() 想法,所以我们可以放弃这种毫无意义的争执吗?
  • 好吧,我只是用它来代替 base64 编码。益处?没什么,除了一点加速。为什么要编辑一个完美的解决方案来复制别人的?
【解决方案6】:

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

gist with tests 可用,欢迎在这里或那里提供任何反馈或建议。

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;                                              
        }                                                                      
    }                                                                          
}                                                                              

【讨论】:

  • Boom 这对我来说非常有效,感谢 El Yobo/Rok。在 CodeIgniter 2.1.3 项目中使用它。
  • 我会注意,如果输入的值没有 %20 个实体,例如“Some Key=Some Value”,那么此函数的输出是“Some_Key=Some Value”,也许正则表达式可以调整?
  • 可以调整正则表达式以捕获非 url 编码的空格...但是如果您的源不是 url 编码的,那么可能会有其他问题,因为处理总是解码和编码字符串,然后parse_str 调用将再次进行urldecode。您正在尝试解析尚未编码的内容?
  • 感谢您的署名。不过,我可能会警告您的代码可能性能更差,因为 POST 通常只有几百字节。我更喜欢这里的简单。
  • 您是否在某个地方获得了这些基准?我很想知道它在哪些场景中速度较慢,因为我测试它的所有速度都在与您相同的速度和两倍的速度之间。我怀疑不同之处在于它所测试的事物的类型:) 您可以轻松地在我的要点中添加一些时间检查以查看它是如何进行的,为什么不将您的与相同的输入进行比较并发布结果和时间?
【解决方案7】:

发生这种情况的原因是 PHP 的旧 register_globals 功能。这 。字符不是变量名中的有效字符,因此 PHP 将其转换为下划线以确保兼容性。

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

【讨论】:

  • 开启 register_globals 也不是一个好主意。事实上,如果可能的话,现在应该关闭它。
  • register_globals 实际上是关闭的,这是 PHP5 中的默认设置。 > 。字符不是变量名中的有效字符不幸的是,我不打算将其用作变量名(我将其作为 $_GET 字典中的键),因此 PHP 中的这种“体贴”没有任何价值 :-(嗯……
  • register_globals 是打开还是关闭都没有关系。 PHP 仍然执行替换。
【解决方案8】:

如果寻找 any 方式来字面意思让 PHP 停止替换 '.' $_GET 或 $_POST 数组中的字符,那么一种方法是修改 PHP 的源代码(在这种情况下相对简单)。

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

另请参阅PHP bug report,它建议进行相同的修改。

要探索,您需要:

  • 下载PHP's C source code
  • 禁用. 替换检查
  • ./configuremake 并部署您自定义的 PHP 构建

源代码更改本身是微不足道的,只需更新one half of one line 中的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&amp;a.a[]=BB&amp;c%20c=dd, 运行 &lt;?php print_r($_GET); 现在会产生:

大批 ( [a.a] => 数组 ( [0] => bb [1] => BB ) [c_c] => dd )

注意事项:

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

【讨论】:

  • 你不应该这样做,但是,为努力 +1。
【解决方案9】:

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

【讨论】:

    【解决方案10】:

    在查看了 Rok 的解决方案后,我想出了一个版本,它解决了我在下面的答案、上面的 crb 和 Rok 的解决方案中的限制。见my improved version


    @crb 的回答 above 是一个好的开始,但有几个问题。

    • 它会重新处理所有内容,这太过分了;只有那些有“。”的字段在名称中需要重新处理。
    • 它无法像原生 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.
    • 您对空间的看法是对的,谢谢。我的解释已经指出我的方法不处理数组,所以我不太确定你为什么要指出这一点。 preg_match_all 必须“处理”一个字符串,而不是提取和重新处理所有未受影响的键和值,所以你也有点偏离轨道。也就是说,您使用parse_string 的方法看起来是一种有趣的方法,稍加调整可能会更好:)
    • 你说你只提取受影响的键,但就计算复杂性而言,你没有。你是说你有某种随机访问来只获取受影响的密钥,但即使不存在受影响的密钥,你也必须访问整个内存。如果您有一个包含 100 兆数据的帖子,那么您提取的内容并不重要,这两种方法都是线性的,O(n)。事实上,如上所述,使用 in_array() 函数会使复杂性变得更糟。
    • 我正在查看 100megs 一次,而不是将其拆分(这会立即使内存加倍),然后再次拆分(再次加倍),就像我正在比较的 crb 的方法一样。大 O 表示法根本没有考虑内存使用情况,而且这个实现也不使用in_array。此外,如果您想运行一些测试,您会注意到上述方法仍然明显更快;不是 O(n) 与 O(n^2),但一种线性方法仍然可以比另一种更快......而这个是 ;)
    • 这种方法的另一个主要优点是,当根本没有工作要做时,即没有为键提供句点或空格时,速度优势最大;这意味着如果您将其放入处理所有请求,它的开销最小,因为它几乎不做任何工作(一个正则表达式),而不是多次提取和编码所有键。
    【解决方案11】:

    好吧,我在下面包含的函数“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;
    }
    

    【讨论】:

      【解决方案12】:

      使用 crb's 我想重新创建整个 $_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);
      ?>
      

      我建议仅将其仅用于个别情况,顺便说一下,我不确定将其放在主头文件顶部的负面影响。

      【讨论】:

        【解决方案13】:

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

        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']);
        

        【讨论】:

        • 请添加一些解释,这将对所有阅读您答案的人有所帮助。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-09-22
        • 1970-01-01
        • 1970-01-01
        • 2012-10-30
        • 1970-01-01
        相关资源
        最近更新 更多