

Posts -> WP-DraftsForFriends: the share form, and every share with its countdown
This plugin generates a unique link you can send to a friend so they can read a post before you publish it. The link works for someone who is not logged in and has no account, it only ever opens the one post it was issued for, and it stops working by itself when the time you set runs out.
Everything happens under Posts -> WP-DraftsForFriends: pick an unpublished post, say how long the link should last, and copy the link it gives you. The list below shows every link you have out, how long each has left, and lets you extend or revoke them. The settings are the second tab of that same page.
Sharing takes the publish_posts capability rather than manage_options: a plugin for sharing your own drafts has no business asking for the capability that lets somebody reconfigure the site.
Modified from Drafts for Friends, originally by Neville Longbottom. The plugin icon is by Freepik from Flaticon.
I spent most of my free time creating, updating, maintaining and supporting these plugins, if you really love my plugins and could spare me a couple of bucks, I will really appreciate it. If not feel free to use it without any obligations.
Go to Posts -> WP-DraftsForFriends. The page has two tabs, Shared Drafts and Settings.
Under Share a Draft, choose an unpublished post, set how long the link should last, and press Share Draft. The link appears in the list below; press Copy link to put it on your clipboard and send it to whoever needs it.
The list shows every link you have out. Expires After counts down and then reads Expired. Twenty rows are shown at a time, every column except the link is sortable, and Screen Options changes how many rows you see.
To extend links, set Extend by to the duration you want to add, tick the rows, choose Extend selected and press Apply. To revoke them, tick the rows and choose Revoke selected. Both are bulk actions rather than links on each row, and deliberately so: a link is a GET, and a browser or link checker that quietly prefetches one would have revoked every share on the page before you knew about it.
The Settings tab sets the duration a new share starts on. It is only a starting value — both the share form and Extend by can be changed for one share without changing the setting. That tab takes manage_options, so an author sees the Shared Drafts tab and not the Settings one.
Anyone with the edit_others_posts capability — administrators and editors — sees every shared draft on the site and can share any unpublished post. Authors and contributors see only their own, and can only share posts they are allowed to edit.
wp draftsforfriends list --user=admin
wp draftsforfriends create 42 --user=admin
wp draftsforfriends create 42 --expires=14 --measure=d --user=admin
wp draftsforfriends extend 3 4 5 --expires=1 --measure=d --user=admin
wp draftsforfriends revoke 3 --yes --user=admin
create prints the share link on a line of its own before its success message, and `list` prints one per row. **A share link is the credential** — whoever holds it reads the unpublished post until the link expires, with no account and no login — so treat the output of both as you would the drafts themselves. Shell history, a CI log and a captured `stdout` are all places those links now live.
Pass --user. WP-CLI runs as nobody unless told otherwise, and every one of these is scoped exactly as the screen is: a share belongs to whoever created it, anyone with edit_others_posts sees them all, and creating, extending or revoking one checks that you may edit the post it points at. Run as nobody, list reports nothing and the rest are refused.
--expires and `--measure` default to the duration on the **Settings** tab, the same value the share form and **Extend by** start on. `extend` and `revoke` take as many ids as you like, exactly as the bulk actions do. `revoke` asks before it acts, because the link stops working immediately and cannot be restored; `--yes` answers for a script.
There is no subcommand for the settings — that is one option row, which wp option get wp_draftsforfriends_options already reads.
wp_draftsforfriends_capability decides who may reach each tab. The context is `shares` for the Shared Drafts tab or `settings` for the Settings tab:
add_filter( 'wp_draftsforfriends_capability', function ( $capability, $context ) {
return 'settings' === $context ? 'manage_options' : 'edit_posts';
}, 10, 2 );
wp_draftsforfriends_share_url filters the link a friend is given, and
wp_draftsforfriends_requested_hash reads the hash back off the request. **They
are one contract.** Change the shape of the link without teaching the plugin to
recognise it and every share link 404s, with nothing on the admin screens
looking wrong:
add_filter( 'wp_draftsforfriends_share_url', function ( $url, $share ) {
return home_url( '/secret/' . $share->hash . '/' );
}, 10, 2 );
add_filter( 'wp_draftsforfriends_requested_hash', function ( $hash ) {
if ( '' !== $hash ) {
return $hash;
}
$path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
return preg_match( '#/secret/([A-Za-z0-9]+)/#', $path, $m ) ? $m[1] : '';
} );
The default link is ?p=<id> rather than the post’s permalink, and that is not
an oversight: the preview works by catching the row WordPress fetches for a bare
post id and puts back before rendering. A permalink looks the post up by slug
among the public statuses, so an unpublished post is never found at all.
Three fire as a share moves through its life, each after the write has
succeeded:
wp_draftsforfriends_share_created — the stored share, and the post it shares.wp_draftsforfriends_share_extended — the share as it now stands, and thewp_draftsforfriends_share_revoked — the share as it was; the link hasadd_action( 'wp_draftsforfriends_share_created', function ( $share, $post ) {
error_log( sprintf( 'Shared "%s" until %s', $post->post_title, $share->date_expired ) );
}, 10, 2 );