Ссылка: что такое идеальный пример кода с использованием расширения MySQL?

Это создание ресурса обучения сообщества. Цель состоит в том, чтобы иметь примеры хорошего кода, которые не повторяют ужасных ошибок, которые так часто можно найти в скопированном/вставленном PHP-коде. Я попросил его сделать Community Wiki.

Это не означает, что это конкурс кодирования. Это не о поиске самого быстрого или самого компактного способа сделать запрос - это для обеспечения хорошей читаемой ссылки, особенно для новичков.

Каждый день возникает огромный поток вопросов с действительно плохими фрагментами кода, используя семейство функций mysql_* в Stack Overflow. Хотя обычно лучше всего направлять этих людей к PDO, иногда это не является возможным (например, унаследованным унаследованным программным обеспечением), а также реалистичным ожиданием (пользователи уже используют его в своем проекте).

Общие проблемы с кодом, использующим библиотеку mysql_*, включают:

  • SQL-инъекция в значениях
  • SQL-инъекция в предложениях LIMIT и имена динамических таблиц
  • Нет сообщений об ошибках ( "Почему этот запрос не работает?" )
  • Отказ от ошибок (т.е. ошибки всегда возникают, даже когда код вводится в производство)
  • Взаимодействие с межсайтовыми скриптами (XSS) в выводе значения

Давайте напишем образец кода PHP, который делает следующее с помощью mySQL_ * семейства функций:

  • Примите два значения POST, id (числовые) и name (строка)
  • Сделайте запрос UPDATE в таблице tablename, изменив столбец name в строке с идентификатором id
  • При отказе выйдите любезно, но покажите подробную ошибку только в режиме производства. trigger_error() будет достаточно; альтернативно используйте метод по вашему выбору
  • Вывести сообщение "$name обновлено".

И не показывает не любую из недостатков, перечисленных выше.

Он должен быть максимально простым. Он идеально не содержит никаких функций или классов. Цель состоит не в создании библиотеки для копирования/вставки, а в , чтобы показать минимальное количество необходимых действий, чтобы сделать запрос к базе данных безопасным.

Бонусные баллы за хорошие комментарии.

Цель состоит в том, чтобы сделать этот вопрос ресурсом, с которым пользователь может ссылаться, когда сталкивается с вопросом, у которого есть плохой код (даже если он вообще не является предметом внимания) или сталкивается с неудачным запросом и не знает, как это исправить.

Для предварительного обсуждения 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 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);
}

Ответ 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();
}

Ответ 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 data, remember Rule #0
    //always use htmlspecialchars() to sanitize user 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 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);

И некоторое объяснение комментариев:

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

С помощью mysql_select_db вы можете создавать ошибки, и их будет не так просто найти и исправить.
Например, в некотором script вы установите db1 как базу данных, но в некоторой функции вам нужно установить db2 в качестве базы данных.
После вызова этой функции база данных будет переключена, и все последующие запросы в script будут разбиты или будут разбиты некоторые данные в неправильной базе данных (если имена таблиц и столбцов совпадут).

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

Некоторые имена столбцов могут быть также SQL-ключевыми словами, а с помощью символа `. Кроме того, все строковые значения, вставленные в запрос, должны быть указаны символом .

//always use htmlspecialchars() to sanitize user data in output
Это поможет вам предотвратить XSS-атаки.

Ответ 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 безопасными, но удобными.

Функция, которую я написал давно, и она хорошо меня обслуживала, пока я не перешел на корпоративное стандартное решение на основе ООП.
Для достижения 2 целей: безопасность и простота использования.

Первый из них достигается путем внедрения заполнителей.
Второй из них достигается путем внедрения заполнителей и различных типов результатов.

Функция, безусловно, не идеальна. Некоторые недостатки:

  • Символы
  • no % должны быть помещены в запрос напрямую, используя синтаксис printf.
  • поддержка нескольких соединений не поддерживается.
  • no placeholder для идентификаторов (как и многие другие удобные заполнители).
  • снова, нет идентификатора заполнителя!. "ORDER BY $field" дело должно быть обработано вручную!
  • конечно, реализация ООП была бы намного более гибкой, имея опрятные отличные методы вместо уродливой "моды", а также другие необходимые методы.

Но это хорошо, безопасно и красно, не нужно устанавливать целую библиотеку.

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

Как видно из приведенных выше примеров, основное отличие от всех кодов, когда-либо опубликованных в Stackoverflow, как безопасности, так и процедуры поиска данных инкапсулированы в код функции. Таким образом, никакое ручное связывание, экранирование/кавычка или литье, а также отсутствие ручного поиска данных.

в сочетании с другой вспомогательной функцией

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.