Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.
Lessons
Every action that changes data in a plugin needs two checks: a capability check confirms the current user is allowed to do it, and a nonce confirms the request was made deliberately from your own form rather than forged by another site.
| Check | Answers the question | Stops |
|---|---|---|
current_user_can() | Is this user allowed to do this? | A Subscriber deleting books by guessing the URL |
| Nonce | Did this user mean to do this, from our page? | CSRF: an admin visiting an evil page that silently submits your form in their logged-in browser |
Watch out: A nonce is not an authentication check. A logged-in Subscriber can get a valid nonce from a page they can see. You always need both: nonce and capability.
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="gdc_books_reset_stats">
<?php wp_nonce_field( 'gdc_books_reset_stats', 'gdc_books_nonce' ); ?>
<?php submit_button( __( 'Reset statistics', 'gdc-books' ), 'delete' ); ?>
</form>wp_nonce_field( $action, $name ) prints a hidden field containing a token tied to the action name, the current user and a time window of 12 to 24 hours.
<?php
function gdc_books_handle_reset_stats() {
// 1. Capability.
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to do this.', 'gdc-books' ), 403 );
}
// 2. Nonce. Dies with "The link you followed has expired" on failure.
check_admin_referer( 'gdc_books_reset_stats', 'gdc_books_nonce' );
// 3. The actual work.
delete_transient( 'gdc_books_stats' );
// 4. Redirect back (POST/redirect/GET), so a refresh does not resubmit.
wp_safe_redirect( add_query_arg( 'gdc-reset', '1', wp_get_referer() ) );
exit;
}
// admin_post_{action} — the value of the hidden "action" field.
add_action( 'admin_post_gdc_books_reset_stats', 'gdc_books_handle_reset_stats' );admin-post.php is WordPress's front door for custom form submissions. It fires admin_post_{action} for logged-in users and admin_post_nopriv_{action} for visitors.
An action triggered by a link — "Delete this book" — needs a nonce in the URL:
<?php
$url = wp_nonce_url(
admin_url( 'admin-post.php?action=gdc_books_archive&book=' . $book_id ),
'gdc_books_archive_' . $book_id // Include the ID: a nonce for book 5 cannot archive book 6.
);
echo '<a href="' . esc_url( $url ) . '">' . esc_html__( 'Archive', 'gdc-books' ) . '</a>';
// In the handler:
$book_id = isset( $_GET['book'] ) ? absint( $_GET['book'] ) : 0;
check_admin_referer( 'gdc_books_archive_' . $book_id );
if ( ! current_user_can( 'edit_post', $book_id ) ) {
wp_die( '', 403 );
}Capabilities such as edit_post take the post ID and answer for that specific post. An Author can edit their own books but not someone else's. Always pass the ID when the action concerns one object.
check_admin_referer(), check_ajax_referer() or wp_verify_nonce().admin-post.php.In accordance with Section 6 & Section 7 of the DPDP Act 2023, third-party analytics and marketing trackers are strictly gated behind your affirmative consent. You may withdraw consent at any time under Section 6(4).
Essential for session authentication (ci_session), Cross-Site Request Forgery security (csrf_cookie_name), and Cloudflare DDoS/bot prevention. Cannot be disabled.
Gated under DPDP Section 7. Allows Google Analytics 4 (Google Consent Mode v2) to measure aggregate, anonymized tool usage.
Enables browser local storage to preserve your theme selection (dark/light mode) and code editor layout between visits.
Used to display developer sponsorships and native tool partner notices.