Comment obtenir un nonce unique pour chaque demande Ajax?

11

J'ai vu quelques discussions sur l'obtention de Wordpress pour régénérer un nonce unique pour les demandes Ajax subséquentes, mais pour la vie de moi, je ne peux pas vraiment demander à Wordpress de le faire - chaque fois que je demande ce que je pense être un nouveau nonce, je reçois le même nonce de Wordpress. Je comprends le concept de nonce_life de WP et même de le définir sur autre chose, mais cela ne m'a pas aidé.

Je ne génère pas le nonce dans l'objet JS dans l'en-tête via la localisation - je le fais sur ma page d'affichage. Je peux obtenir ma page pour traiter 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 mal ... En fin de compte, je veux étendre cela afin qu'il puisse y avoir plusieurs éléments sur la page, chacun avec la possibilité d'ajouter / supprimer - j'ai donc besoin d'une solution qui permettra plusieurs demandes Ajax ultérieures à partir d'une page.

(Et je dois dire que j'ai mis toutes ces fonctionnalités dans un plugin, donc la "page d'affichage" frontale est en fait une fonction incluse avec le plugin ...)

functions.php: localiser, 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();
}

Fonction d'affichage PHP frontend, parmi lesquels:

$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 tout indice ou pointeur pour que WP régénère un nonce unique pour chaque nouvelle demande Ajax ...


MISE À JOUR: J'ai résolu mon problème. Les extraits de code ci-dessus sont valides, mais j'ai modifié la création $ newNonce dans le rappel PHP pour ajouter une chaîne de microsecondes afin de garantir qu'elle est unique dans les requêtes Ajax suivantes.

Tim
la source
D'un regard très bref: vous créez le nonce après l'avoir reçu (à l'écran)? Pourquoi ne le créez-vous pas lors de l'appel de localisation?
kaiser
Le jQuery utilise le nonce initial de l'attribut "data-nonce" dans le lien a # myelement, et l'idée est que la page peut être traitée par Ajax ou par elle-même. Il me semblait que créer le nonce une fois via l'appel localize l'exclurait du traitement non JS, mais je peux me tromper. Quoi qu'il en soit, Wordpress me redonne le même nonce ...
Tim
Aussi: le fait de ne pas mettre le nonce dans l'appel localize n'empêcherait-il pas d'avoir plusieurs éléments sur une page où chaque élément pourrait avoir un nonce unique pour une demande Ajax?
Tim
La création du nonce à l'intérieur de la localisation créerait et le rendrait disponible pour ce seul script. Mais vous pouvez également ajouter une quantité illimitée d'autres valeurs de localisation (clé nommée) avec des nonces séparés.
kaiser
Si vous l'avez résolu, nous vous encourageons à poster votre réponse et à la marquer "acceptée". Cela aidera à garder le site organisé. J'étais juste en train de jouer avec votre code et quelques choses ne fonctionnent pas pour moi, alors doublez cette demande pour que vous publiez votre solution.
s_ha_dum

Réponses:

6

Voici une réponse très longue à ma propre question qui va au-delà de la simple question de générer des nonces uniques pour les requêtes Ajax suivantes. Il s'agit d'une fonctionnalité "ajouter aux favoris" qui a été rendue générique aux fins de la réponse (ma fonctionnalité permet aux utilisateurs d'ajouter les ID de publication des pièces jointes à une liste de favoris, mais cela pourrait s'appliquer à une variété d'autres fonctionnalités qui s'appuient sur Ajax). J'ai codé cela en tant que plugin autonome, et il manque quelques éléments - mais cela devrait être suffisamment détaillé pour fournir l'essentiel si vous souhaitez répliquer la fonctionnalité. Cela fonctionnera sur une publication / page individuelle, mais cela fonctionnera également dans les listes de publications (par exemple, vous pouvez ajouter / supprimer des éléments dans les favoris en ligne via Ajax et chaque publication aura son propre nonce unique pour chaque demande Ajax). Gardez à l'esprit qu'il

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 qui peuvent ê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 et action Ajax)

Pour afficher le lien Ajouter / Supprimer des favoris, appelez-le simplement sur votre page / publication 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');
Tim
la source
3

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 le fasse. Avoir le javascript le recevoir via ajax va à l'encontre du but, en particulier le fournir 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 ne peux pas commenter ci-dessus, donc en ce qui concerne la "solution" publiée, vous obtenez un nouveau nonce à chaque fois mais ne l'utilisez pas dans la demande. Il serait certainement difficile d'obtenir les mêmes microsecondes à chaque fois pour correspondre à chaque nouveau nonce créé de cette façon. Le code PHP vérifie par rapport au nonce d'origine, et le javascript fournit le nonce d'origine ... donc cela fonctionne (car il n'a pas encore expiré).

Joy Reynolds
la source
1
Le problème est que nonce expire après son utilisation et retournera -1 dans la fonction ajax après chaque fois. C'est un problème si vous validez des parties d'un formulaire en PHP et renvoyez des erreurs à imprimer. Le formulaire nonce a été utilisé, mais une erreur s'est effectivement produite dans la validation php des champs, et lorsque le formulaire est soumis à nouveau, cette fois, il ne peut pas être vérifié et check_ajax_refererrenvoie -1, ce qui n'est pas ce que nous voulons!
Solomon Closson