Jak uzyskać unikalną wartość jednorazową dla każdego żądania Ajax?

11

Widziałem kilka dyskusji na temat zmuszenia Wordpressa do zregenerowania unikalnego nonce'a dla kolejnych żądań Ajax, ale przez całe moje życie nie mogę zmusić Wordpressa do zrobienia tego - za każdym razem, gdy pytam, co moim zdaniem powinno być nowe nonce, dostaję ten sam nonce z Wordpress. Rozumiem koncepcję nonce_life WP, a nawet ustawienie jej na coś innego, ale to mi nie pomogło.

Nie generuję nonce w obiekcie JS w nagłówku poprzez lokalizację - robię to na mojej stronie wyświetlania. Mogę sprawić, że moja strona przetworzy żądanie Ajax, ale kiedy poproszę o nową wartość jednorazową z WP w wywołaniu zwrotnym, otrzymam tę samą wartość jednorazową i nie wiem, co robię źle ... Ostatecznie chcę rozszerz to, aby na stronie mogło znajdować się wiele elementów, każdy z możliwością dodawania / usuwania - więc potrzebuję rozwiązania, które pozwoli na wiele kolejnych żądań Ajax z jednej strony.

(I powinienem powiedzieć, że umieściłem całą tę funkcjonalność we wtyczce, więc „strona wyświetlająca” frontonu jest w rzeczywistości funkcją dołączoną do wtyczki ...)

functions.php: lokalizuj, ale nie tworzę tutaj nonce

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

Dzwonię do 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
         }
      }
  });
});

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

Frontendowa funkcja wyświetlania PHP, między innymi:

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

W tym momencie byłbym bardzo wdzięczny za wszelkie wskazówki lub wskazówki, które pozwolą WP zregenerować unikatowy nonce dla każdego nowego żądania Ajax ...


AKTUALIZACJA: Rozwiązałem mój problem. Powyższe fragmenty kodu są prawidłowe, jednak zmieniłem tworzenie $ newNonce w wywołaniu zwrotnym PHP, aby dodać ciąg mikrosekund, aby upewnić się, że jest unikalny dla kolejnych żądań Ajax.

Tim
źródło
Z bardzo krótkiego spojrzenia: tworzysz nonce po jej otrzymaniu (na wyświetlaczu)? Dlaczego nie tworzysz go podczas połączenia lokalizacyjnego?
kaiser
JQuery używa początkowej wartości jednorazowej z atrybutu „data-nonce” w łączu # myelement, a pomysł polega na tym, że strona może być przetwarzana przez Ajax lub samodzielnie. Wydawało mi się, że jednorazowe utworzenie kodu jednorazowego za pomocą wywołania lokalizacji wyklucza go z przetwarzania nieobsługującego JS, ale mogę się mylić. Tak czy inaczej Wordpress daje mi ten sam nonces z powrotem ...
Tim
Ponadto: czy wstawienie wartości jednorazowej w wywołaniu lokalizacji nie zapobiegnie umieszczeniu wielu elementów na stronie, na której każdy element może mieć unikalną wartość jednorazową dla żądania Ajax?
Tim
Utworzenie kodu źródłowego w lokalizacji spowoduje utworzenie i udostępnienie go dla tego jednego skryptu. Ale możesz również dodać nieograniczoną liczbę innych (nazwanych kluczy) lokalizacji wartości z osobnymi jednostkami nonces.
kaiser
Jeśli go rozwiązałeś, zachęcamy do opublikowania odpowiedzi i oznaczenia jej jako „zaakceptowana”. Pomoże to utrzymać porządek na stronie. Właśnie zadzierałem z twoim kodem i kilka rzeczy nie działa dla mnie, więc dwukrotnie proś prośbę o opublikowanie swojego rozwiązania.
s_ha_dum

Odpowiedzi:

6

Oto bardzo długa odpowiedź na moje własne pytanie, która wykracza poza samo pytanie o generowanie unikalnych wartości jednorazowych dla kolejnych żądań Ajax. Jest to funkcja „dodaj do ulubionych”, która została stworzona na potrzeby odpowiedzi (moja funkcja pozwala użytkownikom dodawać identyfikatory postów załączników do zdjęć do listy ulubionych, ale może to dotyczyć wielu innych funkcji, które polegają na Ajax). Zakodowałem to jako samodzielną wtyczkę, a brakuje kilku elementów - ale powinno to być wystarczająco szczegółowe, aby zapewnić sedno, jeśli chcesz powielić tę funkcję. Będzie działał na pojedynczym poście / stronie, ale będzie także działał na listach postów (np. Możesz dodawać / usuwać elementy do ulubionych w linii za pośrednictwem Ajax, a każdy post będzie miał swój unikalny kod dla każdego żądania Ajax). Pamiętaj, że tam ”

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 (wiele rzeczy do debugowania, które można usunąć)

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

Funkcje (wyświetlacz frontowy i działanie Ajax)

Aby wyświetlić link Dodaj / Usuń ulubione, po prostu zadzwoń na swoją stronę / post przez:

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

Funkcja wyświetlacza front-end:

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";
    }

}

Funkcja akcji 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
źródło
3

Naprawdę muszę zakwestionować uzasadnienie otrzymania nowej wartości jednorazowej dla każdego żądania ajax. Oryginalna wartość jednorazowa wygaśnie, ale można z niej skorzystać więcej niż jeden raz, aż to nastąpi. Otrzymanie go przez javascript za pośrednictwem ajax pokonuje cel, zwłaszcza jeśli jest dostępny w przypadku błędu. (Celem jest, aby jednostki jednorazowe stanowiły niewielkie zabezpieczenie dla skojarzenia akcji z użytkownikiem w określonym czasie).

Nie powinienem wspominać o innych odpowiedziach, ale jestem nowy i nie mogę komentować powyżej, więc w odniesieniu do opublikowanego „rozwiązania” za każdym razem otrzymujesz nowy nonce, ale nie używasz go w żądaniu. Byłoby z pewnością trudne, aby mikrosekundy były takie same za każdym razem, aby dopasować każdą nową nonce utworzoną w ten sposób. Kod PHP weryfikuje oryginalną wartość jednorazową, a javascript dostarcza oryginalną wartość jednorazową ... więc działa (ponieważ jeszcze nie wygasła).

Joy Reynolds
źródło
1
Problem polega na tym, że nonce wygasa po użyciu i po każdym zwróci -1 w funkcji ajax. Jest to problem, jeśli sprawdzasz poprawność części formularza w PHP i zwracasz błędy do wydrukowania. Użyto nonce formularza, ale błąd faktycznie pojawił się podczas sprawdzania poprawności pól przez php, a kiedy formularz zostanie ponownie przesłany, tym razem nie można go zweryfikować i check_ajax_refererzwraca -1, co nie jest tym, czego chcemy!
Solomon Closson