参考:啥是使用 MySQL 扩展的完美代码示例? [关闭]

Posted

技术标签:

【中文标题】参考:啥是使用 MySQL 扩展的完美代码示例? [关闭]【英文标题】:Reference: What is a perfect code sample using the MySQL extension? [closed]参考:什么是使用 MySQL 扩展的完美代码示例? [关闭] 【发布时间】:2011-09-06 02:05:38 【问题描述】:

这是为了创建一个社区学习资源。目标是提供不会重复在复制/粘贴 php 代码中经常出现的可怕错误的良好代码示例。我已请求将其制作为社区 Wiki。

并不是一场编码竞赛。这不是为了找到最快或最紧凑的查询方式 - 它是为了提供一个很好的、易读的参考资料,尤其是对于新手。

每天,使用 Stack Overflow 上的 mysql_* 系列函数的 非常糟糕 代码 sn-ps 的问题大量涌入。虽然通常最好将这些人引向 PDO,但有时这既不可能(例如继承的遗留软件)也不是现实的期望(用户已经在他们的项目中使用它)。

使用mysql_* 库的代码的常见问题包括:

值中的 SQL 注入 LIMIT 子句和动态表名中的 SQL 注入 没有错误报告(“为什么这个查询不起作用?”) 错误报告中断(即即使代码投入生产,也总是会出现错误) 值输出中的跨站点脚本 (XSS) 注入

让我们编写一个 PHP 代码示例,使用 mySQL_* family of functions 执行以下操作:

接受两个 POST 值,id(数字)和 name(字符串) 对表 tablename 执行 UPDATE 查询,更改 ID 为 id 的行中的 name 列 失败时,优雅退出,但仅在生产模式下显示详细错误。 trigger_error() 就足够了;或者使用您选择的方法 输出消息“$name更新。”

并且没有显示出上面列出的任何弱点。

应该尽可能简单。理想情况下,它不包含任何函数或类。目标不是创建一个可复制/可粘贴的库,而是展示为确保数据库查询安全而需要做的最少工作。

优秀 cmets 的奖励积分。

我们的目标是让这个问题成为用户在遇到问题代码错误(即使它根本不是问题的焦点)或遇到失败的查询时可以链接到的资源,并且不知道怎么解决。

抢占 PDO 讨论:

是的,通常最好将写这些问题的个人指导给 PDO。当它是一个选项时,我们应该这样做。然而,这并不总是可能的——有时,提问者正在处理遗留代码,或者已经在这个库中取得了长足的进步,现在不太可能改变它。此外,如果使用得当,mysql_* 系列函数是完全安全的。所以请不要在这里回答“使用 PDO”。

【问题讨论】:

【参考方案1】:

我的尝试。试图让它尽可能简单,同时仍然保持一些现实世界的便利。

处理 unicode 并使用松散比较以提高可读性。做个好人;-)

<?php

header('Content-type: text/html; charset=utf-8');
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', 1);
// display_errors can be changed to 0 in production mode to
// suppress PHP's error messages

/*
Can be used for testing
$_POST['id'] = 1;
$_POST['name'] = 'Markus';
*/

$config = array(
    'host' => '127.0.0.1', 
    'user' => 'my_user', 
    'pass' => 'my_pass', 
    'db' => 'my_database'
);

# Connect and disable mysql error output
$connection = @mysql_connect($config['host'], 
    $config['user'], $config['pass']);

if (!$connection) 
    trigger_error('Unable to connect to database: ' 
        . mysql_error(), E_USER_ERROR);


if (!mysql_select_db($config['db'])) 
    trigger_error('Unable to select db: ' . mysql_error(), 
        E_USER_ERROR);


if (!mysql_set_charset('utf8')) 
    trigger_error('Unable to set charset for db connection: ' 
        . mysql_error(), E_USER_ERROR);


$result = mysql_query(
    'UPDATE tablename SET name = "' 
    . mysql_real_escape_string($_POST['name']) 
    . '" WHERE id = "' 
    . mysql_real_escape_string($_POST['id']) . '"'
);

if ($result) 
    echo htmlentities($_POST['name'], ENT_COMPAT, 'utf-8') 
        . ' updated.';
 else 
    trigger_error('Unable to update db: ' 
        . mysql_error(), E_USER_ERROR);

【讨论】:

@Pekka Aw 废话,修复它。谢谢! -1 for #error_reporting(~E_ALL); ... to disable error output - 不好的建议。不能再投反对票了,否则我会为 @ 运营商 -1。 @OZ_ 我明白你关于@ 的观点,但是为什么在生产模式下关闭错误报告不好? @OZ_:为什么这是个问题?在生产模式下,禁用错误输出并没有什么坏处,只要您将其记录在其他地方即可。使用 @ 是隐藏来自 mysql_connect 的讨厌输出的唯一方法。请重新考虑。 @Pekka, @Znarkus, error_reporting(0); 这是一个非常糟糕的建议,因为错误处理程序并不总是会处理错误,因为这个处理程序并不总是会被编码。 ini_set('display_errors',0) - 此变体可用于生产代码,隐藏错误文本,并且仅在处理错误时使用。【参考方案2】:

我决定急于求成,放点东西。这是开始的事情。出错时抛出异常。

function executeQuery($query, $args) 
    $cleaned = array_map('mysql_real_escape_string', $args);

    if($result = mysql_query(vsprintf($query, $cleaned))) 
        return $result;
     else 
        throw new Exception('MySQL Query Error: ' . mysql_error());
    


function updateTablenameName($id, $name) 
    $query = "UPDATE tablename SET name = '%s' WHERE id = %d";

    return executeQuery($query, array($name, $id));


try 
    updateTablenameName($_POST['id'], $_POST['name']);
 catch(Exception $e) 
    echo $e->getMessage();
    exit();

【讨论】:

虽然可行,但对于新手和复制粘贴人员来说太复杂了。 @Aaron - 你读过问题和 cmets 吗?正确阅读它们,只是复制粘贴不是这个问题的解决方案。 关于:失败时,优雅退出,但在生产模式下显示详细错误。也许在echo $e-&gt;getMessage();周围添加一些条件? @Aaron - 我不想为您的回答提及它,而是希望其他人也能效仿。我能理解你自己的答案。 Err,这不是将 $id 值分配给 name 并将 $name 值分配给 id 吗?除了您在调用函数时调用 updateTableName 的事实 updateTablenameName :p。【参考方案3】:
/**
 * Rule #0: never trust users input!
 */

//sanitize integer value
$id = intval($_GET['id']);
//sanitize string value;
$name = mysql_real_escape_string($_POST['name']);
//1. using `dbname`. is better than using mysql_select_db()
//2. names of tables and columns should be quoted by "`" symbol
//3. each variable should be sanitized (even in LIMIT clause)
$q = mysql_query("UPDATE `dbname`.`tablename` SET `name`='".$name."' WHERE `id`='".$id."' LIMIT 0,1 ");
if ($q===false)

    trigger_error('Error in query: '.mysql_error(), E_USER_WARNING);

else

    //be careful! $name contains user's data, remember Rule #0
    //always use htmlspecialchars() to sanitize user's data in output
    print htmlspecialchars($name).' updated';


########################################################################
//Example, how easily is to use set_error_handler() and trigger_error()
//to control error reporting in production and dev-code
//Do NOT use error_reporting(0) or error_reporting(~E_ALL) - each error
//should be fixed, not muted
function err_handler($errno, $errstr, $errfile, $errline)

    $hanle_errors_print = E_ALL & ~E_NOTICE;

    //if we want to print this type of errors (other types we can just write in log-file)
    if ($errno & $hanle_errors_print)
    
        //$errstr can contain user's data, so... Rule #0
        print PHP_EOL.'Error ['.$errno.'] in file '.$errfile.' in line '.$errline
              .': '.htmlspecialchars($errstr).PHP_EOL;
    
    //here you can write error into log-file


set_error_handler('err_handler', E_ALL & ~E_NOTICE & E_USER_NOTICE & ~E_STRICT & ~E_DEPRECATED);

以及对cmets的一些解释:

//1. using `dbname`. is better than using mysql_select_db()

使用 mysql_select_db 可以创建错误,但要查找和修复它们并不容易。 例如,在某些脚本中您将 db1 设置为数据库,但在某些函数中您需要将 db2 设置为数据库。 调用该函数后会切换数据库,脚本中的所有后续查询都将被破坏或破坏错误数据库中的某些数据(如果表名和列名重合)。

//2. names of tables and columns should be quoted by "`" symbol 

某些列名也可以是 SQL 关键字,使用“`”符号会有所帮助。 此外,插入到查询中的所有字符串值都应该用 ' 符号引用。

//always use htmlspecialchars() to sanitize user's data in output 它将帮助您防止XSS-attacks。

【讨论】:

不错,我喜欢!我也喜欢错误处理程序,但对于新手来说,在这里掌握它可能太多了 - 现在可以选择将其删除,还是将其作为“可选”移动到底部? (我把mysqli中的i删掉了) 随意编辑我蹩脚的英语中的错误:) @Pekka 我将尝试更多地关注 mysql_,现在将进行编辑。 我会将字符串连接替换为sprintf。这使得内容更具可读性,并且可能的语法错误(缺少引号和内容)更加明显。 @Yoshi 我觉得是个人喜好,肯定不会更简单。【参考方案4】:
<?  
mysql_connect(); 
mysql_select_db("new"); 
$table = "test"; 
if($_SERVER['REQUEST_METHOD']=='POST') 
  $name = mysql_real_escape_string($_POST['name']); 
  if ($id = intval($_POST['id']))  
    $query="UPDATE $table SET name='$name' WHERE id=$id"; 
   else  
    $query="INSERT INTO $table SET name='$name'"; 
   
  mysql_query($query) or trigger_error(mysql_error()." in ".$query); 
  header("Location: http://".$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF']);  
  exit;  
  
if (!isset($_GET['id'])) 
  $LIST=array(); 
  $query="SELECT * FROM $table";  
  $res=mysql_query($query); 
  while($row=mysql_fetch_assoc($res)) $LIST[]=$row; 
  include 'list.php'; 
 else 
  if ($id=intval($_GET['id']))  
    $query="SELECT * FROM $table WHERE id=$id";  
    $res=mysql_query($query); 
    $row=mysql_fetch_assoc($res); 
    foreach ($row as $k => $v) $row[$k]=htmlspecialchars($v); 
   else  
    $row['name']=''; 
    $row['id']=0; 
   
  include 'form.php'; 
  
?>

form.php

<? include 'tpl_top.php' ?>
<form method="POST">
<input type="text" name="name" value="<?=$row['name']?>"><br>
<input type="hidden" name="id" value="<?=$row['id']?>">
<input type="submit"><br>
<a href="?">Return to the list</a>
</form>
<? include 'tpl_bottom.php' ?>

list.php

<? include 'tpl_top.php' ?>
<a href="?id=0">Add item</a>
<? foreach ($LIST as $row): ?>
<li><a href="?id=<?=$row['id']?>"><?=$row['name']?></a>
<? endforeach ?>
<? include 'tpl_bottom.php' ?>

【讨论】:

【参考方案5】:

看起来我的其他答案没有达到问题的目的。 (这个也不满足一些要求,但可以看出,如果不实现处理占位符的功能,就无法实现安全的解决方案,占位符是安全查询的基石)

因此,这是另一个尝试发布简洁的解决方案以使 mysql 查询安全且方便。

我很久以前编写的一个函数,在我转向基于公司标准 OOP 的解决方案之前,它对我很有用。 有 2 个目标要追求:安全易用性

第一个通过实现占位符实现。 第二个是通过实现占位符和不同的结果类型来实现的。

功能肯定不理想。一些缺点是:

没有% 字符必须直接放在查询中,因为它使用 printf 语法。 不支持多个连接。 没有标识符的占位符(以及许多其他方便的占位符)。 同样,没有标识符占位符!"ORDER BY $field" 案件需人工处理! 当然,OOP 实现会更加灵活,具有简洁的不同方法,而不是丑陋的“模式”变量以及其他必要的方法。

但它很好,安全简洁,无需安装整个库。

function dbget() 
  /*
  usage: dbget($mode, $query, $param1, $param2,...);
  $mode - "dimension" of result:
  0 - resource
  1 - scalar
  2 - row
  3 - array of rows
  */
  $args = func_get_args();
  if (count($args) < 2) 
    trigger_error("dbget: too few arguments");
    return false;
  
  $mode  = array_shift($args);
  $query = array_shift($args);
  $query = str_replace("%s","'%s'",$query); 

  foreach ($args as $key => $val) 
    $args[$key] = mysql_real_escape_string($val);
  

  $query = vsprintf($query, $args);
  if (!$query) return false;

  $res = mysql_query($query);
  if (!$res) 
    trigger_error("dbget: ".mysql_error()." in ".$query);
    return false;
  

  if ($mode === 0) return $res;

  if ($mode === 1) 
    if ($row = mysql_fetch_row($res)) return $row[0];
    else return NULL;
  

  $a = array();
  if ($mode === 2) 
    if ($row = mysql_fetch_assoc($res)) return $row;
  
  if ($mode === 3) 
    while($row = mysql_fetch_assoc($res)) $a[]=$row;
  
  return $a;

?>

使用示例

$name = dbget(1,"SELECT name FROM users WHERE id=%d",$_GET['id']);
$news = dbget(3,"SELECT * FROM news WHERE title LIKE %s LIMIT %d,%d",
              "%$_GET[search]%",$start,$per_page);

从上面的例子可以看出,与***中发布的所有代码的主要区别在于,安全和数据检索例程都封装在函数代码中。因此,无需手动绑定、转义/引用或强制转换,也无需手动数据检索。

结合其他辅助功能

function dbSet($fields,$source=array()) 
  $set = '';
  if (!$source) $source = &$_POST;
  foreach ($fields as $field) 
    if (isset($source[$field])) 
      $set.="`$field`='".mysql_real_escape_string($source[$field])."', ";
    
  
  return substr($set, 0, -2); 

这样使用

$fields = explode(" ","name surname lastname address zip phone regdate");
$_POST['regdate'] = $_POST['y']."-".$_POST['m']."-".$_POST['d'];
$sql = "UPDATE $table SET ".dbSet($fields).", stamp=NOW() WHERE id=%d";
$res = dbget(0,$sql, $_POST['id']);
if (!$res) 
  _503;//calling generic 503 error function

它可能涵盖几乎所有需求,包括来自 OP 的示例案例。

【讨论】:

以上是关于参考:啥是使用 MySQL 扩展的完美代码示例? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

PHP Jquery:聊天系统,啥是理想的框架?

JAVA 啥是设计模式,请举例说明其中一个。

简单来说,啥是工厂?

CMake:啥是可能的目标源类型,它们可以扩展吗?

MYSQL完美解决生产环境改表结构锁表问题

Sql server 真实数据类型,啥是 C# 等价物?