I have created a small search tool that unless you know the EXACT code, you will not be taken to the post. For example, if I enter “OI812” it will take me to a custom post type that is ../CPT/OI812.
The CPT is not part of the regular search and I’ve removed anything with that slug from a canonical redirect. Unless they use my small search tool and enter the code exactly, it will not take them to the page.
So far, so good. However, I would like to have a uniquely generated URL every time they access the page. E.G. ../CPT/HASHorSOMETHINGcrazyANDrandom. This would make sharing the URL useless unless they visit the page and enter the code into the search tool.
I’m curious how one would go about this? I’ve searched around but the search terms for something like this seems to be a little ubiquitous. Any help appreciated.
You could use a partial implementation of JWTs to pass a unique token including identifying information and the requested post ID (or slug) as the endpoint, and check the identifying info upon validation.
URLs containing a unique token can be rewritten to pass the token as a specific variable. A 'parse_query'
action hook can then check for the presence of the token variable, and replace the query with one that will return the proper post if the token is valid – or an error if it isn’t.
In this manner, only the visitor issued the token could use it to access the post (unless someone else both acquires the token and spoofs the original visitor’s IP – this could be further secured with a cookie or session ID of some sort). Forging tokens is impossible without your 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' ];
}
How you actually send visitors to the unique URLs containing a token depends on your application (i.e. how you set up your “search tool”), but using the implementation above you can retrieve a visitor-unique slug for a post ID like this:
// 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 )