web-dev-qa-db-fra.com

URL unique à chaque fois

J'ai créé un petit outil de recherche qui, à moins que vous ne connaissiez le code EXACT, vous ne serez pas amené au poste. Par exemple, si je saisis "OI812", cela me mènera à un type de publication personnalisé qui est ../CPT/OI812.

Le CPT ne fait pas partie de la recherche habituelle et j'ai supprimé quoi que ce soit avec ce slug d'une redirection canonique. À moins qu'ils n'utilisent mon petit outil de recherche et ne saisissent le code exactement, cela ne les mènera pas à la page.

Jusqu'ici tout va bien. Cependant, j'aimerais avoir une URL générée de manière unique chaque fois qu'ils accèdent à la page. PAR EXEMPLE. ../CPT/HASHorSOMETHINGcrazyANDrandom. Cela rendrait le partage de l'URL inutile à moins qu'ils ne visitent la page et entrent le code dans l'outil de recherche.

Je suis curieux de savoir comment on s'y prendrait? J'ai cherché partout, mais les termes de recherche pour quelque chose comme ça semblent être un peu omniprésents. Toute aide appréciée.

4
Eddie

Utilisez add_rewrite_rule ou add_rewrite_endpoint pour capturer la variable HASHorSOMETHINGcrazyANDrandom.

Hashids pourrait également vous aider à générer un hachage que vous pourrez relire plus tard.

$hashids = new Hashids\Hashids('this is my salt');

$post_id = 1;
$request_id = 2;
$random = 3;

$crazy_id = $hashids->encode($post_id, $request_id, $random);

$numbers = $hashids->decode($crazy_id);

UPDATE # 1

Cela crée deux extrémités:

http://example.com/CPT/ {CODE}/{HASH}

http://example.com/CPT/ {CODE} /

Dans les deux cas, un nouveau hachage sera généré avec un nouveau lien à chaque fois. Parce que j'utilise wp_hash_password, je boucle jusqu'à ce qu'un mot de passe ne contienne pas / pour ne pas casser l'URL. Je suis sûr qu'il y a de meilleures façons mais ... ça marche pour ce test. Le mot de passe en clair est basé sur le SERVER_NAME + {CODE} qui a été tiré du premier paramètre.

Chaque URL de hachage est unique mais valide toujours si elle est utilisée avec le code correct.


if( ! class_exists('HashPoint')):

    class HashPoint {

        const ENDPOINT_NAME       = 'CPT'; // endpoint to capture
        const ENDPOINT_QUERY_NAME = '__cpt'; // turns to param

        // WordPress hooks
        public function init() {
            add_filter('query_vars', array($this, 'add_query_vars'), 0);
            add_action('parse_request', array($this, 'sniff_requests'), 0);
            add_action('init', array($this, 'add_endpoint'), 0);
        }

        // Add public query vars
        public function add_query_vars($vars) {
            $vars[] = static::ENDPOINT_QUERY_NAME;
            $vars[] = 'code';
            $vars[] = 'hash';

            return $vars;
        }

        // Add API Endpoint
        public function add_endpoint() {
            add_rewrite_rule('^' . static::ENDPOINT_NAME . '/([^/]*)/([^/]*)/?', 'index.php?' . static::ENDPOINT_QUERY_NAME . '=1&code=$matches[1]&hash=$matches[2]', 'top');
            add_rewrite_rule('^' . static::ENDPOINT_NAME . '/([^/]*)/?', 'index.php?' . static::ENDPOINT_QUERY_NAME . '=1&code=$matches[1]', 'top');

            flush_rewrite_rules(false); //// <---------- REMOVE THIS WHEN DONE
        }

        // Sniff Requests
        public function sniff_requests($wp_query) {
            global $wp;

            if(isset($wp->query_vars[ static::ENDPOINT_QUERY_NAME ])) {
                $this->handle_request(); // handle it
            }
        }

        // Handle Requests
        protected function handle_request() {
            // Control the template used

            add_filter('template_include', function($original_template) {

                // global $wp_query;
                // var_dump ( $wp_query->query_vars );

                // var_dump($original_template);

                return get_template_directory() . '/crazy-hash.php';
            });
        }
    }

    $hashEP = new HashPoint();
    $hashEP->init();

endif; // HashPoint

CRAZY HASH TEMPLATE

<?php
/**
 * Template Name: Crazy Hash
 */
get_header();

global $wp_query;
$code = $wp_query->query_vars[ 'code' ];
$hash = empty($wp_query->query_vars[ 'hash' ]) ? 'NONE' : $wp_query->query_vars[ 'hash' ];
$hash = urldecode($hash);

echo 'Code : ' . $code;
echo '<br />';
echo 'Hash : ' . $hash;
echo '<br />';
echo '<br />';

require_once(ABSPATH . 'wp-includes/class-phpass.php');
$wp_hasher = new PasswordHash(8, true);

$plain = $_SERVER[ 'SERVER_NAME' ] . '-' . $code;
$hash_mash = wp_hash_password($plain);

// make sure we don't have any `/` to break the url
while(strpos($hash_mash, '/')) {
    $hash_mash = wp_hash_password($plain);
}

echo 'Valid?<br />';

if($wp_hasher->CheckPassword($plain, $hash)) {
    echo "YES, Matched<br /><br />";
}
else {
    echo "No, BAD HASH!!!<br /><br />";
}

$url = get_home_url(NULL, 'CPT/' . $code . '/' . urlencode($hash_mash));

echo "Try this Hash : <a href=\"$url\">$hash_mash</a>";
echo '<br /><br />';

// ... more ...

get_footer();

MISE À JOUR # 2 | DURÉE DE VIE NONCE

Pour @birgire - Pour créer un nonce à vie, n’avez-vous pas besoin de supprimer la wp_nonce_tick() de wp_create_nonce ?

function wp_create_lifetime_nonce($action = - 1) {
    $user = wp_get_current_user();
    $uid = (int) $user->ID;
    if( ! $uid) { 
        $uid = apply_filters('lifetime_nonce_user_logged_out', $uid, $action);
    }

    $token = wp_get_session_token();
    $i = 0;//wp_nonce_tick(); -- time is not a factor anymore

    return substr(wp_hash($i . '|' . $action . '|' . $uid . '|' . $token, 'nonce'), - 12, 10);
}

function wp_verify_lifetime_nonce($nonce, $action = - 1) {
    $nonce = (string) $nonce;
    $user = wp_get_current_user();
    $uid = (int) $user->ID;
    if( ! $uid) {
        $uid = apply_filters('lifetime_nonce_user_logged_out', $uid, $action);
    }

    if(empty($nonce)) {
        return false;
    }

    $token = wp_get_session_token();
    $i = 0; //wp_nonce_tick();  -- time is not a factor anymore

    // Nonce generated anytime ago
    $expected = substr(wp_hash($i . '|' . $action . '|' . $uid . '|' . $token, 'nonce'), - 12, 10);
    if(hash_equals($expected, $nonce)) {
        return 1;
    }

    do_action('wp_verify_lifetime_nonce_failed', $nonce, $action, $user, $token);

    // Invalid nonce
    return false;
}

$code = 'OI812';

$lifetime_nonce = wp_create_lifetime_nonce($code);
$nonce = wp_create_nonce($code);

echo "<pre>";
print_r(
    array(
        $code,
        $lifetime_nonce,
        $nonce,
        ! wp_verify_nonce($nonce, $code) ? 'FAILED' : 'WORKED',
        ! wp_verify_lifetime_nonce($lifetime_nonce, $code) ? 'FAILED' : 'WORKED',
    ));
echo "</pre>";
4
jgraup

Vous pouvez utiliser une implémentation partielle de JWTs pour passer un jeton unique comprenant les informations d’identification et l’ID de publication demandé (ou slug) en tant que point de terminaison, puis vérifier les informations d’identification lors de la validation.

Les URL contenant un jeton unique peuvent être réécrites pour transmettre le jeton en tant que variable spécifique. Un 'parse_query' action hook peut alors vérifier la présence de la variable de jeton et remplacer la requête par une autre qui renverra la publication correcte si le jeton est valide - ou une erreur s'il ne l'est pas.

De cette manière, seul le visiteur ayant émis le jeton pourrait l'utiliser pour accéder à la publication (à moins que quelqu'un d'autre n'acquiert le jeton et usurpe l'adresse IP du visiteur d'origine - cette information pourrait être sécurisée davantage avec un cookie ou un identifiant de session). Forger des jetons est impossible sans votre secret.

$secret           = '{insert randomly generated string here}';
$custom_post_type = 'my_cpt';
$unique_url_base  = 'cpt';

add_action( 'init', 'wpse_212309_rewrite_unique_token_url' );

/**
 * Adds a rewrite rule to forward URLs in the format /cpt/{unique token} to
 * index.php?post_type=my_cpt&unique_token={unique token}. Supersedes WordPress
 * rewrites for the endpoint.
 **/
function wpse_212309_rewrite_unique_token_url(){
    add_rewrite_rule(
        trailingslashit( $unique_url_base ) . '([^\.]*.[^\.]*)$',
        'index.php?post_type=' . $custom_post_type . '&unique_token=$matches[1]',
        'top'
    );
}

add_action( 'parse_query', 'wpse_212309_decode_unique_token_query' );

/**
 * Replaces queries for the 'my_cpt' post-type containing a unique token with
 * the appropriate 'my_cpt' post if the token is valid (i.e., passed to the
 * server from the client IP to which it was assigned).
 **/
function wpse_212309_decode_unique_token_query( $wp ) {
    if( is_admin() )
        return;

    if( isset( $wp->query_vars[ 'p' ] ) || $custom_post_type != $wp->query_vars[ 'post_type' ] || empty( $_GET[ 'unique_token' ] ) )
        return;

    $post_id = wpse_212309_get_post_id_from_unique_slug( $_GET[ 'unique_token' ] );

    if( ! $post_id ) {
        $wp->set_404();
        status_header( 404 );
        return;
    }

    $wp->parse_request( 'p=' . $post_id );
}

/**
 * Encodes data into a URL-friendly JWT-esque token including IP information
 * for the requesting party, as well as an optional expiration timestamp.
 **/
function wpse_212309_encode_token( $payload, $expiration = null ) {    
    $payload[ 'aud' ] = hash( 'md5', $_SERVER[ 'REMOTE_ADDR' ] . $_SERVER[ 'HTTP_X_FORWARDED_FOR' ] );
    $payload[ 'iss' ] = time();

    if( isset( $expiration ) )
        $payload[ 'exp' ] = $expiration;

    $payload = base64_encode( json_encode( $payload ) );
    $hash    = hash( 'md5', $payload . $secret );

    return urlencode( $payload . '.' . $hash );
}

/**
 * Decodes a token generated by 'wpse_212309_encode_token()', returning the
 * payload if the token is both unaltered and sent by the original client IP
 * or false otherwise.
 **/
function wpse_212309_decode_token( $token ) {
    if( empty( $token ) || -1 === strpos( $token, '.' ) )
        return false;

    $token   = urldecode( $token );
    $token   = explode( '.', $token );
    $hash    = $token[1];
    $payload = $token[0];

    // If the payload or the hash is missing, the token's invalid.
    if( empty( $payload ) || empty( $hash ) )
        return false;

    $hash_check = hash( 'md5', $payload . $secret );

    // Has the payload and/or hash been modified since the token was issued?
    if( $hash_check !== $hash )
        return false;

    $payload = base64_decode( $payload );

    if( ! $payload )
        return false;

    $payload = json_decode( $payload, true );

    if( ! $payload )
        return false;

    $audience_check = hash( 'md5', $_SERVER[ 'REMOTE_ADDR' ] . $_SERVER[ 'HTTP_X_FORWARDED_FOR' ] );

    // Was this token passed to the server by the IP that it was issued to?
    if( $audience_check != $payload[ 'aud' ] )
        return false;

    // Does the payload have an expiration date - if so, has it expired?
    if( ! empty( $payload[ 'exp' ] ) && $payload[ 'exp' ] > time() )
        return false;

    // Token validated - return the payload as legitimate data.
    return $payload;
}

/**
 * Produces a token associating a post ID with a particular client, suitable
 * for inclusion in a URL. Optionally takes a "time to live" argument, in
 * in seconds, before the token should expire.
 **/
function wpse_212309_generate_unique_slug( $post_id, $ttl = null ) {
    $expiration = null;

    if( $ttl )
        $expiration = time() + $ttl * 1000;

    return wpse_212309_encode_token( array( 'pid' => $post_id ), $expiration );
}

/**
 * Returns a post ID from a token if the token was in fact issued to the 
 * requesting client IP, or false otherwise.
 **/
function wpse_212309_get_post_id_from_unique_slug( $token ) {
    $payload = wpse_212309_decode_token( $token );

    if( ! $payload )
        return false;

    return $payload[ 'pid' ];
}

La manière dont vous envoyez réellement les visiteurs aux URL uniques contenant un jeton dépend de votre application (c'est-à-dire comment vous configurez votre "outil de recherche"), mais en utilisant la mise en œuvre ci-dessus, vous pouvez récupérer un slug unique du visiteur pour un identifiant de publication comme celui-ci:

// A slug that only works for the visitor it was issued to:
$unique_slug = wpse_212309_generate_unique_slug( $post_id );

// OR, for one that additionally expires in an hour:
$unique_slug = wpse_212309_generate_unique_slug( $post_id, 3600 )
4
bosco

Une solution simple alternative.

Créatif utilise de Nonces

WordPress a un système interne pour générer un hachage unique qui peut ensuite être vérifié: nonces . Normalement, les nonces sont utilisés pour empêcher les attaques CSRF , mais en considérant que out-of-the-box a WP nonce:

  • est un hash
  • peut être vérifié
  • est valable uniquement pour une durée limitée
  • est couplé à un utilisateur spécifique

Cela correspond très bien à tous vos besoins.

Pas de règles de réécriture

Je serai honnête: je n'aime pas l'API de réécriture de WordPress. Les règles sont stockées dans une base de données, elles doivent être "vidées" avant d'être utilisées et l'API elle-même est loin d'être idéale.

Dans des cas comme celui-ci, je me contenterais d'un niveau peu élevé et utiliserais le crochet 'do_parse_request' filter pour vérifier l'URL secrète et définir les vars appropriés, si nécessaire.

Le format de l'URL

Pour le code ci-dessous, je supposerai un lien comme celui-ci:

home_url( '/s/'. $post_id . '|' . wp_create_nonce('my-cpt'.$post_id) )

Cela utilise des informations, ce qui le rend unique par utilisateur et le même utilisateur peut l'utiliser pendant une durée limitée, par défaut un jour, mais il peut être modifié à l'aide de 'nonce_life' filter.

Cela ressemblera à ceci https://example.com/s/MTUy|ab5f9370de.

Décrire l'URL et envoyer pour poster

Avec le filtre 'do_parse_request', nous pouvons intercepter la demande d’URL telle que celle décrite ci-dessus avant que celle-ci soit analysée par WordPres, et empêcher WordPress de continuer à traiter l’URL.

is_admin() or add_filter('do_parse_request', function($do, $wp) {

     // quick way to get current url
     $url = trim(esc_url_raw(add_query_arg(array())), '/');
     // check if WP has some subfolder in home url
     $path = trim(parse_url(home_url(),  PHP_URL_PATH), '/');
     $path and $path .= '/';

     // this is not one of ours secret urls, just do nothing
     if (strpos($url, $path.'s/') !== 0) {
         return $do;

     // extract post id and nonce from url
     $sectretUrl = explode('|', preg_replace('~^'.$path.'s/~', '', $url), 2);
     $id = (int) base64_decode(urldecode($sectretUrl[0])) ;
     $nonce = empty($sectretUrl[1]) ? false : $sectretUrl[1];

     // verify nonce, if not valid let WordPress continue the flow
     // that very likely ends on a 404
     if (!$id || !$nonce || ! wp_verify_nonce($nonce, 'my-cpt'.$id)) {
         return $do;

     // everything ok, let's set query var and tell WP don't parse request
     $wp->query_vars = array('p' => $id);
     $wp->is_secret_ok = $id;
     return false;

}, PHP_INT_MAX, 2);

Ça y est, ça marche. Tous avec un extrait de code unique de seulement 15 lignes de code (à l'exclusion des commentaires). Pas même besoin de vider les règles de réécriture.

Mais vous avez toujours un problème.

Désactiver l'accès standard

Si vous regardez l'exemple d'URL secret que j'ai écrit ci-dessus, https://example.com/s/MTUy|ab5f9370de, il est assez facile de comprendre que la première partie est codée en base64.

Si quelqu'un essaie de le décoder, trouvera l'identifiant de la publication. Et en utilisant une URL comme "http://example.com?p={$decoded_id}", cette personne pourra consulter le message.

De plus, je suppose que le titre du post est visible. Si vous utilisez le slug généré automatiquement par WordPress, utilisez une URL du type "http://example.com?p={$guessed_slug}" pour que votre message soit à nouveau visible.

Cela signifie que vous devez empêcher l'accès à l'URL standard pour le poste CPT. Peut-être n'autoriser que les utilisateurs privilégiés, tels que les administrateurs et les éditeurs.

add_action('template_redirect', function() {
  // when not our CPT or user is privileged, do nothing
  if (! is_singular('my-cpt') || current_user_can('edit_others_posts')) {
    return;
  }
  global $wp;
  // if this is from "standard" url, exit with error
  if (! isset($wp->is_secret_ok) || $wp->is_secret_ok !== (int) get_queried_object_id()) {
     wp_die('Not allowed.');
  }
  // prevent canonical redirect that will ends in a 404 request
  add_filter('redirect_canonical', '__return_false');
}, -1);

Pour empêcher l'accès aux utilisateurs provenant d'URL "standard", j'ai utilisé une variable $wp->is_secret_ok que j'ai définie dans le fragment de code précédent, lorsque l'URL hachée est vérifiée.

Création du lien CPT secret

L'URL dont nous avons besoin est un peu complexe, nous voudrons peut-être créer une fonction pour la construire, en prenant comme paramètre l'identifiant de publication.

Il est également possible d'utiliser cette fonction pour filtrer le lien permanent et laisser WordPress générer automatiquement l'URL "secrète" lorsque vous appelez simplement the_permalink().

Quelque chose comme ça:

function my_cpt_secret_url($postId) {
   $id = urlencode(base64_encode((string)$postId));

   return home_url('/s/'.$id.'|'.wp_create_nonce('my-cpt'.$postId));
}

is_admin() or add_filter('post_type_link', function($link, $post) {
  if ($post->post_type === 'my-cpt') {
      return my_cpt_secret_url($post->ID)
  return $link;
}, 30, 2);
3
gmazzap