¿Cómo obtener un nonce único para cada solicitud de Ajax?

11

He visto un par de discusiones sobre cómo hacer que Wordpress regenere un nonce único para solicitudes posteriores de Ajax, pero por mi vida no puedo hacer que Wordpress lo haga, cada vez que solicito lo que creo que debería ser un nuevo nonce, recibo el mismo nonce de Wordpress. Entiendo el concepto de nonce_life de WP e incluso establecerlo en otra cosa, pero eso no me ha ayudado.

No genero el nonce en el objeto JS en el encabezado a través de la localización, lo hago en mi página de visualización. Puedo hacer que mi página procese la solicitud de Ajax, pero cuando solicito un nuevo nonce de WP en la devolución de llamada, recibo el mismo nonce y no sé qué estoy haciendo mal ... Finalmente quiero extienda 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 posteriores de Ajax desde una página.

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

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

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

Llamando a 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
         }
      }
  });
});

Recibiendo PHP:

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

   // Get various POST vars and do some other stuff...

   // Prep JSON response & generate new, unique nonce
   $newNonce = wp_create_nonce('myplugin_myaction_nonce_' . $postID . '_' 
       . str_replace('.', '', gettimeofday(true)));
   $response['newNonce'] = $newNonce;

   // Also let the page process itself if there is no JS/Ajax capability
   } else {
      header("Location: " . $_SERVER["HTTP_REFERER"];
   }
   die();
}

Función de visualización PHP frontend, entre las cuales se encuentra:

$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 . '">My Link</a>';

En este punto, estaría realmente agradecido por cualquier pista o indicador para que WP regenere un nonce único para cada nueva solicitud de 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 de PHP para agregar una cadena de microsegundos para garantizar que sea única en las solicitudes posteriores de Ajax.

Tim
fuente
De un vistazo muy breve: ¿Estás creando el nonce después de recibirlo (en exhibición)? ¿Por qué no lo está creando durante la llamada de localización?
Kaiser
JQuery está utilizando el nonce inicial del atributo "data-nonce" en el enlace a # myelement, y la idea es que la página pueda ser procesada por Ajax o por sí misma. Me pareció que crear el nonce una vez a través de la llamada localize lo excluiría del procesamiento que no sea JS, pero podría estar equivocado al respecto. De cualquier manera, Wordpress me devuelve el mismo nonce ...
Tim
Además: ¿No colocar el nonce en la llamada localize evitaría que uno tenga varios elementos en una página donde cada elemento podría tener un nonce único para una solicitud de Ajax?
Tim
Crear el nonce dentro de la localización crearía y lo haría disponible para ese script. Pero también puede agregar una cantidad ilimitada de otros valores de localización (clave) con nonces separados.
kaiser
Si lo ha resuelto, se le anima a publicar su respuesta y marcarla como "aceptada". Ayudará a mantener el sitio organizado. Estaba jugando con su código y un par de cosas no funcionan para mí, así que duplique esa solicitud para que publique su solución.
s_ha_dum

Respuestas:

6

Aquí hay una respuesta muy larga de mi propia pregunta que va más allá de solo abordar la cuestión de generar nonces únicos para solicitudes posteriores de Ajax. Esta es una función de "agregar a favoritos" que se hizo genérica a los efectos de la respuesta (mi función permite a los usuarios agregar las ID de publicación de archivos 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 complemento independiente, y faltan algunos elementos, pero esto debería ser suficiente detalle para proporcionar la esencia si desea replicar la función. Funcionará en una publicación / página individual, pero también funcionará en listas de publicaciones (por ejemplo, puede agregar / eliminar elementos a favoritos en línea a través de Ajax y cada publicación tendrá su propio nonce único para cada solicitud de Ajax). Tenga en cuenta que hay '

scripts.php

/**
* Enqueue front-end jQuery
*/
function enqueueFavoritesJS()
{
    // Only show Favorites Ajax JS if user is logged in
    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()
{
    // Toggle item in Favorites
    $(".faves-link").click(function(e) {
        // Prevent self eval of requests and use Ajax instead
        e.preventDefault();
        var $this = $(this);
        console.log("Starting click event...");

        // Fetch initial variables from the page
        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("Starting Ajax...");

        $.ajax({
            type: "POST",
            dataType: "json",
            url: ajaxVars.ajaxurl,
            data: {
                // Send JSON back to PHP for eval
                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("Removing from Favorites...");
                    console.log("Removing...");
                } else {
                    $this.text("Adding to Favorites...");
                    console.log("Adding...");
                }
            },
            success: function(response) {
                // Process JSON sent from PHP
                if(response.type == "success") {
                    console.log("Success!");
                    console.log("New nonce: " + response.newNonce);
                    console.log("New toggle: " + response.theToggle);
                    console.log("Message from PHP: " + response.message);
                    $this.text(response.message);
                    $this.attr("data-toggle", response.theToggle);
                    // Set new nonce
                    _ajax_nonce = response.newNonce;
                    console.log("_ajax_nonce is now: " + _ajax_nonce);
                } else {
                    console.log("Failed!");
                    console.log("New nonce: " + response.newNonce);
                    console.log("Message from PHP: " + response.message);
                    $this.parent().html("<p>" + response.message + "</p>");
                    _ajax_nonce = response.newNonce;
                    console.log("_ajax_nonce is now: " + _ajax_nonce);
                }
            },
            error: function(e, x, settings, exception) {
                // Generic debugging
                var errorMessage;
                var statusErrorMap = {
                    '400' : "Server understood request but request content was invalid.",
                    '401' : "Unauthorized access.",
                    '403' : "Forbidden resource can't be accessed.",
                    '500' : "Internal Server Error",
                    '503' : "Service Unavailable"
                };
                if (x.status) {
                    errorMessage = statusErrorMap[x.status];
                    if (!errorMessage) {
                        errorMessage = "Unknown Error.";
                    } else if (exception == 'parsererror') {
                        errorMessage = "Error. Parsing JSON request failed.";
                    } else if (exception == 'timeout') {
                        errorMessage = "Request timed out.";
                    } else if (exception == 'abort') {
                        errorMessage = "Request was aborted by server.";
                    } else {
                        errorMessage = "Unknown Error.";
                    }
                    $this.parent().html(errorMessage);
                    console.log("Error message is: " + errorMessage);
                } else {
                    console.log("ERROR!!");
                    console.log(e);
                }
            }
        }); // Close $.ajax
    }); // End click event
});

Funciones (pantalla frontal y acción Ajax)

Para generar el enlace Agregar o quitar favoritos, simplemente instálelo en su página / publicación a través de:

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

Función de visualización frontal:

function myFavoritesLink($user_ID, $postID)
{
    global $user_ID;
    if (is_user_logged_in()) {
        // Set initial element toggle value & link text - udpated by callback
        $myUserMeta = get_user_meta($user_ID, 'myMetadata', true);
        if (is_array($myUserMeta['metadata']) && in_array($postID, $myUserMeta['metadata'])) {
            $toggle = "y";
            $linkText = "Remove from Favorites";
        } else {
            $toggle = "n";
            $linkText = "Add to Favorites";
        }

        // Create Ajax-only nonce for initial request only
        // New nonce returned in 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 {
        // User not logged in
        echo '<p>Sign in to use the Favorites feature.</p>' . "\n";
    }

}

Función de acción Ajax:

/**
* Toggle add/remove for Favorites
*/
function toggleFavorites()
{
    if (is_user_logged_in()) {
        // Verify nonce
        $ajaxNonce = 'myplugin_myaction' . $_POST['post_id'];
        if (! wp_verify_nonce($_POST['_ajax_nonce'], $ajaxNonce)) {
            exit('Sorry!');
        }
        // Process POST vars
        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);

        // Init myUserMeta array if it doesn't exist
        if ($myUserMeta['myMetadata'] === '' || ! is_array($myUserMeta['myMetadata'])) {
            $myUserMeta['myMetadata'] = array();
        }

        // Toggle the item in the Favorites list
        if ($toggle === "y" && in_array($postID, $myUserMeta['myMetadata'])) {
            // Remove item from Favorites list
            $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 = "Add to Favorites";
        } else {
            // Add item to Favorites list
            $myUserMeta['myMetadata'][] = $postID;
            $newToggle = "y";
            $message = "Remove from Favorites";
        }

        // Prep for the response
        // Nonce for next request - unique with microtime string appended
        $newNonce = wp_create_nonce('myplugin_myaction_' . $postID . '_' 
            . str_replace('.', '', gettimeofday(true)));
        $updateUserMeta = update_user_meta($userID, 'myMetadata', $myUserMeta);

        // Response to jQuery
        if($updateUserMeta === false) {
            $response['type'] = "error";
            $response['theToggle'] = $toggle;
            $response['message'] = "Your Favorites could not be updated.";
            $response['newNonce'] = $newNonce;
        } else {
            $response['type'] = "success";
            $response['theToggle'] = $newToggle;
            $response['message'] = $message;
            $response['newNonce'] = $newNonce;
        }

        // Process with Ajax, otherwise process with 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();
    } // End is_user_logged_in()
}
add_action('wp_ajax_myFavorites', 'toggleFavorites');
Tim
fuente
3

Realmente tengo que cuestionar el razonamiento detrás de obtener un nuevo nonce para cada solicitud de ajax. El nonce original caducará, pero puede usarse más de una vez hasta que lo haga. Hacer que JavaScript lo reciba a través de ajax anula el propósito, especialmente al proporcionarlo en un caso de error. (El propósito de nonces ser un poco de seguridad para asociar una acción con un usuario dentro de un marco de tiempo).

Se supone que no debo mencionar otras respuestas, pero soy nuevo y no puedo comentar más arriba, por lo que en lo que respecta a la "solución" publicada, está recibiendo un nuevo nonce cada vez pero no lo está utilizando en la solicitud. Ciertamente sería complicado obtener los microsegundos iguales 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á suministrando el nonce original ... por lo que funciona (porque aún no ha expirado).

Joy Reynolds
fuente
1
El problema es que nonce caduca después de que se usa y devolverá -1 en la función ajax después de cada vez. Este es un problema si está validando partes de un formulario en PHP y devuelve errores para imprimir. Se utilizó el formulario nonce, pero en realidad se produjo un error en la validación de php de los campos, y cuando el formulario se vuelve a enviar, esta vez, no se puede verificar y check_ajax_refererdevuelve -1, ¡lo cual no es lo que queremos!
Solomon Closson