Como obter um nonce exclusivo para cada solicitação do Ajax?

11

Eu já vi algumas discussões sobre como fazer com que o Wordpress regenere um nonce exclusivo para solicitações subsequentes do Ajax, mas, para a minha vida, não consigo fazer com que o Wordpress faça isso - toda vez que solicito o que acho que deveria ser um novo nonce, recebo o mesmo nonce de volta do Wordpress. Entendo o conceito de nonce_life do WP e até defini-lo para outra coisa, mas isso não me ajudou.

Eu não giro o nonce no objeto JS no cabeçalho via localização - faço na minha página de exibição. Posso fazer com que minha página processe a solicitação do Ajax, mas quando solicito um novo nonce do WP no retorno de chamada, recebo o mesmo nonce de volta e não sei o que estou fazendo de errado ... Finalmente, quero estenda isso para que possa haver vários itens na página, cada um com a capacidade de adicionar / remover - por isso, preciso de uma solução que permita várias solicitações subsequentes do Ajax em uma página.

(E devo dizer que coloquei toda essa funcionalidade em um plug-in, portanto, a "página de exibição" do front-end é na verdade uma função incluída no plug-in ...)

functions.php: localize, mas não crio um nonce aqui

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

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

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

Função de exibição PHP front-end, entre as quais:

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

Nesse momento, ficaria muito grato por qualquer pista ou indicação de fazer o WP regenerar um noncece único para cada nova solicitação do Ajax ...


ATUALIZAÇÃO: Eu resolvi meu problema. Os trechos de código acima são válidos, no entanto, alterei a criação de $ newNonce no retorno de chamada PHP para anexar uma sequência de microssegundos para garantir que ela seja exclusiva nas solicitações subsequentes do Ajax.

Tim
fonte
De uma olhada muito breve: você está criando o nonce depois de recebê-lo (em exibição)? Por que você não o está criando durante a chamada de localização?
Kaiser
O jQuery está usando o nonce inicial do atributo "data-nonce" no link a # myelement, e a idéia é que a página possa ser processada pelo Ajax ou por si só. Pareceu-me que criar o nonce uma vez por meio da chamada de localização o excluiria do processamento não-JS, mas eu poderia estar errado sobre isso. De qualquer maneira Wordpress me dá o mesmo nonce de volta ...
Tim
Além disso: a colocação do nonce na chamada de localização não impediria que houvesse vários itens em uma página em que cada item pudesse ter um nonce exclusivo para uma solicitação do Ajax?
Tim
Criar o nonce dentro da localização criaria e disponibilizaria para esse script. Mas você também pode adicionar uma quantidade ilimitada de outros valores (localizados com a chave) com nonces separados.
kaiser
Se você a resolveu, incentive-o a postar sua resposta e marcá-la como "aceita". Isso ajudará a manter o site organizado. Eu estava apenas mexendo no seu código e algumas coisas não funcionam para mim, então o dobro dessa solicitação para você postar sua solução.
#

Respostas:

6

Aqui está uma resposta muito longa da minha própria pergunta que vai além de apenas abordar a questão de gerar nonces únicos para solicitações subsequentes do Ajax. Esse é um recurso "adicionar aos favoritos", que foi tornado genérico para os fins da resposta (meu recurso permite que os usuários adicionem os IDs de postagem de anexos de fotos a uma lista de favoritos, mas isso pode se aplicar a vários outros recursos que dependem de Ajax). Eu codifiquei isso como um plug-in independente e faltam alguns itens - mas isso deve ser suficiente para fornecer a essência, se você quiser replicar o recurso. Ele funcionará em uma postagem / página individual, mas também funcionará em listas de postagens (por exemplo, você pode adicionar / remover itens dos favoritos em linha via Ajax e cada postagem terá seu próprio nonce exclusivo para cada solicitação do Ajax). Lembre-se de que existe

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 (Muitas coisas de depuração que podem ser removidas)

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

Funções (exibição front-end e ação do Ajax)

Para gerar o link Adicionar / Remover Favoritos, basta chamá-lo em sua página / postagem via:

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

Função de exibição 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";
    }

}

Função de ação 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
fonte
3

Eu realmente tenho que questionar o raciocínio por trás de obter um novo nonce para cada solicitação de ajax. O nonce original expirará, mas poderá ser usado mais de uma vez até que exista. O recebimento do javascript através do ajax anula o objetivo, especialmente fornecendo-o em um caso de erro. (O objetivo de nonces ser um pouco de segurança para associar uma ação a um usuário dentro de um período de tempo.)

Não devo mencionar outras respostas, mas sou novo e não posso comentar acima. Portanto, no que diz respeito à "solução" postada, você recebe um novo aviso sempre que não o está usando na solicitação. Certamente seria difícil obter os microssegundos sempre o mesmo para corresponder a cada novo nonce criado dessa maneira. O código PHP está sendo verificado com relação ao nonce original e o javascript está fornecendo o nonce original ... portanto, ele funciona (porque ainda não expirou).

Joy Reynolds
fonte
11
O problema é que o nonce expira após ser usado e retorna -1 na função ajax após cada vez. Este é um problema se você estiver validando partes de um formulário no PHP e retornar erros para imprimir. O formulário nonce foi usado, mas ocorreu um erro na validação php dos campos e, quando o formulário é enviado novamente, desta vez, não pode ser verificado e check_ajax_refererretorna -1, o que não é o que queremos!
Solomon Closson