web-dev-qa-db-fra.com

Comment obtenir un nonce unique pour chaque requête Ajax?

J'ai assisté à quelques discussions sur le fait de faire en sorte que Wordpress régénère un nonce unique pour les requêtes Ajax ultérieures, mais je ne peux vraiment pas faire en sorte que Wordpress le fasse - chaque fois que je demande ce que je pense être une nouvelle Nonce, je reçois le même nonce de Wordpress. Je comprends le concept de nonce_life de WP et le mets même à autre chose, mais cela ne m'a pas aidé.

Je ne génère pas le nonce dans l'objet JS de l'en-tête via la localisation. Je le fais sur ma page d'affichage. Je peux obtenir que ma page traite la demande Ajax, mais lorsque je demande un nouveau nonce à WP dans le rappel, je reçois le même nonce, et je ne sais pas ce que je fais de mal. .. En fin de compte, je souhaite étendre cette fonctionnalité de manière à ce qu'il puisse y avoir plusieurs éléments sur la page, chacun avec la possibilité d'ajouter/de supprimer - j'ai donc besoin d'une solution qui autorisera plusieurs demandes Ajax ultérieures à partir d'une page.

(Et je devrais dire que j'ai mis toute cette fonctionnalité dans un plugin, donc la "page d'affichage" front-end est en fait une fonction incluse dans le plugin ...)

functions.php: localize, mais je ne crée pas de nonce ici

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

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

Réception de 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();
}

Frontend PHP fonction d'affichage, parmi laquelle se trouve:

$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>';

À ce stade, je serais vraiment reconnaissant pour n'importe quel indices ou indications permettant à WP de régénérer un nonce unique pour chaque nouvelle demande Ajax ...


UPDATE: J'ai résolu mon problème. Les extraits de code ci-dessus sont valides. Cependant, j'ai modifié la création $ newNonce dans le callback PHP pour ajouter une chaîne en microsecondes afin de garantir son caractère unique lors des requêtes Ajax suivantes.

10
Tim

Voici une très longue réponse de ma propre question qui va au-delà de la génération de nonces uniques pour les demandes Ajax ultérieures. Il s'agit d'une fonctionnalité "Ajouter aux favoris" devenue générique aux fins de la réponse (cette fonctionnalité permet aux utilisateurs d'ajouter les identifiants de publication des pièces jointes de photo à une liste de favoris, mais cela pourrait s'appliquer à une variété d'autres fonctionnalités qui dépendent Ajax). J'ai codé ceci en tant que plugin autonome, et il manque quelques éléments - mais cela devrait être suffisamment détaillé pour fournir le Gist si vous voulez reproduire la fonctionnalité. Cela fonctionnera sur une publication/page individuelle, mais également dans des listes de publications (par exemple, vous pouvez ajouter/supprimer des éléments aux favoris en ligne via Ajax et chaque publication aura son propre nonce unique pour chaque demande Ajax). Gardez à l'esprit qu'il existe probablement un moyen plus efficace et/ou plus élégant de le faire, et cela ne fonctionne actuellement que pour Ajax - je n'ai pas pris la peine de traiter les données non-Ajax $ _POST.

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 (beaucoup de choses de débogage pouvant être supprimées)

$(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
});

Fonctions (affichage frontal & action Ajax)

Pour afficher le lien Ajouter/Supprimer des favoris, appelez-le simplement sur votre page/poste via:

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

Fonction d'affichage 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";
    }

}

Fonction d'action 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');
5
Tim

Je dois vraiment remettre en question le raisonnement derrière l'obtention d'un nouveau nonce pour chaque demande ajax. Le nonce d'origine expirera, mais il peut être utilisé plusieurs fois jusqu'à ce qu'il disparaisse. Le fait de recevoir le javascript via ajax va à l'encontre du but recherché, notamment en le fournissant en cas d'erreur. (Le but des nonces étant un peu de sécurité pour associer une action à un utilisateur dans un laps de temps).

Je ne suis pas censé mentionner d'autres réponses, mais je suis nouveau et je ne peux pas commenter ci-dessus. Par conséquent, en ce qui concerne la "solution" publiée, vous recevez un nouveau nonce à chaque fois mais vous ne l'utilisez pas dans la demande. Il serait certainement délicat d’obtenir les mêmes microsecondes à chaque fois pour faire correspondre chaque nouveau nonce ainsi créé. Le code PHP se vérifie par rapport au nonce d'origine et le javascript fournit le nonce d'origine ... donc ça marche (car il n'a pas encore expiré).

3
Joy Reynolds