Как получить уникальный одноразовый код (nonce) для каждого Ajax-запроса?
Я видел несколько обсуждений о том, как заставить WordPress генерировать уникальный одноразовый код (nonce) для последующих Ajax-запросов, но никак не могу добиться этого — каждый раз, когда я запрашиваю, казалось бы, новый nonce, WordPress возвращает мне тот же самый код. Я понимаю концепцию nonce_life в WordPress и даже пробовал изменять это значение, но это не помогло.
Я не генерирую nonce в JS-объекте в заголовке через локализацию — я делаю это на своей странице отображения. Мне удалось заставить страницу обрабатывать Ajax-запрос, но когда я запрашиваю новый nonce из WordPress в callback-функции, я получаю тот же самый код, и я не понимаю, что делаю не так... В конечном итоге я хочу расширить эту функциональность, чтобы на странице могло быть несколько элементов, каждый с возможностью добавления/удаления — поэтому мне нужно решение, которое позволит выполнять несколько последовательных Ajax-запросов с одной страницы.
(И я должен сказать, что всю эту функциональность я поместил в плагин, поэтому frontend "страница отображения" на самом деле является функцией, включенной в плагин...)
functions.php: локализация, но я не создаю nonce здесь
wp_localize_script('myjs', 'ajaxVars', array('ajaxurl' => 'admin-ajax.php')));
Вызывающий JS:
$("#myelement").click(function(e) {
e.preventDefault();
post_id = $(this).data("data-post-id");
user_id = $(this).data("data-user-id");
nonce = $(this).data("data-nonce");
$.ajax({
type: "POST",
dataType: "json",
url: ajaxVars.ajaxurl,
data: {
action: "myfaves",
post_id: post_id,
user_id: user_id,
nonce: nonce
},
success: function(response) {
if(response.type == "success") {
nonce = response.newNonce;
... other stuff
}
}
});
});
Принимающий PHP:
function myFaves() {
$ajaxNonce = 'myplugin_myaction_nonce_' . $postID;
if (!wp_verify_nonce($_POST['nonce'], $ajaxNonce))
exit('Извините!');
// Получаем различные POST-переменные и делаем другие действия...
// Подготавливаем JSON-ответ и генерируем новый уникальный nonce
$newNonce = wp_create_nonce('myplugin_myaction_nonce_' . $postID . '_'
. str_replace('.', '', gettimeofday(true)));
$response['newNonce'] = $newNonce;
// Также позволяем странице обработать себя, если нет JS/Ajax-возможностей
} else {
header("Location: " . $_SERVER["HTTP_REFERER"];
}
die();
}
Frontend PHP функция отображения, среди которой есть:
$nonce = wp_create_nonce('myplugin_myaction_nonce_' . $post->ID);
$link = admin_url('admin-ajax.php?action=myfaves&post_id=' . $post->ID
. '&user_id=' . $user_ID
. '&nonce=' . $nonce);
echo '<a id="myelement" data-post-id="' . $post->ID
. '" data-user-id="' . $user_ID
. '" data-nonce="' . $nonce
. '" href="' . $link . '">Моя ссылка</a>';
На данном этапе я был бы очень благодарен за любые подсказки или указания по получению уникального nonce для каждого нового Ajax-запроса в WordPress...
ОБНОВЛЕНИЕ: Я решил свою проблему. Приведенные выше фрагменты кода верны, однако я изменил создание $newNonce в PHP callback-функции, добавив строку с микросекундами, чтобы гарантировать уникальность для последующих Ajax-запросов.
Это очень подробный ответ на мой собственный вопрос, который выходит за рамки простого решения проблемы генерации уникальных одноразовых номеров (nonces) для последующих Ajax-запросов. Речь идет о функции "добавить в избранное", которая была сделана универсальной для целей ответа (моя функция позволяет пользователям добавлять ID вложений фотографий в список избранного, но это можно применить и к другим функциям, использующим Ajax). Я реализовал это как отдельный плагин, и в нем отсутствуют некоторые детали — но здесь достаточно информации, чтобы понять суть, если вы хотите повторить эту функциональность. Она будет работать как на отдельных записях/страницах, так и в списках записей (например, вы можете добавлять/удалять элементы из избранного прямо на месте через Ajax, и каждая запись будет иметь свой уникальный одноразовый номер для каждого Ajax-запроса). Учтите, что, вероятно, существует более эффективный и/или элегантный способ сделать это, и пока что это работает только для Ajax — я еще не реализовал обработку данных $_POST без использования Ajax.
scripts.php
/**
* Подключение jQuery для фронтенда
*/
function enqueueFavoritesJS()
{
// Показывать Ajax JS для избранного только для авторизованных пользователей
if (is_user_logged_in()) {
wp_enqueue_script('favorites-js', MYPLUGIN_BASE_URL . 'js/favorites.js', array('jquery'));
wp_localize_script('favorites-js', 'ajaxVars', array('ajaxurl' => admin_url('admin-ajax.php')));
}
}
add_action('wp_enqueue_scripts', 'enqueueFavoritesJS');
favorites.js (Много отладочного кода, который можно удалить)
$(document).ready(function()
{
// Переключение элемента в избранном
$(".faves-link").click(function(e) {
// Предотвращаем стандартное поведение и используем Ajax
e.preventDefault();
var $this = $(this);
console.log("Начало события клика...");
// Получаем переменные со страницы
post_id = $this.attr("data-post-id");
user_id = $this.attr("data-user-id");
the_toggle = $this.attr("data-toggle");
ajax_nonce = $this.attr("data-nonce");
console.log("data-post-id: " + post_id);
console.log("data-user-id: " + user_id);
console.log("data-toggle: " + the_toggle);
console.log("data-nonce: " + ajax_nonce);
console.log("Начало Ajax...");
$.ajax({
type: "POST",
dataType: "json",
url: ajaxVars.ajaxurl,
data: {
// Отправляем JSON обратно в PHP для обработки
action : "myFavorites",
post_id: post_id,
user_id: user_id,
_ajax_nonce: ajax_nonce,
the_toggle: the_toggle
},
beforeSend: function() {
if (the_toggle == "y") {
$this.text("Удаление из избранного...");
console.log("Удаление...");
} else {
$this.text("Добавление в избранное...");
console.log("Добавление...");
}
},
success: function(response) {
// Обрабатываем JSON, полученный от PHP
if(response.type == "success") {
console.log("Успех!");
console.log("Новый nonce: " + response.newNonce);
console.log("Новое состояние: " + response.theToggle);
console.log("Сообщение от PHP: " + response.message);
$this.text(response.message);
$this.attr("data-toggle", response.theToggle);
// Устанавливаем новый nonce
_ajax_nonce = response.newNonce;
console.log("_ajax_nonce теперь: " + _ajax_nonce);
} else {
console.log("Ошибка!");
console.log("Новый nonce: " + response.newNonce);
console.log("Сообщение от PHP: " + response.message);
$this.parent().html("<p>" + response.message + "</p>");
_ajax_nonce = response.newNonce;
console.log("_ajax_nonce теперь: " + _ajax_nonce);
}
},
error: function(e, x, settings, exception) {
// Общая отладка
var errorMessage;
var statusErrorMap = {
'400' : "Сервер понял запрос, но его содержимое было неверным.",
'401' : "Несанкционированный доступ.",
'403' : "Запрещенный ресурс.",
'500' : "Внутренняя ошибка сервера",
'503' : "Сервис недоступен"
};
if (x.status) {
errorMessage = statusErrorMap[x.status];
if (!errorMessage) {
errorMessage = "Неизвестная ошибка.";
} else if (exception == 'parsererror') {
errorMessage = "Ошибка. Не удалось разобрать JSON-запрос.";
} else if (exception == 'timeout') {
errorMessage = "Превышено время ожидания запроса.";
} else if (exception == 'abort') {
errorMessage = "Запрос был прерван сервером.";
} else {
errorMessage = "Неизвестная ошибка.";
}
$this.parent().html(errorMessage);
console.log("Сообщение об ошибке: " + errorMessage);
} else {
console.log("ОШИБКА!!");
console.log(e);
}
}
}); // Закрытие $.ajax
}); // Конец события клика
});
Функции (отображение на фронтенде и Ajax-действие)
Для вывода ссылки "Добавить/Удалить из избранного" просто вызовите ее на своей странице/записи:
if (function_exists('myFavoritesLink') {
myFavoritesLink($user_ID, $post->ID);
}
Функция отображения на фронтенде:
function myFavoritesLink($user_ID, $postID)
{
global $user_ID;
if (is_user_logged_in()) {
// Устанавливаем начальное состояние элемента и текст ссылки - обновляется в ответе
$myUserMeta = get_user_meta($user_ID, 'myMetadata', true);
if (is_array($myUserMeta['metadata']) && in_array($postID, $myUserMeta['metadata'])) {
$toggle = "y";
$linkText = "Удалить из избранного";
} else {
$toggle = "n";
$linkText = "Добавить в избранное";
}
// Создаем одноразовый номер для Ajax-запроса (только для начального запроса)
// Новый nonce возвращается в ответе
$ajaxNonce = wp_create_nonce('myplugin_myaction_' . $postID);
echo '<p class="faves-action"><a class="faves-link"'
. ' data-post-id="' . $postID
. '" data-user-id="' . $user_ID
. '" data-toggle="' . $toggle
. '" data-nonce="' . $ajaxNonce
. '" href="#">' . $linkText . '</a></p>' . "\n";
} else {
// Пользователь не авторизован
echo '<p>Войдите, чтобы использовать функцию избранного.</p>' . "\n";
}
}
Функция Ajax-действия:
/**
* Переключение добавления/удаления из избранного
*/
function toggleFavorites()
{
if (is_user_logged_in()) {
// Проверяем nonce
$ajaxNonce = 'myplugin_myaction' . $_POST['post_id'];
if (! wp_verify_nonce($_POST['_ajax_nonce'], $ajaxNonce)) {
exit('Извините!');
}
// Обрабатываем переменные POST
if (isset($_POST['post_id']) && is_numeric($_POST['post_id'])) {
$postID = $_POST['post_id'];
} else {
return;
}
if (isset($_POST['user_id']) && is_numeric($_POST['user_id'])) {
$userID = $_POST['user_id'];
} else {
return;
}
if (isset($_POST['the_toggle']) && ($_POST['the_toggle'] === "y" || $_POST['the_toggle'] === "n")) {
$toggle = $_POST['the_toggle'];
} else {
return;
}
$myUserMeta = get_user_meta($userID, 'myMetadata', true);
// Инициализируем массив myUserMeta, если он не существует
if ($myUserMeta['myMetadata'] === '' || ! is_array($myUserMeta['myMetadata'])) {
$myUserMeta['myMetadata'] = array();
}
// Переключаем элемент в списке избранного
if ($toggle === "y" && in_array($postID, $myUserMeta['myMetadata'])) {
// Удаляем элемент из списка избранного
$myUserMeta['myMetadata'] = array_flip($myUserMeta['myMetadata']);
unset($myUserMeta['myMetadata'][$postID]);
$myUserMeta['myMetadata'] = array_flip($myUserMeta['myMetadata']);
$myUserMeta['myMetadata'] = array_values($myUserMeta['myMetadata']);
$newToggle = "n";
$message = "Добавить в избранное";
} else {
// Добавляем элемент в список избранного
$myUserMeta['myMetadata'][] = $postID;
$newToggle = "y";
$message = "Удалить из избранного";
}
// Подготовка ответа
// Новый nonce для следующего запроса - уникальный с добавлением микросекунд
$newNonce = wp_create_nonce('myplugin_myaction_' . $postID . '_'
. str_replace('.', '', gettimeofday(true)));
$updateUserMeta = update_user_meta($userID, 'myMetadata', $myUserMeta);
// Ответ для jQuery
if($updateUserMeta === false) {
$response['type'] = "error";
$response['theToggle'] = $toggle;
$response['message'] = "Не удалось обновить список избранного.";
$response['newNonce'] = $newNonce;
} else {
$response['type'] = "success";
$response['theToggle'] = $newToggle;
$response['message'] = $message;
$response['newNonce'] = $newNonce;
}
// Обработка через Ajax, иначе стандартная обработка
if (! empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') {
$response = json_encode($response);
echo $response;
} else {
header("Location: " . $_SERVER["HTTP_REFERER"]);
}
exit();
} // Конец is_user_logged_in()
}
add_action('wp_ajax_myFavorites', 'toggleFavorites');

Мне действительно приходится задуматься о логике получения нового одноразового кода (nonce) для каждого AJAX-запроса. Исходный nonce действительно истекает, но его можно использовать несколько раз до этого момента. Получение nonce через AJAX в JavaScript сводит на нет его предназначение, особенно если он предоставляется в случае ошибки. (Основная цель nonce — обеспечение базовой безопасности, связывая действие с пользователем в рамках определенного временного промежутка.)
Мне не следует упоминать другие ответы, но я новичок и не могу комментировать выше, поэтому относительно предложенного «решения»: вы получаете новый nonce каждый раз, но не используете его в запросе. Было бы крайне сложно добиться совпадения микросекунд каждый раз, чтобы новый nonce соответствовал созданному таким образом. PHP-код проверяет исходный nonce, а JavaScript передает именно его... поэтому это работает (потому что срок его действия еще не истек).

Проблема в том, что nonce истекает после использования и будет возвращать -1 в ajax-функции после каждого раза. Это становится проблемой, если вы проверяете части формы в PHP и возвращаете ошибки для вывода. Nonce формы был использован, но ошибка фактически произошла при валидации полей в PHP, и когда форма отправляется снова, на этот раз она не может быть проверена, и check_ajax_referer
возвращает -1, что не то, что нам нужно!
