Это вторая часть серии о пользовательских таблицах базы данных в WordPress. В первой части мы рассмотрели причины и против использования пользовательских таблиц. Мы рассмотрели некоторые детали, которые необходимо учитывать, — именование столбцов, типы столбцов — а также способы создания таблицы. Прежде чем идти дальше, мы должны обсудить, как безопасно взаимодействовать с этой новой таблицей. В предыдущей статье я рассмотрел общую очистку и проверку — в этом уроке мы рассмотрим это более подробно в контексте баз данных.
Безопасность при взаимодействии с таблицей базы данных имеет первостепенное значение, поэтому мы рассмотрим ее в начале этой серии. Если все сделано неправильно, вы можете оставить свою таблицу открытой для манипуляций с помощью SQL-инъекции. Это может позволить хакеру извлечь информацию, заменить контент или даже изменить поведение вашего сайта — и ущерб, который они могут нанести, не ограничивается вашей пользовательской таблицей.
Предположим, мы хотим разрешить администраторам удалять записи из нашего журнала активности. Распространенная ошибка, которую я видел, заключается в следующем:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
if ( !empty($_GET[‘action’])
&& ‘delete-activity-log’ == $_GET[‘action’]
&& isset($_GET[‘log_id’]) ) {
global $wpdb;
unsafe_delete_log($_GET[‘log_id’]);
}
function unsafe_delete_log( $log_id ){
global $wpdb;
$sql = «DELETE FROM {$wpdb->wptuts_activity_log} WHERE log_id = $log_id»;
$deleted = $wpdb->query( $sql );
}
|
Так что здесь не так? Много: они не проверили разрешения, поэтому любой может удалить журнал активности. Они также не проверяли одноразовые номера, поэтому даже при проверке разрешений администратор может быть обманут в удалении журнала. Все это было описано в этом уроке . Но их третья ошибка unsafe_delete_log()
первые две: unsafe_delete_log()
использует переданное значение в команде SQL, не экранируя его первым. Это оставляет его широко открытым для манипуляций.
Давайте предположим, что его предполагаемое использование
1
|
www.unsafe-site.com?action=delete-activity-log&log_id=7
|
Что делать, если злоумышленник посетил (или обманул администратора): www.unsafe-site.com?action=delete-activity-log&log_id=1;%20DROP%20TABLE%20wp_posts
. log_id
содержит команду SQL, которая впоследствии вводится в $sql
и будет выполняться как:
1
|
DELETE from wp_wptuts_activity_log WHERE log_id=1;
|
Результат: вся таблица wp_posts
удалена. Я видел такой код на форумах — и в результате любой посетитель их сайта может обновить или удалить любую таблицу в своей базе данных.
Если первые две ошибки были исправлены, то это затруднит работу этого типа атак — но не исключено, и это не защитит от «злоумышленника», у которого есть разрешение на удаление журналов активности. Невероятно важно защитить свой сайт от SQL-инъекций. Это также невероятно просто: WordPress предоставляет метод prepare
. В этом конкретном примере:
1
2
3
4
5
|
function safe_delete_log( $log_id ){
global $wpdb;
$sql = $wpdb->prepare(«DELETE from {$wpdb->wptuts_activity_log} WHERE log_id = %d», $log_id);
$deleted = $wpdb->query( $sql )
}
|
Команда SQL теперь будет выполняться как
1
|
DELETE from wp_wptuts_activity_log WHERE log_id=1;
|
Санитарная обработка запросов к базе данных
Большую часть санации можно выполнить исключительно с использованием $wpdb
global, особенно с помощью метода prepare
. Он также предоставляет методы для вставки и обновления данных в таблицы безопасно. Обычно они работают путем замены неизвестного ввода или связывания ввода с заполнителем формата. Этот формат сообщает WordPress, каких данных ожидать:
-
%s
обозначает строку -
%d
обозначает целое число -
%f
обозначает плавание
Мы начнем с рассмотрения трех методов, которые не только очищают запросы, но и создают их для вас.
Вставка данных
WordPress предоставляет метод $wpdb->insert()
. Это обертка для вставки данных в базу данных и обработки санитарных условий. Требуется три параметра:
- Имя таблицы — название таблицы
- Данные — массив данных для вставки в виде столбца -> пары значений
- Форматы — массив форматов для соответствующего значения в массиве данных (например,
%s
,%d
,%f
)
Обратите внимание, что ключи данных должны быть столбцами: если есть ключ, который не соответствует столбцу, может быть выдана ошибка.
В следующих примерах мы явно установили данные — но, конечно, в целом, эти данные были бы получены из пользовательского ввода — так что это может быть что угодно. Как обсуждалось в этой статье, данные должны были быть проверены в первую очередь, чтобы возвратить любые ошибки пользователю — но нам все еще нужно очистить данные перед добавлением их в нашу таблицу. Мы будем смотреть на проверку в следующей статье этой серии.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
global $wpdb;
//
$user_id = 1;
$activity = 1;
$object_id = 1479;
$activity_date = date_i18n(‘Ymd H:i:s’, false, true);
$inserted = $wpdb->insert(
$wpdb->wptuts_activity_log,
array(
‘user_id’=>$user_id,
‘activity’=>$activity,
‘object_id’=>$object_id,
‘activity_date’=> $activity_date,
),
array (
‘%d’,
‘%s’,
‘%d’,
‘%s’,
)
);
if( $inserted ){
$insert_id = $wpdb->insert_id;
}else{
//Insert failed
}
|
Обновление данных
Для обновления данных в базе данных у нас есть $wpdb->update()
. Этот метод принимает пять аргументов:
- Имя таблицы — название таблицы
- Данные — массив данных для обновления в виде пар столбец-> значение
- Где — массив данных для сопоставления в виде пар столбец-> значение
- Формат данных — массив форматов для соответствующих значений данных
- Где Формат — массив форматов для соответствующих значений «где»
Это обновляет все строки, которые соответствуют массиву where, значениями из массива данных. Опять же, как и в случае $wpdb->insert()
ключи массива данных должны соответствовать столбцу. Возвращает false
при ошибке или количестве обновленных строк.
В следующем примере мы обновляем любые записи с идентификатором журнала «14» (который должен быть не более одной записи, так как это наш первичный ключ). Обновляет идентификатор пользователя до 2, а активность — до «отредактировано».
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
global $wpdb;
$user_id=2;
$activity=’edited’;
$log_id = 14;
$updated = $wpdb->update(
$wpdb->wptuts_activity_log,
array(
‘user_id’=>$user_id,
‘activity’=>$activity,
),
array(‘log_id’=>$log_id,),
array( ‘%d’, ‘%s’),
array( ‘%d’),
);
if( $updated ){
//Number of rows updated = $updated
}
|
Удаление
Начиная с 3.4, WordPress также предоставляет метод $wpdb->delete()
для простого (и безопасного) удаления строк. Этот метод принимает три параметра:
- Имя таблицы — название таблицы
- Где — массив данных для сопоставления в виде пар столбец-> значение
- Форматы — массив форматов для соответствующего типа значения (например,
%s
,%d
,%f
)
Если вы хотите, чтобы ваш код был совместим с WordPress pre-3.4, вам нужно использовать метод $wpdb->prepare
для $wpdb->prepare
соответствующего оператора SQL. Пример этого был приведен выше. Метод $wpdb->delete
возвращает количество $wpdb->delete
строк, в противном случае — false, чтобы вы могли определить, было ли удаление успешным.
1
2
3
4
5
6
7
8
9
|
global $wpdb;
$deleted = $wpdb->delete(
$wpdb->wptuts_activity_log,
array(‘log_id’=>14,),
array( ‘%d’),
);
if( $deleted ){
//Number of rows deleted = $deleted
}
|
esc_sql
В свете вышеприведенных методов и более общего $wpdb->prepare()
обсуждаемого далее, эта функция немного избыточна. Он предоставляется в качестве полезной оболочки для $wpdb->escape()
, который сам по себе является прославленной addslashes
. Поскольку обычно более целесообразно и целесообразно использовать вышеупомянутые три метода или $wpdb->prepare()
, вы, вероятно, обнаружите, что вам редко требуется использовать esc_sql()
.
В качестве простого примера:
1
2
|
$activity = ‘commented’;
$sql = «DELETE FROM {$wpdb->wptuts_activity_log} WHERE activity='».esc_sql($activity).»‘;»;
|
Общие запросы
Для общих команд SQL, где (то есть те, которые не вставляют, удаляют или обновляют строки), мы должны использовать метод $wpdb->prepare()
. Он принимает переменное количество аргументов. Первый — это SQL-запрос, который мы хотим выполнить со всеми «неизвестными» данными, замененными местозаполнителями соответствующего формата. Эти значения передаются в качестве дополнительных аргументов в порядке их появления.
Например, вместо:
1
2
3
4
5
6
|
$sql = «SELECT* FROM {$wpdb->wptuts_activity_log}
WHERE user_id = $user_id
AND object_id = $object_id
AND activity = $activity
ORDER BY activity_date $order»;
$logs = $wpdb->get_results($sql);
|
у нас есть
1
2
3
4
5
6
7
|
$sql = $wpdb->prepare(«SELECT* FROM {$wpdb->wptuts_activity_log}
WHERE user_id = %d
AND object_id = %d
AND activity = %s
ORDER BY activity_date %s»,
$user_id,$object_id,$activity, $order );
$logs = $wpdb->get_results($sql);
|
Метод prepare
делает две вещи.
- Он применяет
mysql_real_escape_string()
(илиaddslashes()
) к вставляемым значениям. В частности, это предотвратит выпадение значений, содержащих кавычки, из запроса. - Он применяет
vsprintf()
при добавлении значений в запрос, чтобы убедиться, что они отформатированы надлежащим образом (целые числа являются целыми числами, числами с плавающей точкой и т.д.). Вот почему наш пример в самом начале статьи вычеркнул все, кроме «1».
Более сложные запросы
Вы должны обнаружить, что $wpdb->prepare
, а также методы вставки, обновления и удаления — это все, что вам действительно нужно. Иногда, хотя бывают обстоятельства, когда требуется более «ручной» подход — иногда просто с точки зрения читабельности. Например, предположим, у нас есть неизвестный массив действий, для которых мы хотим все журналы. Мы * могли бы * динамически добавить заполнители %s
к SQL-запросу, но более прямой подход кажется более простым:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
//An unknown array that should contain strings being queried for
$activities = array( … );
//Sanitize the contents of the array
$activities = array_map(‘esc_sql’,$activities);
$activities = array_map(‘sanitize_title_for_query’,$activities);
//Create a string from the sanitised array forming the inner part of the IN( … ) statement
$in_sql = «‘».
//Add this to the query
$sql = «SELECT* FROM $wpdb->wptuts_activity_log WHERE activity IN({$in_sql});»
//Perform the query
$logs = $wpdb->get_results($sql);
|
Идея состоит в том, чтобы применить esc_sql
и sanitize_title_for_query
к каждому элементу в массиве. Первый добавляет косую $wpdb->prepare()
чтобы избежать терминов — аналогично тому, что делает $wpdb->prepare()
. Второй просто применяет sanitize_title_with_dashes()
— хотя поведение может быть полностью изменено с помощью фильтров. Фактический оператор SQL формируется путем внедрения очищенного массива в строку, разделенную запятыми, которая добавляется в часть запроса IN(...)
.
Если ожидается, что массив будет содержать целые числа, тогда достаточно использовать intval()
или absint()
для absint()
каждого элемента в массиве.
Whitelisting
В других случаях белый список может быть уместным. Например, неизвестный ввод может быть массивом столбцов, которые должны быть возвращены в запросе. Поскольку мы знаем, что такое столбцы базы данных, мы можем просто внести их в белый список — удалив любые поля, которые мы не распознаем. Однако, чтобы сделать наш код понятным для человека, мы должны учитывать регистр символов. Для этого мы преобразуем все, что мы получаем, в нижний регистр — поскольку в первой части мы специально использовали имена столбцов в нижнем регистре.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
//An unknown array that should contain columns to be included in the query
$fields = array( … );
//A whitelist of allowed fields
$allowed_fields = array( … );
//Convert fields to lowercase (as our column names are all lower case — see part 1)
$fields = array_map(‘strtolower’,$fields);
//Sanitize by white listing
$fields = array_intersect($fields, $allowed_fields);
//Return only selected fields.
if( empty($fields) ){
$sql = «SELECT* FROM {$wpdb->wptuts_activity_log}»;
}else{
$sql = «SELECT «.implode(‘,’,$fields).» FROM {$wpdb->wptuts_activity_log}»;
}
//Perform the query
$logs = $wpdb->get_results($sql);
|
Белый список также удобен при установке части запроса ORDER BY
(если это установлено пользовательским вводом): данные можно упорядочить только как DESC
или ASC
.
1
2
3
4
5
6
7
8
|
//Unknown user input (expected to be asc or desc)
$order = $_GET[‘order’];
//Allow input to be any, or mixed, case
$order = strtoupper($order);
//Sanitised order value
$order = ( ‘ASC’ == $order ? ‘ASC’ : ‘DEC’ );
|
Как запросы
Операторы SQL LIKE поддерживают использование подстановочных знаков, таких как %
(ноль или более символов) и _
(ровно один символ), при сопоставлении значений с запросом. Например, значение foobar
будет соответствовать любому из запросов:
1
2
3
4
|
SELECT * FROM $wpdb->wptuts_activity_log WHERE activity LIKE ‘foo%’
SELECT * FROM $wpdb->wptuts_activity_log WHERE activity LIKE ‘%bar’
SELECT * FROM $wpdb->wptuts_activity_log WHERE activity LIKE ‘%oba%’
SELECT * FROM $wpdb->wptuts_activity_log WHERE activity LIKE ‘fo_bar%’
|
Однако эти специальные символы могут фактически присутствовать в искомом термине — и, таким образом, чтобы их нельзя было интерпретировать как символы подстановки — нам нужно избегать их. Для этого WordPress предоставляет like_escape()
. Обратите внимание, что это не предотвращает SQL-инъекцию, а только экранирует символы %
и _
: вам все равно нужно использовать esc_sql()
или $wpdb->prepare()
.
1
2
3
4
5
6
7
8
9
|
//Collect term
$term = $_GET[‘activity’];
//Escape any wildcards
$term = like_escape($term);
$sql = $wpdb->prepare(«SELECT* FROM $wpdb->wptuts_activity_log WHERE activity LIKE %s», ‘%’.$term.’%’);
$logs = $wpdb->get_results($sql);
|
Query Wrapper Функции
В примерах, которые мы рассмотрели, мы использовали два других метода $wpdb
:
-
$wpdb->query( $sql )
— выполняет любой заданный ему запрос и возвращает количество затронутых строк. -
$wpdb->get_results( $sql, $ouput)
— выполняет заданный ему запрос и возвращает соответствующий набор результатов (т. е. соответствующие строки).$output
устанавливает формат возвращаемых результатов:-
ARRAY_A
— числовой массив строк, где каждая строка представляет собой ассоциативный массив, снабженный столбцами. -
ARRAY_N
— числовой массив строк, где каждая строка является числовым массивом. -
OBJECT
— числовой массив строк, где каждая строка является объектом строки. По умолчанию. -
OBJECT_K
— ассоциативный массив строк (по значению первого столбца), где каждая строка является ассоциативным массивом.
-
Есть и другие, о которых мы тоже не упомянули:
-
$wpdb->get_row( $sql, $ouput, $row)
— выполняет запрос и возвращает одну строку.$row
устанавливает, какая строка должна быть возвращена, по умолчанию это 0, первая подходящая строка.$output
устанавливает формат строки:-
ARRAY_A
— строка представляет собойcolumn=>value
параcolumn=>value
. -
ARRAY_N
— строка представляет собой числовой массив значений. -
OBJECT
— строка возвращается как объект. По умолчанию.
-
-
$wpdb->get_col( $sql, $column)
— выполняет запрос и возвращает числовой массив значений из указанного столбца.$column
указывает, какой столбец нужно вернуть как целое число. По умолчанию это 0, первый столбец. -
$wpdb->get_var( $sql, $column, $row)
— выполняет запрос и возвращает определенное значение.$row
и$column
такие же, как указано выше, и указывают, какое значение вернуть. Например,1$activities_by_user_1 = $wpdb->get_var(«SELECT COUNT(*) FROM {$wpdb->wptuts_activity_log} WHERE user_id = 1»);
Важно отметить, что эти методы являются просто оболочками для выполнения SQL-запроса и форматирования результата. Они не очищают запрос — поэтому не следует использовать их в одиночку, когда запрос содержит «неизвестные» данные.
Резюме
Мы довольно много рассмотрели в этом уроке — и очистка данных — важная тема для понимания. В следующей статье мы применим его к нашему плагину. Мы рассмотрим разработку набора функций- wp_insert_post()
(похожих на такие функции, как wp_insert_post()
, wp_delete_post()
и т. wp_insert_post()
, wp_delete_post()
добавят слой абстракции между нашим плагином и базой данных.