Cómo obtener un nonce único para cada solicitud Ajax en WordPress

29 ene 2013, 04:59:40
Vistas: 17.6K
Votos: 12

He visto varias discusiones sobre hacer que WordPress regenere un nonce único para solicitudes Ajax posteriores, pero no logro que WordPress lo haga. Cada vez que solicito lo que debería ser un nuevo nonce, obtengo el mismo nonce de WordPress. Entiendo el concepto de nonce_life en WP e incluso configurarlo de otra manera, pero eso no me ha ayudado.

No genero el nonce en el objeto JS en el encabezado mediante localización, lo hago en mi página de visualización. Puedo hacer que mi página procese la solicitud Ajax, pero cuando solicito un nuevo nonce a WP en la devolución de llamada, obtengo el mismo nonce y no sé qué estoy haciendo mal... Finalmente quiero extender esto para que pueda haber múltiples elementos en la página, cada uno con la capacidad de agregar/eliminar, por lo que necesito una solución que permita múltiples solicitudes Ajax posteriores desde una página.

(Y debo decir que he puesto toda esta funcionalidad en un plugin, por lo que la "página de visualización" frontal es en realidad una función incluida en el plugin...)

functions.php: localizo, pero no creo un nonce aquí

wp_localize_script('myjs', 'ajaxVars', array('ajaxurl' => 'admin-ajax.php')));

JS que realiza la llamada:

$("#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;
            ... otras cosas
         }
      }
  });
});

PHP que recibe:

function myFaves() {
   $ajaxNonce = 'myplugin_myaction_nonce_' . $postID;
   if (!wp_verify_nonce($_POST['nonce'], $ajaxNonce))
      exit('¡Lo siento!');

   // Obtener varias variables POST y hacer otras cosas...

   // Preparar respuesta JSON y generar nuevo nonce único
   $newNonce = wp_create_nonce('myplugin_myaction_nonce_' . $postID . '_' 
       . str_replace('.', '', gettimeofday(true)));
   $response['newNonce'] = $newNonce;

   // También permitir que la página se procese si no hay capacidad JS/Ajax
   } else {
      header("Location: " . $_SERVER["HTTP_REFERER"];
   }
   die();
}

Función PHP de visualización en el frontend, entre la cual está:

$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 . '">Mi Enlace</a>';

En este punto estaría realmente agradecido por cualquier pista o indicación para hacer que WP regenere un nonce único para cada nueva solicitud Ajax...


ACTUALIZACIÓN: He resuelto mi problema. Los fragmentos de código anteriores son válidos, sin embargo cambié la creación de $newNonce en la devolución de llamada PHP para agregar una cadena de microsegundos que asegure que sea único en solicitudes Ajax posteriores.

5
Comentarios

Con un vistazo muy rápido: ¿Estás creando el nonce después de recibirlo (al mostrarlo)? ¿Por qué no lo creas durante la llamada a localize?

kaiser kaiser
29 ene 2013 07:36:36

jQuery está usando el nonce inicial del atributo "data-nonce" en el enlace a#myelement, y la idea es que la página pueda procesarse tanto por Ajax como por sí misma. Me pareció que crear el nonce una sola vez mediante la llamada a localize lo excluiría del procesamiento sin JS, pero podría estar equivocado en eso. De cualquier forma Wordpress me devuelve el mismo nonce...

Tim Tim
29 ene 2013 17:17:11

También: ¿No sería que al poner el nonce en la llamada a localize impediría tener múltiples elementos en una página donde cada uno podría tener un nonce único para una solicitud Ajax?

Tim Tim
29 ene 2013 18:11:09

Crear el nonce dentro del localize lo generaría y lo haría disponible solo para ese script. Pero también podrías agregar una cantidad ilimitada de otros valores localizados (con nombres de clave) con nonces separados.

kaiser kaiser
29 ene 2013 21:32:50

Si lo has resuelto, se te anima a publicar tu respuesta y marcarla como "aceptada". Esto ayudará a mantener el sitio organizado. Estaba revisando tu código y hay un par de cosas que no me funcionan, así que refuerzo la petición de que publiques tu solución.

s_ha_dum s_ha_dum
1 feb 2013 17:55:30
Todas las respuestas a la pregunta 2
0

Aquí está una respuesta muy extensa de mi propia pregunta que va más allá de simplemente abordar la cuestión de generar nonces únicos para solicitudes Ajax posteriores. Esta es una función de "agregar a favoritos" que se hizo genérica para los propósitos de la respuesta (mi función permite a los usuarios agregar los IDs de publicaciones de adjuntos de fotos a una lista de favoritos, pero esto podría aplicarse a una variedad de otras funciones que dependen de Ajax). Codifiqué esto como un plugin independiente, y hay algunos elementos faltantes, pero esto debería ser suficiente detalle para transmitir la esencia si deseas replicar la función. Funcionará en una publicación/página individual, pero también funcionará en listas de publicaciones (por ejemplo, puedes agregar/eliminar elementos de favoritos en línea mediante Ajax y cada publicación tendrá su propio nonce único para cada solicitud Ajax). Ten en cuenta que probablemente haya una forma más eficiente y/o más elegante de hacer esto, y actualmente esto funciona solo para Ajax; aún no me he molestado en procesar datos $_POST que no sean Ajax.

scripts.php

/**
* Encolar jQuery para el front-end
*/
function enqueueFavoritesJS()
{
    // Solo mostrar JS de Ajax para Favoritos si el usuario está conectado
    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 (Muchas cosas de depuración que se pueden eliminar)

$(document).ready(function()
{
    // Alternar elemento en Favoritos
    $(".faves-link").click(function(e) {
        // Prevenir autoevaluación de solicitudes y usar Ajax en su lugar
        e.preventDefault();
        var $this = $(this);
        console.log("Iniciando evento de clic...");

        // Obtener variables iniciales de la página
        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("Iniciando Ajax...");

        $.ajax({
            type: "POST",
            dataType: "json",
            url: ajaxVars.ajaxurl,
            data: {
                // Enviar JSON de vuelta a PHP para evaluación
                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("Eliminando de Favoritos...");
                    console.log("Eliminando...");
                } else {
                    $this.text("Agregando a Favoritos...");
                    console.log("Agregando...");
                }
            },
            success: function(response) {
                // Procesar JSON enviado desde PHP
                if(response.type == "success") {
                    console.log("¡Éxito!");
                    console.log("Nuevo nonce: " + response.newNonce);
                    console.log("Nuevo toggle: " + response.theToggle);
                    console.log("Mensaje de PHP: " + response.message);
                    $this.text(response.message);
                    $this.attr("data-toggle", response.theToggle);
                    // Establecer nuevo nonce
                    _ajax_nonce = response.newNonce;
                    console.log("_ajax_nonce ahora es: " + _ajax_nonce);
                } else {
                    console.log("¡Falló!");
                    console.log("Nuevo nonce: " + response.newNonce);
                    console.log("Mensaje de PHP: " + response.message);
                    $this.parent().html("<p>" + response.message + "</p>");
                    _ajax_nonce = response.newNonce;
                    console.log("_ajax_nonce ahora es: " + _ajax_nonce);
                }
            },
            error: function(e, x, settings, exception) {
                // Depuración genérica
                var errorMessage;
                var statusErrorMap = {
                    '400' : "El servidor entendió la solicitud pero el contenido de la solicitud no era válido.",
                    '401' : "Acceso no autorizado.",
                    '403' : "No se puede acceder al recurso prohibido.",
                    '500' : "Error interno del servidor",
                    '503' : "Servicio no disponible"
                };
                if (x.status) {
                    errorMessage = statusErrorMap[x.status];
                    if (!errorMessage) {
                        errorMessage = "Error desconocido.";
                    } else if (exception == 'parsererror') {
                        errorMessage = "Error. Falló el análisis de la solicitud JSON.";
                    } else if (exception == 'timeout') {
                        errorMessage = "La solicitud ha excedido el tiempo de espera.";
                    } else if (exception == 'abort') {
                        errorMessage = "La solicitud fue abortada por el servidor.";
                    } else {
                        errorMessage = "Error desconocido.";
                    }
                    $this.parent().html(errorMessage);
                    console.log("El mensaje de error es: " + errorMessage);
                } else {
                    console.log("¡ERROR!");
                    console.log(e);
                }
            }
        }); // Cerrar $.ajax
    }); // Finalizar evento de clic
});

Funciones (visualización en front-end & acción Ajax)

Para mostrar el enlace de Agregar/Eliminar Favoritos, simplemente llámalo en tu página/publicación mediante:

if (function_exists('myFavoritesLink') {
    myFavoritesLink($user_ID, $post->ID);
}

Función de visualización en front-end:

function myFavoritesLink($user_ID, $postID)
{
    global $user_ID;
    if (is_user_logged_in()) {
        // Establecer valor inicial de alternancia del elemento y texto del enlace - actualizado por callback
        $myUserMeta = get_user_meta($user_ID, 'myMetadata', true);
        if (is_array($myUserMeta['metadata']) && in_array($postID, $myUserMeta['metadata'])) {
            $toggle = "y";
            $linkText = "Eliminar de Favoritos";
        } else {
            $toggle = "n";
            $linkText = "Agregar a Favoritos";
        }

        // Crear nonce solo para Ajax para la solicitud inicial solamente
        // Nuevo nonce devuelto en el callback
        $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 {
        // Usuario no conectado
        echo '<p>Inicia sesión para usar la función de Favoritos.</p>' . "\n";
    }

}

Función de acción Ajax:

/**
* Alternar agregar/eliminar para Favoritos
*/
function toggleFavorites()
{
    if (is_user_logged_in()) {
        // Verificar nonce
        $ajaxNonce = 'myplugin_myaction' . $_POST['post_id'];
        if (! wp_verify_nonce($_POST['_ajax_nonce'], $ajaxNonce)) {
            exit('¡Lo siento!');
        }
        // Procesar variables 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);

        // Inicializar array myUserMeta si no existe
        if ($myUserMeta['myMetadata'] === '' || ! is_array($myUserMeta['myMetadata'])) {
            $myUserMeta['myMetadata'] = array();
        }

        // Alternar el elemento en la lista de Favoritos
        if ($toggle === "y" && in_array($postID, $myUserMeta['myMetadata'])) {
            // Eliminar elemento de la lista de Favoritos
            $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 = "Agregar a Favoritos";
        } else {
            // Agregar elemento a la lista de Favoritos
            $myUserMeta['myMetadata'][] = $postID;
            $newToggle = "y";
            $message = "Eliminar de Favoritos";
        }

        // Preparar para la respuesta
        // Nonce para la próxima solicitud - único con cadena de microtime añadida
        $newNonce = wp_create_nonce('myplugin_myaction_' . $postID . '_' 
            . str_replace('.', '', gettimeofday(true)));
        $updateUserMeta = update_user_meta($userID, 'myMetadata', $myUserMeta);

        // Respuesta a jQuery
        if($updateUserMeta === false) {
            $response['type'] = "error";
            $response['theToggle'] = $toggle;
            $response['message'] = "No se pudieron actualizar tus Favoritos.";
            $response['newNonce'] = $newNonce;
        } else {
            $response['type'] = "success";
            $response['theToggle'] = $newToggle;
            $response['message'] = $message;
            $response['newNonce'] = $newNonce;
        }

        // Procesar con Ajax, de lo contrario procesar con self
        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();
    } // Finalizar is_user_logged_in()
}
add_action('wp_ajax_myFavorites', 'toggleFavorites');
1 feb 2013 19:47:34
1

Realmente tengo que cuestionar el razonamiento detrás de obtener un nuevo nonce para cada solicitud AJAX. El nonce original expirará, pero puede usarse más de una vez hasta que eso ocurra. Hacer que el JavaScript lo reciba a través de AJAX va en contra del propósito, especialmente al proporcionarlo en un caso de error. (El propósito de los nonces es brindar un poco de seguridad para asociar una acción con un usuario dentro de un período de tiempo).

No se supone que mencione otras respuestas, pero soy nuevo y no puedo comentar arriba, así que con respecto a la "solución" publicada, estás obteniendo un nuevo nonce cada vez pero no lo estás usando en la solicitud. Ciertamente sería complicado obtener los mismos microsegundos cada vez para que coincidan con cada nuevo nonce creado de esa manera. El código PHP está verificando contra el nonce original, y el JavaScript está proporcionando el nonce original... así que funciona (porque aún no ha expirado).

28 nov 2013 06:15:41
Comentarios

El problema es que el nonce expira después de ser utilizado y devolverá -1 en la función ajax cada vez. Esto es un problema si estás validando partes de un formulario en PHP y devuelves errores para mostrarlos. El nonce del formulario fue usado, pero ocurrió un error en la validación PHP de los campos, y cuando el formulario se envía nuevamente, esta vez no puede ser verificado y check_ajax_referer devuelve -1, ¡lo cual no es lo que queremos!

Solomon Closson Solomon Closson
26 oct 2016 22:22:04