<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Foxe Labs — Blog</title><description>Notes on open-source software, expert advisor design, risk, and what Foxe Labs is building.</description><link>https://foxelabs.com/blog/</link><language>en-us</language><item><title>How to automatically log out inactive users in WordPress</title><link>https://foxelabs.com/blog/auto-logout-inactive-users-wordpress/</link><guid isPermaLink="true">https://foxelabs.com/blog/auto-logout-inactive-users-wordpress/</guid><description>WordPress keeps a login alive for 2 days — 14 with &quot;Remember Me&quot; — no matter how long the browser sits untouched. Here are four ways to add an idle timeout that signs inactive users out, from a one-line snippet to a full warning-countdown setup, with honest pros and cons for each.</description><pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Walk away from a logged-in WordPress dashboard and nothing happens. Not after
lunch, not overnight, not the next day. Out of the box, a WordPress login lasts
&lt;strong&gt;48 hours&lt;/strong&gt; — and &lt;strong&gt;14 days&lt;/strong&gt; if the user ticked “Remember Me” — regardless of
whether anyone touched the keyboard in between. There is no idle timeout, no
“you’ve been signed out due to inactivity” screen, nothing.&lt;/p&gt;
&lt;p&gt;For a personal blog, fine. For plenty of real sites, not fine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A clinic, school, or office where shared and public computers are normal, and
a walked-away-from dashboard is an open door.&lt;/li&gt;
&lt;li&gt;A WooCommerce store where shop managers handle customer addresses and order
history, and PCI-flavoured checklists ask for a 15-minute idle timeout on
anything touching cardholder data.&lt;/li&gt;
&lt;li&gt;A membership or LMS site where one login left open on a library machine is
everyone’s problem.&lt;/li&gt;
&lt;li&gt;Any site where an audit, an insurer, or a compliance framework asks the
question “are inactive sessions terminated?” and the honest answer is “no,
they last two weeks.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This guide covers four ways to automatically log out inactive users on a
WordPress website — from a one-line filter to a complete idle-timeout setup with
a warning countdown — with honest pros and cons for each.&lt;/p&gt;
&lt;h2 id=&quot;first-how-wordpress-decides-youre-still-logged-in&quot;&gt;First, how WordPress decides you’re still logged in&lt;/h2&gt;
&lt;p&gt;When a user signs in, WordPress generates a &lt;em&gt;session token&lt;/em&gt;, stores it in the
&lt;code&gt;wp_usermeta&lt;/code&gt; table under the &lt;code&gt;session_tokens&lt;/code&gt; key, and puts a matching
authentication cookie in the browser. Every request compares cookie to token.
As long as both exist and the token hasn’t expired, the user is signed in.&lt;/p&gt;
&lt;p&gt;The details that matter for auto logout:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Expiry is fixed at login time.&lt;/strong&gt; The token gets an expiration timestamp when
it’s created — 2 days, or 14 with “Remember Me” — and nothing about the user’s
activity moves it. WordPress never asks “when were you last active?”, only
“has your token expired yet?”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Closing the browser changes nothing.&lt;/strong&gt; The token lives server-side.
Reopen the browser within the cookie lifetime and the session resumes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;There is no inactivity concept anywhere in core.&lt;/strong&gt; No last-activity
timestamp, no idle check, no filter that fires when a user “goes quiet.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So “auto logout after inactivity” is really two separate problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Shorten how long a login lasts overall&lt;/strong&gt; (the session lifetime).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End a session that goes unused&lt;/strong&gt; (the idle timeout) — which requires
tracking activity yourself, because core doesn’t.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The methods below climb that ladder.&lt;/p&gt;
&lt;h2 id=&quot;method-1--shorten-the-session-with-auth_cookie_expiration&quot;&gt;Method 1 — Shorten the session with auth_cookie_expiration&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;auth_cookie_expiration&lt;/code&gt; filter controls how long the login cookie (and the
matching server token) lasts. Drop this into a small companion plugin:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;add_filter&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;auth_cookie_expiration&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;expiration&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;user_id&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;remember&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;	// 4 hours for everyone, &amp;quot;Remember Me&amp;quot; or not.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; HOUR_IN_SECONDS&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can respect “Remember Me” while still tightening both values:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;add_filter&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;auth_cookie_expiration&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;expiration&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;user_id&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;remember&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;remember&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; DAY_IN_SECONDS &lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; HOUR_IN_SECONDS&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;New durations apply from each user’s &lt;em&gt;next&lt;/em&gt; login — existing tokens keep the
expiry they were minted with.&lt;/p&gt;
&lt;div class=&quot;pros&quot;&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Core API, three lines, no dependencies.&lt;/li&gt;
&lt;li&gt;Genuinely server-side. No JavaScript involved, nothing the user can bypass.&lt;/li&gt;
&lt;li&gt;Solves the “two weeks is absurd” half of the problem outright.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;div class=&quot;cons&quot;&gt;&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It is not an idle timeout.&lt;/strong&gt; A 4-hour session lasts 4 hours whether the user works the whole time or leaves immediately. Set it to 15 minutes and you’ll log out people mid-sentence; set it to 4 hours and an abandoned machine stays open for 4 hours.&lt;/li&gt;
&lt;li&gt;No warning. The session just dies, and unsaved work in a half-written post dies with it.&lt;/li&gt;
&lt;li&gt;One value for the whole site unless you write role-branching logic yourself.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;p&gt;Use this filter when the requirement is “logins shouldn’t last two weeks.”
When the requirement is “log out &lt;em&gt;inactive&lt;/em&gt; users,” keep reading.&lt;/p&gt;
&lt;h2 id=&quot;method-2--a-php-snippet-that-tracks-activity-and-ends-stale-sessions&quot;&gt;Method 2 — A PHP snippet that tracks activity and ends stale sessions&lt;/h2&gt;
&lt;p&gt;A real idle timeout needs two pieces: record when each user was last active,
and end the session once that timestamp is older than your limit. Here’s a
minimal server-side version:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; MY_IDLE_TIMEOUT = &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;15&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; * MINUTE_IN_SECONDS;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// 1. Record activity (throttled to one write per minute).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;add_action&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;init&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; is_user_logged_in&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; wp_doing_ajax&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; wp_doing_cron&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;		return&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;user_id&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; get_current_user_id&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;last&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;    =&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; get_user_meta&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;user_id&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;my_last_active&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; time&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;last&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; MINUTE_IN_SECONDS &lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		update_user_meta&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;user_id&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;my_last_active&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; time&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// 2. Enforce the timeout before recording new activity.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;add_action&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;init&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; is_user_logged_in&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; wp_doing_ajax&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; wp_doing_cron&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;		return&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;user_id&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; get_current_user_id&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;last&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;    =&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; get_user_meta&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;user_id&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;my_last_active&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;	if&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;last&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; time&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;last&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; MY_IDLE_TIMEOUT &lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		wp_destroy_current_session&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		wp_clear_auth_cookie&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		wp_safe_redirect&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; wp_login_url&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;		exit&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;},&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;pros&quot;&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;An actual inactivity timeout, enforced server-side. Closing the tab or disabling JavaScript doesn’t help an attacker — the next request from that browser is dead.&lt;/li&gt;
&lt;li&gt;You own the code and can tune every rule.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;div class=&quot;cons&quot;&gt;&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The logout only happens on the next request.&lt;/strong&gt; An idle dashboard sits there looking signed-in until someone clicks; only then do they land on the login screen. Fixing that requires a JavaScript companion (Method 3).&lt;/li&gt;
&lt;li&gt;No warning countdown, so long-form editors lose unsaved work.&lt;/li&gt;
&lt;li&gt;The timestamp is &lt;em&gt;per user&lt;/em&gt;, but sessions are &lt;em&gt;per device&lt;/em&gt; — &lt;code&gt;wp_destroy_current_session()&lt;/code&gt; only ends the requesting device, while activity on a phone keeps the timestamp fresh for an abandoned laptop too. Per-session tracking means storing timestamps per token, which is where the snippet stops being short.&lt;/li&gt;
&lt;li&gt;Edge cases are on you: AJAX-heavy admin screens, the Heartbeat API firing from background tabs (count it as activity and an unattended dashboard never idles out), REST requests, multi-role users, WP-CLI.&lt;/li&gt;
&lt;li&gt;It’s custom code the next developer has to discover and maintain.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;p&gt;This snippet is the honest core of every idle-logout plugin ever written. The
gap between it and a finished tool is the last 80%: the warning, per-role rules,
per-session accuracy, and the Heartbeat problem.&lt;/p&gt;
&lt;h2 id=&quot;method-3--a-javascript-inactivity-timer-and-why-it-cant-stand-alone&quot;&gt;Method 3 — A JavaScript inactivity timer (and why it can’t stand alone)&lt;/h2&gt;
&lt;p&gt;The approach most tutorials reach for: a script that watches for mouse and
keyboard events and redirects to the logout URL after X quiet minutes.&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;JS&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; () {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;	const&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; IDLE_MS&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;15&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;60&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;1000&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;	let&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; timer&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;	function&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; reset&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;		clearTimeout&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;( &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;timer&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;		timer&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setTimeout&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;( () &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;			window&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;location&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;href&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;myVars&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;logoutUrl&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// localised wp_logout_url() + nonce&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;		}, &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;IDLE_MS&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	[ &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;mousemove&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;keydown&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;scroll&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;click&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;touchstart&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ].&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;forEach&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;		( &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ) &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; document&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;addEventListener&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;( &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;evt&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;reset&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, { passive: &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; } )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;	);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;	reset&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;})();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(The logout URL must be generated with &lt;code&gt;wp_logout_url()&lt;/code&gt; server-side and passed
via &lt;code&gt;wp_localize_script&lt;/code&gt;, because it carries a nonce.)&lt;/p&gt;
&lt;div class=&quot;pros&quot;&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;The logout happens &lt;em&gt;on time&lt;/em&gt;, visibly, right in front of the user — no waiting for a next request.&lt;/li&gt;
&lt;li&gt;Easy to extend into a warning dialog: show a countdown at 14 minutes, redirect at 15.&lt;/li&gt;
&lt;li&gt;The only method that can clean up the screen the moment time runs out.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;div class=&quot;cons&quot;&gt;&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It is not security — it’s UI.&lt;/strong&gt; Close the tab, drop the laptop lid, kill JavaScript, or block the redirect, and the server-side session remains fully valid for its whole 2–14 day life. Anyone reopening that browser is still signed in.&lt;/li&gt;
&lt;li&gt;Every tab runs its own timer. Typing in tab A while tab B’s timer expires logs you out of both, unless you sync activity across tabs (BroadcastChannel / localStorage events) — more code.&lt;/li&gt;
&lt;li&gt;The logout nonce expires after 24 hours, so a day-old tab redirects to a “link has expired” screen instead of logging out.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;p&gt;The right mental model: &lt;strong&gt;the server decides, the browser performs.&lt;/strong&gt; A
JavaScript timer is the correct way to make the logout visible and punctual, and
the wrong way to make it real. It needs Method 2 behind it — which means
building and maintaining both halves, plus the cross-tab sync between them.&lt;/p&gt;
&lt;h2 id=&quot;method-4--a-per-role-idle-timeout-from-the-admin-recommended&quot;&gt;Method 4 — A per-role idle timeout from the admin (recommended)&lt;/h2&gt;
&lt;p&gt;Everything above converges on one design: server-side activity tracking and
enforcement, a JavaScript layer for the countdown and punctual logout, throttled
writes so tracking doesn’t hammer the database, and per-role rules because an
administrator and a subscriber don’t warrant the same timeout. That’s precisely
what the &lt;a href=&quot;/software/plugins/loggedin/auto-logout&quot;&gt;Auto Logout add-on&lt;/a&gt; for the
free &lt;a href=&quot;/software/plugins/loggedin&quot;&gt;Loggedin plugin&lt;/a&gt; ships as a settings panel:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Idle timeout in minutes&lt;/strong&gt; — from 1 minute up to 30 days, off by default so
installing it never logs anyone out by surprise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A warning countdown&lt;/strong&gt; — a dialog appears before time runs out, with &lt;strong&gt;Stay
signed in&lt;/strong&gt; (renews the session) and &lt;strong&gt;Sign out now&lt;/strong&gt; buttons. Ignore it and
the logout happens at zero.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-role timeouts&lt;/strong&gt; — an hour for admins, fifteen minutes for authors,
nothing for subscribers. Unlisted roles keep the global value; when a user
holds several roles with rules, the shortest timeout wins.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom session lengths&lt;/strong&gt; — replaces WordPress’s hard-coded 2-day and 14-day
durations with your own values (Method 1, without the code snippet).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server-side enforcement&lt;/strong&gt; — the idle check runs early on every
authenticated request, so closing the tab or disabling JavaScript doesn’t
extend a session. The browser script only makes the logout punctual and
visible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The edge cases handled&lt;/strong&gt; — Heartbeat, AJAX, and REST requests are checked
but never counted as activity (so a background admin tab can’t keep a session
alive forever); WP-CLI and cron are exempt; activity in one tab keeps other
tabs alive with no extra requests; and writes are throttled to roughly one
small database write per user per quarter of the timeout.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It lives on the &lt;strong&gt;Users → Loggedin → Settings&lt;/strong&gt; screen. Set the minutes, save,
done.&lt;/p&gt;
&lt;h3 id=&quot;how-the-methods-compare&quot;&gt;How the methods compare&lt;/h3&gt;
&lt;div class=&quot;table-wrap&quot;&gt;




























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th style=&quot;text-align:center&quot;&gt;auth_cookie_expiration&lt;/th&gt;&lt;th style=&quot;text-align:center&quot;&gt;PHP snippet&lt;/th&gt;&lt;th style=&quot;text-align:center&quot;&gt;JS timer&lt;/th&gt;&lt;th style=&quot;text-align:center&quot;&gt;Auto Logout&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;True idle timeout (not just shorter sessions)&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;looks like one&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Enforced server-side&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Logout happens on time, visibly&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Warning countdown before logout&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;manual&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Per-role rules&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;manual&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;manual&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;manual&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Heartbeat/AJAX handled correctly&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;n/a&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;manual&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Non-technical owners can configure it&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The snippet methods are perfectly good for a developer who wants one specific
behaviour and owns the codebase. The add-on is the same architecture with the
last 80% finished — worth it the moment “log out idle users” is a requirement
someone will audit rather than a weekend experiment.&lt;/p&gt;
&lt;h2 id=&quot;picking-a-sensible-timeout&quot;&gt;Picking a sensible timeout&lt;/h2&gt;
&lt;p&gt;Numbers that hold up in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;5–15 minutes&lt;/strong&gt; for privileged roles — administrators, shop managers, anyone
who can see customer data. PCI DSS’s own idle-session guidance is 15 minutes,
and shared-computer environments (clinics, schools, front desks) sit at the
low end.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;15–30 minutes&lt;/strong&gt; for editorial roles, paired with a warning countdown so a
slow writing session doesn’t eat a draft.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An hour or more, or no idle timeout at all&lt;/strong&gt;, for subscribers and customers
who are only reading — aggressive logouts here cost you logins-per-visit and
goodwill for little security gain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session length&lt;/strong&gt; (the Method 1 value) can stay comparatively generous —
a day, say — once a real idle timeout exists, because the idle rule catches
abandoned sessions long before the cookie expires.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And whatever you choose, roll it out with the warning enabled. The difference
between “the site signed me out and lost my work” and “the site told me it was
about to sign me out” is the difference between a support ticket and a shrug.&lt;/p&gt;
&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;WordPress ships with no inactivity logout — a login lasts 2 days, or 14 with
“Remember Me,” no matter how long the browser sits untouched. You can shorten
that lifetime with one &lt;code&gt;auth_cookie_expiration&lt;/code&gt; filter, build a true idle
timeout with a PHP activity tracker, and make it punctual with a JavaScript
countdown — and now you know exactly which half of the problem each piece
solves, and where the sharp edges are (Heartbeat, multi-tab sync, per-session
tracking, expired nonces).&lt;/p&gt;
&lt;p&gt;For a site where idle logout is a requirement rather than an experiment, the
&lt;a href=&quot;/software/plugins/loggedin/auto-logout&quot;&gt;Auto Logout add-on&lt;/a&gt; packages that whole
architecture — server-side enforcement, warning countdown, per-role timeouts,
custom session lengths — into one settings panel on top of the free
&lt;a href=&quot;/software/plugins/loggedin&quot;&gt;Loggedin plugin&lt;/a&gt;. And if you also want to see who’s
signed in right now, or end a specific session yourself, that’s the
&lt;a href=&quot;/software/plugins/loggedin/active-sessions&quot;&gt;Active Sessions add-on&lt;/a&gt; — covered
in &lt;a href=&quot;/blog/view-active-login-sessions-wordpress&quot;&gt;its own guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;— JJ&lt;/p&gt;</content:encoded><category>Software</category><category>wordpress</category><category>security</category><category>loggedin</category><author>support@foxelabs.com (Joel James)</author></item><item><title>Gold Scalpel: a pre-tuned XAUUSD scalper EA for MT5</title><link>https://foxelabs.com/blog/gold-scalpel-xauusd-scalper-ea/</link><guid isPermaLink="true">https://foxelabs.com/blog/gold-scalpel-xauusd-scalper-ea/</guid><description>Gold Scalpel is our first expert advisor — a fully automated gold scalper for MetaTrader 5 that trades genuine XAUUSD breakouts, moves every trade to breakeven early, and ships pre-tuned with just eight settings.</description><pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today we’re shipping &lt;strong&gt;Gold Scalpel&lt;/strong&gt;, the first expert advisor out of Foxe Labs —
a fully automated &lt;strong&gt;XAUUSD scalper EA&lt;/strong&gt; for MetaTrader 5. It was built from a
specific frustration: most &lt;strong&gt;gold scalper&lt;/strong&gt; EAs on the market are either curve-fit
demos that fall apart out of sample, or account-blowing grid bots that martingale
their way to a margin call. We wanted the opposite — a disciplined gold scalper that
treats &lt;strong&gt;capital preservation as the first job&lt;/strong&gt; and edge as the second.&lt;/p&gt;
&lt;p&gt;Here’s what it is, how it trades, and how to try it.&lt;/p&gt;
&lt;h2 id=&quot;what-is-gold-scalpel&quot;&gt;What is Gold Scalpel?&lt;/h2&gt;
&lt;p&gt;Gold Scalpel is a pre-tuned, fully automated MQL5 expert advisor for &lt;strong&gt;XAUUSD
(gold)&lt;/strong&gt; on &lt;strong&gt;MetaTrader 5&lt;/strong&gt;. Rather than predicting direction, it identifies
meaningful highs and lows around price, positions itself on &lt;strong&gt;both sides&lt;/strong&gt; of the
range, and commits only when price genuinely breaks through — never on a touch or a
near-miss. The moment it’s in a trade, the focus switches to protection: the stop
advances to &lt;strong&gt;breakeven&lt;/strong&gt; as soon as the trade is onside, then trails the move.&lt;/p&gt;
&lt;p&gt;It ships &lt;strong&gt;dialled in at the factory&lt;/strong&gt; — the strategy’s internals are fixed, and you
adjust only a handful of account-specific settings (about eight in total). There’s
nothing to optimise, no dozens of parameters to guess at.&lt;/p&gt;
&lt;h2 id=&quot;how-the-gold-scalping-strategy-works&quot;&gt;How the gold scalping strategy works&lt;/h2&gt;
&lt;p&gt;Every trade follows the same disciplined lifecycle:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Wait for a real move.&lt;/strong&gt; No breakout, no trade — the EA stays patient.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enter on the break.&lt;/strong&gt; It’s already positioned on both sides, so it catches the
genuine push when it comes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Protect quickly.&lt;/strong&gt; As soon as the trade is onside, the stop moves to breakeven,
so a winner rarely turns back into a loser.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trail the move.&lt;/strong&gt; The stop follows price, locking in more of a clean run and
exiting automatically when momentum fades.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stand aside.&lt;/strong&gt; A short cooldown after each trade avoids re-entering the same
exhausted move.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That produces an &lt;strong&gt;asymmetric risk profile&lt;/strong&gt;: most trades end at scratch or a small
profit, a few ride a clean breakout for a real win, and large losses are rare
because protection kicks in fast. You don’t need a high win rate — you need the
&lt;strong&gt;average loss kept tiny&lt;/strong&gt;, which is exactly what early breakeven is engineered to
do. There’s &lt;strong&gt;no martingale, grid, or averaging&lt;/strong&gt; into losers.&lt;/p&gt;
&lt;h2 id=&quot;risk-control-comes-first&quot;&gt;Risk control comes first&lt;/h2&gt;
&lt;p&gt;A scalper lives and dies by its losers, so Gold Scalpel runs several protections
automatically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Early breakeven &amp;amp; trailing&lt;/strong&gt; — the primary safeguard that keeps the average loss
small.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Position cap&lt;/strong&gt; — bounds how many trades can be open at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Post-trade cooldown&lt;/strong&gt; — a short pause that prevents over-trading one move.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optional losing-streak cooldown&lt;/strong&gt; — pause new entries after a run of losses, then
resume automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic weekend pause&lt;/strong&gt; — cancels resting orders and stops new trades into the
weekend gap.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maximum-lot ceiling&lt;/strong&gt; — a hard cap on size, whatever the risk calc suggests.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Position sizing is &lt;strong&gt;percentage-of-equity&lt;/strong&gt;: set your risk percentage and the EA
computes the lot, capped by your maximum lot and snapped to your broker’s lot step.
A live &lt;strong&gt;on-chart dashboard&lt;/strong&gt; shows account, strategy state, and position counts,
refreshed every second — even when the market is closed.&lt;/p&gt;
&lt;h2 id=&quot;what-the-testing-shows&quot;&gt;What the testing shows&lt;/h2&gt;
&lt;p&gt;Gold Scalpel’s performance is tracked forward in real time on a public
&lt;strong&gt;&lt;a href=&quot;https://www.mql5.com/en/signals/2382197?source=foxelabs.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;MQL5 live signal&lt;/a&gt;&lt;/strong&gt;.
For a reference point, here’s a single real-tick backtest of the shipping build — one
run, on one broker, over one period:&lt;/p&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Test condition&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Symbol / platform&lt;/td&gt;&lt;td&gt;XAUUSD · MetaTrader 5&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Period&lt;/td&gt;&lt;td&gt;~18 months (Jan 2025 – Jul 2026)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Modelling&lt;/td&gt;&lt;td&gt;Every tick based on real ticks (100%)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Risk per trade&lt;/td&gt;&lt;td&gt;2.0% (the conservative default)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Win rate&lt;/td&gt;&lt;td&gt;~78%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Profit factor&lt;/td&gt;&lt;td&gt;~3.9&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Max drawdown&lt;/td&gt;&lt;td&gt;~6.8%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Total trades&lt;/td&gt;&lt;td&gt;319&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;At that 2% default risk, the ~18-month run compounded &lt;strong&gt;$1,000 into ~$15,232 — a
+1,423% return&lt;/strong&gt;. Because sizing is percentage-of-equity and compounding, that figure
scales directly with your risk setting: raising the risk above the default produces a
much larger return &lt;em&gt;and&lt;/em&gt; a proportionally larger drawdown. Most traders should stay at
&lt;strong&gt;1–2%&lt;/strong&gt; for a smoother curve like this one.&lt;/p&gt;
&lt;p&gt;The high win rate is a &lt;em&gt;by-product&lt;/em&gt; of early breakeven — many trades are protected
before they can turn into losers — not a promise that most trades always win. The
metrics that actually describe the edge are the risk-adjusted ones: &lt;strong&gt;win
consistency, profit factor, and drawdown&lt;/strong&gt;. Be sceptical of raw percentage returns,
because sizing is percentage-of-equity and compounding, so the headline return scales
directly with your risk setting — a bigger number at higher risk is the &lt;em&gt;same&lt;/em&gt;
strategy taking more risk, not a better one.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Trading involves risk.&lt;/strong&gt; Every figure above is from a &lt;strong&gt;backtest&lt;/strong&gt;, not live
trading. Past performance — including real-tick backtests and the live signal — does
&lt;strong&gt;not&lt;/strong&gt; guarantee future results, and results vary with your broker, spread, period,
and risk setting. Always validate on the free demo and a demo account before
committing real capital, and only risk what you can afford to lose.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;who-its-for-and-what-you-need&quot;&gt;Who it’s for (and what you need)&lt;/h2&gt;
&lt;p&gt;Gold Scalpel is for traders who want a hands-off gold scalper with a few simple, safe
settings rather than a parameter jungle — and who prefer many small, tightly-managed
trades over a few big swings. Because it’s a scalper, &lt;strong&gt;spread is the single biggest
factor&lt;/strong&gt; in how it performs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Platform:&lt;/strong&gt; MetaTrader 5, on the &lt;strong&gt;XAUUSD&lt;/strong&gt; symbol, any chart timeframe.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Account:&lt;/strong&gt; hedging or netting, &lt;strong&gt;raw-spread or zero-spread&lt;/strong&gt; strongly recommended.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Balance:&lt;/strong&gt; runs from $100 in testing; &lt;strong&gt;$1,000+&lt;/strong&gt; recommended for live capital,
paired with a conservative 1–2% risk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VPS:&lt;/strong&gt; recommended for 24/5 operation, not required.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;how-to-get-it&quot;&gt;How to get it&lt;/h2&gt;
&lt;p&gt;Gold Scalpel is a &lt;strong&gt;one-time $199 purchase&lt;/strong&gt; on the MQL5 Market — lifetime updates,
activation on up to 10 devices, and a &lt;strong&gt;free demo&lt;/strong&gt; that runs in the MT5 Strategy
Tester so you can backtest on your own gold history first.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Product details:&lt;/strong&gt; &lt;a href=&quot;/trading/expert-advisors/gold-scalpel&quot;&gt;Gold Scalpel on Foxe Labs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buy or download the free demo:&lt;/strong&gt; &lt;a href=&quot;https://www.mql5.com/en/market/product/175235?source=foxelabs.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Gold Scalpel on the MQL5 Market&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Setup guide:&lt;/strong&gt; &lt;a href=&quot;https://docs.foxelabs.com/trading/gold-scalpel/getting-started&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Getting started docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gold Scalpel is the first of several tools on our roadmap — a universal trading
journal and a set of precision indicators are in the works. Test it honestly, size it
sensibly, and let the discipline compound.&lt;/p&gt;</content:encoded><category>Trading</category><category>expert-advisors</category><category>xauusd</category><category>mql5</category><author>support@foxelabs.com (Joel James)</author></item><item><title>How to view active login sessions on a WordPress site</title><link>https://foxelabs.com/blog/view-active-login-sessions-wordpress/</link><guid isPermaLink="true">https://foxelabs.com/blog/view-active-login-sessions-wordpress/</guid><description>WordPress lists every account that exists, but never the ones signed in right now. Here are four ways to see active login sessions — from a raw SQL query to a one-click admin tool — with honest pros and cons for each.</description><pubDate>Sat, 27 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;WordPress is famously bad at one thing: telling you who’s actually signed in right
now. The Users screen lists every account that &lt;em&gt;exists&lt;/em&gt; on the site — not the ones
that are logged in. There’s no built-in “active sessions” panel, no per-device
list, no last-seen column. For most sites this gap is invisible. For some it’s a
real problem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A membership site where one paid account is being shared across a family, a
classroom, or a Discord server.&lt;/li&gt;
&lt;li&gt;An LMS install where students log in from a coffee shop, forget to sign out, and
a stolen laptop quietly keeps the seat warm for weeks.&lt;/li&gt;
&lt;li&gt;A WooCommerce store where a customer just got refunded and you’d like to revoke
their access without waiting for the session cookie to expire.&lt;/li&gt;
&lt;li&gt;A staff portal where the security team needs to know which devices a leaver was
still signed into when they handed in their notice.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If any of those sound familiar, this guide is for you. Below are four ways to view
currently active login sessions on a WordPress website — from a raw database query
through to a one-click admin tool — with honest pros and cons for each.&lt;/p&gt;
&lt;h2 id=&quot;first-whats-a-session-in-wordpress&quot;&gt;First, what’s a “session” in WordPress?&lt;/h2&gt;
&lt;p&gt;A WordPress session is created the moment a user successfully logs in. Internally
it’s a &lt;em&gt;session token&lt;/em&gt; — a random string stored against the user in the database.
The token is also placed in an authentication cookie on the user’s browser. Every
subsequent request hits the server, the cookie comes along, and WordPress matches
the cookie against the stored token to keep the user signed in.&lt;/p&gt;
&lt;p&gt;Two important facts that catch people out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One session per browser, per device.&lt;/strong&gt; A phone and a laptop count as two
sessions. Two Chrome profiles on the same laptop count as two. An incognito
window counts as a third.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Closing the tab does not end the session.&lt;/strong&gt; The token lives on the server until
the user explicitly clicks Log Out, the token expires (2 days by default, 14 days
if “Remember Me” was ticked), or another login displaces it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s why a stale browser tab a week later can still render as if the user is
signed in — and why simply asking “is anyone logged in?” is a more useful question
than it sounds.&lt;/p&gt;
&lt;p&gt;WordPress stores these tokens in the &lt;code&gt;wp_usermeta&lt;/code&gt; table under the meta key
&lt;code&gt;session_tokens&lt;/code&gt;. That’s the same row every method below reads from.&lt;/p&gt;
&lt;h2 id=&quot;method-1--run-a-sql-query-against-wp_usermeta&quot;&gt;Method 1 — Run a SQL query against wp_usermeta&lt;/h2&gt;
&lt;p&gt;The most direct way to see active sessions is to ask the database. Open phpMyAdmin,
Adminer, or your hosting control panel’s SQL console, and run:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; u.ID, u.user_login, u.user_email, um.meta_value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; wp_users u&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;JOIN&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; wp_usermeta um &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;ON&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; um.user_id = u.ID&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; um.meta_key = &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;session_tokens&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You’ll get one row per user who has ever logged in and not been pruned. The
&lt;code&gt;meta_value&lt;/code&gt; column is a serialized PHP array of every token that user holds, keyed
by token verifier. Each token entry looks like this once you unserialize it:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    &amp;#39;expiration&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&amp;gt; &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;1719999999&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,   &lt;/span&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// unix timestamp&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    &amp;#39;login&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;      =&amp;gt; &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;1719399999&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,   &lt;/span&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// unix timestamp&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    &amp;#39;ip&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;         =&amp;gt; &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;203.0.113.4&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    &amp;#39;ua&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;         =&amp;gt; &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;Mozilla/5.0 …&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;pros&quot;&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Works on every WordPress install. No plugins, no code.&lt;/li&gt;
&lt;li&gt;Gives you the raw data — IP, user-agent, login time, expiry.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;div class=&quot;cons&quot;&gt;&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;meta_value&lt;/code&gt; is serialized PHP. Reading it in your head is unrealistic past a few rows.&lt;/li&gt;
&lt;li&gt;Expired tokens stick around until the user logs in again, so the count is not the same as “currently active.” You have to filter &lt;code&gt;expiration &amp;gt; NOW()&lt;/code&gt; yourself.&lt;/li&gt;
&lt;li&gt;No search. No sort. No pagination.&lt;/li&gt;
&lt;li&gt;Anyone running this needs database access — usually a developer, not the site owner.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;p&gt;This method is fine for a one-off diagnostic. It’s not a workflow.&lt;/p&gt;
&lt;h2 id=&quot;method-2--use-wp-cli&quot;&gt;Method 2 — Use WP-CLI&lt;/h2&gt;
&lt;p&gt;If you have shell access, WP-CLI gives you the same data in a much more readable
form. To dump every user’s active sessions:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;BASH&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; list&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; --field=ID&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; | &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; read&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; uid&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  echo&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;quot;User #&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;$uid&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;:&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  wp&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; eval&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    \$tokens = get_user_meta( &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;$uid&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;, &amp;#39;session_tokens&amp;#39;, true );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    \$tokens = is_array( \$tokens ) ? \$tokens : array();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    foreach ( \$tokens as \$hash =&amp;gt; \$t ) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;      if ( \$t[&amp;#39;expiration&amp;#39;] &amp;gt; time() ) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;        echo &amp;#39;  &amp;#39;, date( &amp;#39;Y-m-d H:i&amp;#39;, \$t[&amp;#39;login&amp;#39;] ),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;             &amp;#39;  &amp;#39;, \$t[&amp;#39;ip&amp;#39;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;             &amp;#39;  &amp;#39;, substr( \$t[&amp;#39;ua&amp;#39;], 0, 60 ), \&amp;quot;\n\&amp;quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;  &amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;done&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can also destroy any user’s sessions in one line:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;BASH&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;wp&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; session&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; destroy&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;r&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;pros&quot;&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;No serialization to read by eye — PHP does the work.&lt;/li&gt;
&lt;li&gt;Scriptable. Easy to drop into a cron or a bash script.&lt;/li&gt;
&lt;li&gt;The built-in &lt;code&gt;wp user session destroy&lt;/code&gt; command is reliable.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;div class=&quot;cons&quot;&gt;&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Requires SSH access and WP-CLI installed.&lt;/li&gt;
&lt;li&gt;No nice list view. The output is whatever your terminal can render.&lt;/li&gt;
&lt;li&gt;Still no search across users by anything except ID.&lt;/li&gt;
&lt;li&gt;Not something you can hand to a non-technical site owner.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;p&gt;WP-CLI is the right tool for one-off incident response if you’re already on the
box. It’s still not a workflow.&lt;/p&gt;
&lt;h2 id=&quot;method-3--a-short-php-snippet-using-wp_session_tokens&quot;&gt;Method 3 — A short PHP snippet using WP_Session_Tokens&lt;/h2&gt;
&lt;p&gt;WordPress exposes a clean PHP API for working with session tokens: the
&lt;code&gt;WP_Session_Tokens&lt;/code&gt; class. Drop the following into a small companion plugin (or
your theme’s &lt;code&gt;functions.php&lt;/code&gt; if you really must) to print a list of every user with
at least one active session:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;add_action&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;admin_notices&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ()&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; current_user_can&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;manage_options&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;    $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;users&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; get_users&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; array&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;fields&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; array&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;ID&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;user_login&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    echo &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;&amp;lt;div class=&amp;quot;notice notice-info&amp;quot;&amp;gt;&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Active sessions:&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;ul&amp;gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;    foreach&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;users&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; as&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;u&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;        $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;manager&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;  =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; WP_Session_Tokens&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;::&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;get_instance&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;u&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;ID&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;        $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;sessions&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;manager&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;get_all&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;();&lt;/span&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; // array of currently-valid tokens&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; empty&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;sessions&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;            continue&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        printf&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;            &amp;#39;&amp;lt;li&amp;gt;%s — %d active session(s)&amp;lt;/li&amp;gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;            esc_html&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;u&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;user_login&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;            count&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;sessions&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;        );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    echo &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;&amp;lt;/ul&amp;gt;&amp;lt;/div&amp;gt;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;pros&quot;&gt;&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Uses the official WP API. &lt;code&gt;get_all()&lt;/code&gt; already filters out expired tokens, so the count is correct.&lt;/li&gt;
&lt;li&gt;Compatible with custom session backends (Redis, Memcached) — anything that implements &lt;code&gt;WP_Session_Tokens&lt;/code&gt; is honoured.&lt;/li&gt;
&lt;li&gt;A great starting point if you want to build something custom.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;div class=&quot;cons&quot;&gt;&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;
&lt;li&gt;Iterating every user is expensive past a few hundred. On a 50,000-user site this will time out.&lt;/li&gt;
&lt;li&gt;It’s still just a list. No per-device drilldown, no IP, no sign-out button.&lt;/li&gt;
&lt;li&gt;You’re maintaining custom code. The next person has to figure out what it does.&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
&lt;p&gt;This one is closer to a real solution than the previous two, but you’re effectively
writing the start of an admin tool you’ll never quite finish.&lt;/p&gt;
&lt;h2 id=&quot;a-quick-word-on-plugins-people-try-that-dont-do-this&quot;&gt;A quick word on plugins people try (that don’t do this)&lt;/h2&gt;
&lt;p&gt;A common piece of advice on forums is to install &lt;strong&gt;User Switching&lt;/strong&gt;. It’s an
excellent plugin — for switching between users — but it does not show you who is
currently signed in. It’s worth mentioning because if you search for “see who’s
logged in WordPress,” you’ll find it recommended, and you’ll waste an hour
discovering it doesn’t do what you want.&lt;/p&gt;
&lt;p&gt;Same goes for &lt;strong&gt;WP Activity Log&lt;/strong&gt;, &lt;strong&gt;Stream&lt;/strong&gt;, and other audit-logging plugins.
They tell you who did things and when. They don’t tell you who is signed in right
now.&lt;/p&gt;
&lt;h2 id=&quot;method-4--a-one-click-admin-tool-recommended&quot;&gt;Method 4 — A one-click admin tool (recommended)&lt;/h2&gt;
&lt;p&gt;If you’ve made it this far, the through-line is obvious: WordPress has every piece
of information you need in the database, but no UI to look at it. The fastest way to
close that gap is to add the missing UI.&lt;/p&gt;
&lt;p&gt;The free &lt;a href=&quot;/software/plugins/loggedin&quot;&gt;Loggedin plugin&lt;/a&gt; already adds a
concurrent-login limit and a Force Logout panel to the &lt;strong&gt;Users → Loggedin&lt;/strong&gt; screen.
Its &lt;a href=&quot;/software/plugins/loggedin/active-sessions&quot;&gt;Active Sessions add-on&lt;/a&gt; extends
that screen with a dedicated &lt;strong&gt;Sessions&lt;/strong&gt; tab showing every user with at least one
live session, in one sortable, searchable list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One row per user — display name, username, email, role, session count, last login
time, last IP.&lt;/li&gt;
&lt;li&gt;Sortable by username, email, session count, or last login.&lt;/li&gt;
&lt;li&gt;Searchable across username, email, and display name.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-device drilldown&lt;/strong&gt; — click any row to open a modal listing every device
that user is signed in from, with IP, user-agent, sign-in time, and expiry side by
side.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single-session sign-out&lt;/strong&gt; — revoke one device without disturbing the others
(perfect for a stolen phone or an old tablet the user forgot about).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One-click sign-out-all&lt;/strong&gt; — kill every session for a user, same as Loggedin’s
Force Logout panel but without typing the username.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bulk sign-out from the list&lt;/strong&gt; — select multiple users and sign them all out in
one action (handy after a credential leak or a refund batch).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It reads from the same &lt;code&gt;WP_Session_Tokens&lt;/code&gt; storage every method above reads from,
so the data is identical — there’s no new database table, no migration, no cron.
Install it, open the tab, see your sessions.&lt;/p&gt;
&lt;h3 id=&quot;why-this-is-the-practical-answer&quot;&gt;Why this is the practical answer&lt;/h3&gt;
&lt;div class=&quot;table-wrap&quot;&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th style=&quot;text-align:center&quot;&gt;SQL query&lt;/th&gt;&lt;th style=&quot;text-align:center&quot;&gt;WP-CLI&lt;/th&gt;&lt;th style=&quot;text-align:center&quot;&gt;PHP snippet&lt;/th&gt;&lt;th style=&quot;text-align:center&quot;&gt;Active Sessions&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Non-technical owners can use it&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Auto-filters expired tokens&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;manual&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Per-device view (IP, UA, expiry)&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;manual&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;manual&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;manual&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Sign someone out from the UI&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;partial&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Bulk operations&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;scripted&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Scales to thousands of users&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;—&lt;/td&gt;&lt;td style=&quot;text-align:center&quot;&gt;✓&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The other methods are perfectly valid for a one-off diagnostic or a developer who
just needs to see what’s there. For anything you’re going to do more than once,
having a real UI on top of the data is the difference between a workflow and a
chore.&lt;/p&gt;
&lt;h2 id=&quot;what-about-signing-someone-out-once-youve-found-them&quot;&gt;What about signing someone out once you’ve found them?&lt;/h2&gt;
&lt;p&gt;Whichever method you use to see sessions, ending one is straightforward. WordPress
ships the API; you just have to call it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One session, in code:&lt;/strong&gt; &lt;code&gt;WP_Session_Tokens::get_instance( $user_id )-&amp;gt;destroy( $token_verifier );&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All sessions for a user, in code:&lt;/strong&gt; &lt;code&gt;WP_Session_Tokens::get_instance( $user_id )-&amp;gt;destroy_all();&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All sessions for a user, in WP-CLI:&lt;/strong&gt; &lt;code&gt;wp user session destroy &amp;lt;user&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All sessions for a user, in the admin:&lt;/strong&gt; the core
&lt;a href=&quot;/software/plugins/loggedin&quot;&gt;Loggedin plugin&lt;/a&gt;’s Force Logout panel (free) accepts
a user ID, email, or username and clears them in one click.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-device sign-out in the admin:&lt;/strong&gt; the
&lt;a href=&quot;/software/plugins/loggedin/active-sessions&quot;&gt;Active Sessions add-on&lt;/a&gt;’s per-user
modal — the only built-in option here that lets you revoke a single device
without disturbing the others.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A signed-out user won’t be notified, but the next page they try to load (or the next
AJAX request from an already-open tab) will redirect them to &lt;code&gt;wp-login&lt;/code&gt;. If you want
that to happen the moment you click — without waiting for the user’s next click —
pair Loggedin with its
&lt;a href=&quot;/software/plugins/loggedin/realtime-logout&quot;&gt;Real-time Logout add-on&lt;/a&gt;, which reloads
the user’s open tabs the moment their session ends.&lt;/p&gt;
&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;WordPress doesn’t show you who is currently signed in by default, but the data has
always been there in the &lt;code&gt;wp_usermeta&lt;/code&gt; table. You can read it with SQL, with
WP-CLI, or with a short PHP snippet — all three work, and any of them is fine for a
quick one-off check.&lt;/p&gt;
&lt;p&gt;For a workflow you’ll repeat — handling membership-share disputes, responding to a
credential leak, kicking a refunded customer, auditing devices for a leaver — the
answer is to add the missing UI rather than re-deriving it from raw data every time.
That’s the gap the Active Sessions add-on fills, sitting on top of the free
Loggedin plugin’s session controls.&lt;/p&gt;
&lt;p&gt;Either way, you now know what a WordPress session actually is, where it lives, how
to find it, and how to end it. That alone puts you ahead of most install owners.&lt;/p&gt;
&lt;p&gt;— JJ&lt;/p&gt;</content:encoded><category>Software</category><category>wordpress</category><category>security</category><category>loggedin</category><author>support@foxelabs.com (Joel James)</author></item><item><title>404 to 301 v4: a complete rewrite for custom redirects and 404 error management</title><link>https://foxelabs.com/blog/404-to-301-v4-custom-redirects-and-404-manager/</link><guid isPermaLink="true">https://foxelabs.com/blog/404-to-301-v4-custom-redirects-and-404-manager/</guid><description>404 to 301 v4 is a ground-up rewrite — a React admin, a full custom redirect manager, indexed database tables, a REST API, WP-CLI support, and an official add-ons catalogue.</description><pubDate>Sat, 20 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;After years of steady iteration, &lt;a href=&quot;/software/plugins/404-to-301&quot;&gt;404 to 301&lt;/a&gt; v4 is
here — rebuilt from the ground up with a modern React admin, a full custom redirect
manager, a faster database layer, a REST API, WP-CLI support, and a growing
ecosystem of official add-ons. If broken links and 404 errors are costing you SEO
equity and visitors, this release gives you precise control over every URL on your
site.&lt;/p&gt;
&lt;h2 id=&quot;why-custom-redirects-and-404-errors-matter-for-seo&quot;&gt;Why custom redirects and 404 errors matter for SEO&lt;/h2&gt;
&lt;p&gt;Every 404 error on your WordPress site is a missed opportunity. When a visitor — or
a search engine crawler — lands on a broken URL, they hit a dead end. The link
equity built up by that URL evaporates, the visitor leaves, and your bounce rate
climbs.&lt;/p&gt;
&lt;p&gt;Custom redirects — specifically 301 permanent redirects — pass the full SEO value of
an old URL to the new one. They are essential after a site migration, a URL
restructure, a domain change, or any redesign that moves pages around. Without them,
you quietly bleed organic traffic every day.&lt;/p&gt;
&lt;p&gt;But redirects alone are not the whole picture. You need to know which links are
broken before you can fix them. That is where 404 logging comes in — and it is what
has always made 404 to 301 different from a plain redirect plugin.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A redirect plugin fixes broken links. 404 to 301 finds them too.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;whats-new-in-v4&quot;&gt;What’s new in v4&lt;/h2&gt;
&lt;p&gt;Version 4 is a complete rewrite. The old admin has been replaced with a fast, modern
React-powered interface. The database layer now uses dedicated, indexed tables for
both logs and redirects, so every operation — matching a redirect, logging a 404,
running a bulk action — is faster at scale.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom redirect manager with exact, prefix and regex matching&lt;/li&gt;
&lt;li&gt;Per-redirect type, active/inactive toggle, hit counter and last-hit timestamp&lt;/li&gt;
&lt;li&gt;Dedicated, indexed database tables for logs and redirects&lt;/li&gt;
&lt;li&gt;Modern logs and redirects tables with search, filters, bulk actions and pagination&lt;/li&gt;
&lt;li&gt;Per-log lifecycle status — open, ignored, or fixed&lt;/li&gt;
&lt;li&gt;Email notifications with a configurable hit threshold&lt;/li&gt;
&lt;li&gt;REST API at &lt;code&gt;/wp-json/404-to-301/v1/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;WP-CLI command set — &lt;code&gt;wp 404-to-301 logs|redirects|settings&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;GDPR-friendly logging with IP masking and path exclusions&lt;/li&gt;
&lt;li&gt;Official add-ons catalogue for free and premium extensions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;custom-redirect-manager&quot;&gt;Custom redirect manager&lt;/h2&gt;
&lt;p&gt;The headline feature in v4 is the built-in redirect manager. You can now create
unlimited custom redirects directly inside 404 to 301 — no separate plugin needed.&lt;/p&gt;
&lt;h3 id=&quot;three-matching-modes&quot;&gt;Three matching modes&lt;/h3&gt;
&lt;p&gt;Every redirect rule uses one of three match types. &lt;strong&gt;Exact&lt;/strong&gt; matching targets one
specific URL path and nothing else. &lt;strong&gt;Prefix&lt;/strong&gt; matching catches any URL that begins
with the given path — useful after restructuring a section of your site, such as
moving everything under &lt;code&gt;/blog/&lt;/code&gt; to &lt;code&gt;/articles/&lt;/code&gt;. &lt;strong&gt;Regex&lt;/strong&gt; matching gives you full
regular expression support for complex, pattern-based rules, which is particularly
powerful for developers managing large URL sets.&lt;/p&gt;
&lt;h3 id=&quot;per-redirect-control&quot;&gt;Per-redirect control&lt;/h3&gt;
&lt;p&gt;Each rule carries its own redirect type — 301, 302, 307 or any other valid HTTP
status — so you are not locked into a single site-wide setting. Toggle any redirect
active or inactive without deleting it. A hit counter and last-hit timestamp on
every row show you which rules are actually being used, so you can confidently clean
up stale rules over time.&lt;/p&gt;
&lt;h3 id=&quot;global-404-fallback&quot;&gt;Global 404 fallback&lt;/h3&gt;
&lt;p&gt;For any 404 that does not match a custom rule, you can set a global fallback
destination — your homepage, a custom page, or any URL — with the redirect type of
your choice. It is the safety net that catches everything you have not written an
explicit rule for.&lt;/p&gt;
&lt;p&gt;The matching engine uses a hashed, indexed database lookup so redirect resolution
stays fast even with thousands of rules in place. Normal, healthy page loads are
never touched — the plugin only does work when a 404 occurs.&lt;/p&gt;
&lt;h2 id=&quot;404-error-logging&quot;&gt;404 error logging&lt;/h2&gt;
&lt;p&gt;Knowing which URLs are broken is just as important as redirecting them. The v4 logs
table is a significant upgrade.&lt;/p&gt;
&lt;p&gt;Every 404 is recorded with the requested URL, referrer, user agent, IP address and
timestamp. Repeat hits to the same URL are deduplicated — one row per unique URL
with a running hit count, so a popular broken link does not flood your table with
thousands of identical rows.&lt;/p&gt;
&lt;p&gt;Each log entry carries a lifecycle status — open, ignored, or fixed — so you can
triage and track your broken link backlog over time. Filter by date range and
status, search by URL or referrer, and bulk-update or delete entries in a single
action. You can also turn any logged 404 into a custom redirect in a couple of
clicks; the URL pre-fills the redirect form so there is no copy-pasting involved.&lt;/p&gt;
&lt;h2 id=&quot;email-notifications&quot;&gt;Email notifications&lt;/h2&gt;
&lt;p&gt;You do not need to log into the dashboard to know when broken links appear. The
built-in email notification sends an alert when a 404 is recorded, with a
configurable hit threshold so busy sites do not flood your inbox with every single
occurrence. Set the threshold to five hits and you will only hear about URLs that
have genuinely failed multiple times — a real problem worth acting on, not a one-off
crawler hiccup.&lt;/p&gt;
&lt;h2 id=&quot;rest-api-and-wp-cli&quot;&gt;REST API and WP-CLI&lt;/h2&gt;
&lt;p&gt;Version 4 is built for developers and modern WordPress workflows. A full REST API at
&lt;code&gt;/wp-json/404-to-301/v1/&lt;/code&gt; covers every admin operation — querying and managing logs,
creating and updating redirects, reading and writing settings. If you are building
headless workflows, automating site maintenance, or feeding 404 data into external
dashboards or reporting tools, the API gives you a clean interface to work against.&lt;/p&gt;
&lt;p&gt;The complete CLI command set is available via &lt;code&gt;wp 404-to-301&lt;/code&gt;, with subcommands for
&lt;code&gt;logs&lt;/code&gt;, &lt;code&gt;redirects&lt;/code&gt; and &lt;code&gt;settings&lt;/code&gt;. Multisite is fully supported — each site in the
network maintains its own separate redirects, logs and settings.&lt;/p&gt;
&lt;h2 id=&quot;official-add-ons&quot;&gt;Official add-ons&lt;/h2&gt;
&lt;p&gt;v4 ships with an official add-ons catalogue — free and premium extensions that
integrate directly with the parent plugin. Each add-on does one thing well.&lt;/p&gt;
&lt;h3 id=&quot;free-add-ons&quot;&gt;Free add-ons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redirects Importer&lt;/strong&gt; lets you bulk-import custom redirects from a CSV file,
or migrate your existing rules directly from other redirect plugins —
Redirection by John Godley and 301 Redirects by WebFactory are both supported.
No manual re-entry, no copy-pasting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs Exporter&lt;/strong&gt; adds a one-click CSV export to the 404 Logs page. The export
is filter-aware, so you can export just the rows matching your current search
or date range — ready for Google Sheets, Excel, or your SEO team’s reports.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;premium-add-ons&quot;&gt;Premium add-ons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Logs Cleaner&lt;/strong&gt; automatically prunes the 404 log table by age, by row count,
or on a recurring schedule. Set it once and the database stays lean without
any manual housekeeping — essential for high-traffic sites where the log table
would otherwise grow without bound.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Email Reports&lt;/strong&gt; delivers scheduled digests of your 404 activity — daily,
weekly or monthly — straight to your inbox, each with an attached CSV of the
underlying log rows. Stay on top of broken links without logging into the
dashboard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Telegram Alerts&lt;/strong&gt; pushes a Telegram message the moment a 404 error is
recorded. Alerts are dispatched in the background so visitors never wait on
the Telegram API — you get real-time visibility into broken links with no
front-end slowdown.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;get-started&quot;&gt;Get started&lt;/h2&gt;
&lt;p&gt;404 to 301 is free on WordPress.org. Install it from your plugins screen or download
it directly. If you are upgrading from v3, back up your database first — v4 creates
new dedicated tables and migrates your existing data on activation.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wordpress.org/plugins/404-to-301/&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Download 404 to 301 free on WordPress.org →&lt;/a&gt;&lt;/p&gt;</content:encoded><category>Software</category><category>wordpress</category><category>seo</category><category>plugins</category><author>support@foxelabs.com (Joel James)</author></item><item><title>From enemies to best friends</title><link>https://foxelabs.com/blog/from-enemies-to-best-friends/</link><guid isPermaLink="true">https://foxelabs.com/blog/from-enemies-to-best-friends/</guid><description>Programming and I started out as enemies. This is the story of how it became the thing I do best — a seven-year detour through mechanical engineering that never happened, hacking, and one very good teacher.</description><pubDate>Wed, 23 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A disclaimer before we start: I write software for a living, not stories. I’ve
never done this before, so bear with me if the seams show.&lt;/p&gt;
&lt;p&gt;Okay. Once upon a time.&lt;/p&gt;
&lt;p&gt;I’m Joel James, a software engineer of about seven years now. I’ve meant to write
this down for most of them — I actually started it back in my second year of
college — but like a lot of side projects, it sat at ninety percent finished
while I told myself I’d get to it. This is me finally getting to it.&lt;/p&gt;
&lt;h2 id=&quot;the-beginning&quot;&gt;The beginning&lt;/h2&gt;
&lt;p&gt;I very nearly became a mechanical engineer. That was the plan. I showed up to
enrol, the mechanical seats were already gone, and the only thing left was a
B.Tech in Information Technology. So that’s the seat I took — less a decision than
a leftover.&lt;/p&gt;
&lt;p&gt;Up to that point, a computer was the thing I switched on during the Computer
Period at school to open MS Paint. That was the full extent of it. Programming, when
I finally met it, was not love at first sight. It was closer to a running feud —
something I fought with every single day and mostly lost. Somewhere in there,
though, the machine itself started to win me over, even while the code on it was
still my sworn enemy.&lt;/p&gt;
&lt;h2 id=&quot;the-turning-point-that-wasnt&quot;&gt;The turning point that wasn’t&lt;/h2&gt;
&lt;p&gt;Late in my second year a relative handed me an old computer of my own, and that
computer led me straight into hacking, which was where all my actual interest
lived. Programming and I were still not on speaking terms. I finished my degree
with the bare minimum marks and told myself, with real conviction, that I was
done with code for good.&lt;/p&gt;
&lt;p&gt;Real life had other ideas. Hacking, it turned out, was not going to pay the
rent. With nowhere else to turn I looked back over my shoulder — and there was
programming, still standing right where I’d left it. So I signed up for a
three-month course, half expecting to hate it. Instead I had the best three
months I’d had with a computer, mostly thanks to a teacher who was genuinely
brilliant. That’s the stretch where I stopped fighting and decided, properly, to
become a programmer.&lt;/p&gt;
&lt;h2 id=&quot;finding-wordpress&quot;&gt;Finding WordPress&lt;/h2&gt;
&lt;p&gt;I joined an IT company the moment the course ended. I was still a beginner, and a
serious project landing on my desk was enough to make my hands shake — but that
first company threw me straight at real, big work, which is exactly what I needed.&lt;/p&gt;
&lt;p&gt;On the side I kept experimenting, and one of those experiments was a platform
called WordPress. I got to lead a project on it, and I was hooked. I tried
building a plugin, published it more or less to see what would happen, and a
throwaway attempt pulled in far more people and far kinder words than I ever
expected. That reaction lit something up. I started losing sleep to WordPress in
the best way, building more plugins — the trail of them is on our &lt;a href=&quot;/software&quot;&gt;software
page&lt;/a&gt; if you’re curious.&lt;/p&gt;
&lt;p&gt;The company itself didn’t have much WordPress work, so my day job ran on Laravel
and Drupal instead. But WordPress never left my head, and I never let it go. It
wasn’t all momentum, either. There was a low stretch I won’t sugarcoat — the kind
where being let go plays on a loop in your head at 3 a.m. I’ve given that its own
post rather than crowd it in here.&lt;/p&gt;
&lt;h2 id=&quot;the-turning-point-that-actually-was&quot;&gt;The turning point that actually was&lt;/h2&gt;
&lt;p&gt;By the third year I’d landed somewhere genuinely good: flexible hours, a team I
liked, work I looked forward to. I could have happily stayed. But WordPress was
still my thing, so I kept an eye on the companies working in it — and one day a
WordPress developer role opened up at WPMU DEV (Incsub), a name I respected a
lot.&lt;/p&gt;
&lt;p&gt;I applied without taking it seriously, fully assuming nothing would come of it.
Then a trial task turned into an offer letter, and the offer turned into the
hardest call of my career: stay with the job I loved, or gamble on a harder,
bigger one. The company was abroad, fully remote — and remote work was barely a
thing where I lived back then. No office to walk into, no colleague to meet in
person, everything through a screen. I took it anyway, on the plain belief that
you don’t get anything worth having without risking something for it.&lt;/p&gt;
&lt;h2 id=&quot;the-end-the-real-one&quot;&gt;The end (the real one)&lt;/h2&gt;
&lt;p&gt;It worked. And it turned out to be about far more than getting to build WordPress
for a living. It was the shape of the work — being able to do it from anywhere,
around the people and the life that matter, without asking permission to step
away when something came up. It put me in rooms (virtual ones) full of sharp
people from all over the world. I’ve learned and un-learned more than I can count,
often in lead roles, usually with the newest tools in hand. That’s the part I’m
proudest of.&lt;/p&gt;
&lt;p&gt;So that’s the story. Programming went from the enemy I dreaded to the thing my
days are built around. These days I’m circling back to the projects I abandoned
at ninety percent — this write-up being one of them — and picking them up one at
a time.&lt;/p&gt;
&lt;p&gt;If you’ve had a turn like this of your own, the kind where the thing you resisted
becomes the thing you can’t imagine living without, I’d genuinely like to hear it.&lt;/p&gt;
&lt;p&gt;Till the next one — take care.&lt;/p&gt;
&lt;p&gt;— JJ&lt;/p&gt;</content:encoded><category>Writing</category><category>personal</category><category>career</category><category>wordpress</category><author>support@foxelabs.com (Joel James)</author></item><item><title>Partial string search in an array using PHP</title><link>https://foxelabs.com/blog/partial-string-search-in-an-array-using-php/</link><guid isPermaLink="true">https://foxelabs.com/blog/partial-string-search-in-an-array-using-php/</guid><description>There&apos;s no built-in PHP function to find every array element that contains a substring. Here&apos;s the small helper I wrote for it — and the two one-liners that replace it on modern PHP.</description><pubDate>Mon, 27 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I once needed something PHP doesn’t ship: a function that takes an array of
strings and returns every element containing a given substring. &lt;code&gt;in_array()&lt;/code&gt; only
matches whole values, &lt;code&gt;array_search()&lt;/code&gt; does the same and returns a key, and
&lt;code&gt;array_filter()&lt;/code&gt; gets you close but you still have to supply the matching logic.
So for a WordPress project I wrote a tiny helper — and it turns out there are a
couple of one-liners worth knowing too.&lt;/p&gt;
&lt;h2 id=&quot;the-helper-a-loop-around-strpos&quot;&gt;The helper: a loop around strpos&lt;/h2&gt;
&lt;p&gt;The most readable version is just a loop that keeps every string the keyword
appears inside. &lt;code&gt;strpos()&lt;/code&gt; returns the position of the first match, or &lt;code&gt;false&lt;/code&gt; if
there’s none — so the &lt;code&gt;!== false&lt;/code&gt; check is doing the work:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; * Partial-search an array of strings for a keyword.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; *&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt; string[]&lt;/span&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; $array   Array of strings to search.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;@param&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;   $keyword Substring to look for.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; *&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; * &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;@return&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt; string[]&lt;/span&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; Every element that contains the keyword.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; array_partial_search&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;( &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;array&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;array&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;keyword&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ): &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;array&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;    $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;found&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;    foreach&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ( $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;array&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; as $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ( &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;strpos&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;keyword&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ) !== &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;            $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;found&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;[] = $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;found&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;fruits&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = [ &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;apple&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;grapes&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;orange&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;pineapple&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;found&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;array_partial_search&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;fruits&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;ap&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// [ &amp;#39;apple&amp;#39;, &amp;#39;grapes&amp;#39;, &amp;#39;pineapple&amp;#39; ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The one thing that catches people out here is the &lt;code&gt;!== false&lt;/code&gt; — with &lt;code&gt;strpos&lt;/code&gt;
you &lt;em&gt;must&lt;/em&gt; use the strict operator. A match at position &lt;code&gt;0&lt;/code&gt; (the keyword at the
very start of the string, like &lt;code&gt;&amp;#39;ap&amp;#39;&lt;/code&gt; in &lt;code&gt;&amp;#39;apple&amp;#39;&lt;/code&gt;) is a falsy &lt;code&gt;0&lt;/code&gt;, so a loose
&lt;code&gt;!= false&lt;/code&gt; would silently drop it.&lt;/p&gt;
&lt;h2 id=&quot;the-modern-one-liner-str_contains&quot;&gt;The modern one-liner: str_contains&lt;/h2&gt;
&lt;p&gt;PHP 8.0 added &lt;code&gt;str_contains()&lt;/code&gt;, which returns a plain boolean and reads exactly
like what you mean. Drop it into &lt;code&gt;array_filter()&lt;/code&gt; and the helper collapses to a
single line — no strict-comparison footgun, no loop:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;found&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;array_filter&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;    $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;fruits&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;    fn&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; str_contains&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;ap&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One difference to keep in mind: &lt;code&gt;array_filter()&lt;/code&gt; &lt;strong&gt;preserves the original keys&lt;/strong&gt;,
so you get &lt;code&gt;[0 =&amp;gt; &amp;#39;apple&amp;#39;, 1 =&amp;gt; &amp;#39;grapes&amp;#39;, 3 =&amp;gt; &amp;#39;pineapple&amp;#39;]&lt;/code&gt; — note the missing
&lt;code&gt;2&lt;/code&gt;. If you want a clean, re-indexed list, wrap it in &lt;code&gt;array_values()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-regex-one-liner-preg_grep&quot;&gt;The regex one-liner: preg_grep&lt;/h2&gt;
&lt;p&gt;If you’re happy to reach for a regular expression, &lt;code&gt;preg_grep()&lt;/code&gt; does the whole
thing in one call — it returns every array element that matches a pattern:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;found&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;preg_grep&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;/ap/&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;fruits&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s the shortest option, but there’s a catch: your keyword is now a &lt;em&gt;regex&lt;/em&gt;.
If it ever contains characters like &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;(&lt;/code&gt; or &lt;code&gt;/&lt;/code&gt;, they’ll be interpreted
as pattern syntax rather than literal text. When the keyword is user input, escape
it with &lt;code&gt;preg_quote()&lt;/code&gt; first:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;keyword&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;(ap&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;pattern&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; .&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; preg_quote&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;keyword&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;/&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ) &lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;/&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;found&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;preg_grep&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;pattern&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;fruits&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Like &lt;code&gt;array_filter()&lt;/code&gt;, &lt;code&gt;preg_grep()&lt;/code&gt; preserves keys — &lt;code&gt;array_values()&lt;/code&gt; again if
you want them reset.&lt;/p&gt;
&lt;h2 id=&quot;case-insensitive-search&quot;&gt;Case-insensitive search&lt;/h2&gt;
&lt;p&gt;All three approaches match case by default, so &lt;code&gt;&amp;#39;AP&amp;#39;&lt;/code&gt; wouldn’t find &lt;code&gt;&amp;#39;apple&amp;#39;&lt;/code&gt;.
Making them case-insensitive is a small tweak per approach:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// strpos → stripos&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;stripos&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;keyword&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ) !== &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;false&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// str_contains → lower-case both sides&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str_contains&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; strtolower&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; ),&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; strtolower&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;keyword&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; )&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// preg_grep → the /i flag&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;preg_grep&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt; &amp;#39;/ap/i&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;fruits&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; );&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;which-one-to-reach-for&quot;&gt;Which one to reach for&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;On &lt;strong&gt;PHP 8+&lt;/strong&gt;, &lt;code&gt;array_filter()&lt;/code&gt; with &lt;code&gt;str_contains()&lt;/code&gt; is the clearest — it says
what it does and sidesteps the &lt;code&gt;!== false&lt;/code&gt; trap entirely.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preg_grep()&lt;/code&gt; is the most compact, and the natural choice when you’re matching a
&lt;em&gt;pattern&lt;/em&gt; rather than a fixed substring — just remember &lt;code&gt;preg_quote()&lt;/code&gt; for
untrusted input.&lt;/li&gt;
&lt;li&gt;The explicit &lt;strong&gt;helper&lt;/strong&gt; still earns its place when you’re on older PHP, or when
you want an obvious, named function that the next person can read at a glance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of them is doing anything clever — that’s rather the point. It’s a gap small
enough that PHP never filled it, and small enough that any of these one-liners
closes it in a line.&lt;/p&gt;
&lt;p&gt;— JJ&lt;/p&gt;</content:encoded><category>Software</category><category>php</category><category>arrays</category><category>strings</category><author>support@foxelabs.com (Joel James)</author></item><item><title>My first WordPress core contribution is live</title><link>https://foxelabs.com/blog/first-wordpress-core-contribution-live/</link><guid isPermaLink="true">https://foxelabs.com/blog/first-wordpress-core-contribution-live/</guid><description>WordPress 4.7 shipped today with my name in the contributor list — a small patch for a broken importer link that now runs on millions of sites. Here&apos;s the bug, the fix, and why giving back is worth it.</description><pubDate>Wed, 07 Dec 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://wordpress.org/news/2016/12/vaughan/&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;WordPress 4.7&lt;/a&gt; — named “Vaughan” in
honour of the jazz vocalist Sarah “Sassy” Vaughan — shipped today. And for the
first time, my name is in the list of core contributors. It’s a small patch, but
seeing your name in a release that runs on millions of sites is a hard feeling to
put into words.&lt;/p&gt;
&lt;p&gt;The 4.7 release was led by Helen Hou-Sandí with release deputies Jeff Paul and
Aaron Jorbin. There were &lt;strong&gt;482 contributors&lt;/strong&gt; this cycle — &lt;strong&gt;205 of them for the
first time&lt;/strong&gt;, including me.&lt;/p&gt;
&lt;h2 id=&quot;giving-back&quot;&gt;Giving back&lt;/h2&gt;
&lt;p&gt;Almost everything I build on belongs to open source. WordPress, Drupal, Laravel —
I lean on them every day, for personal projects and professional work alike. So I
try to give back to the community whenever I can. Over the years that’s mostly
meant building and maintaining a handful of
&lt;a href=&quot;https://profiles.wordpress.org/joelcj91/#content-plugins&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;free WordPress plugins&lt;/a&gt;;
I’d like to do more as I learn.&lt;/p&gt;
&lt;h2 id=&quot;the-contribution&quot;&gt;The contribution&lt;/h2&gt;
&lt;p&gt;One of the best ways to give back is to contribute to core itself, because a single
fix there can help millions of websites at once. That thought alone was enough
motivation to turn a private workaround into a real patch.&lt;/p&gt;
&lt;p&gt;I hit the bug while importing content into a multisite install. After installing
the WordPress Importer plugin, the AJAX response came back with a &lt;strong&gt;broken link&lt;/strong&gt;.
I fixed it for my own site first by editing a single core file — the kind of quick
patch that’s easy to keep to yourself and move on.&lt;/p&gt;
&lt;p&gt;Instead, I opened a ticket on
&lt;a href=&quot;https://core.trac.wordpress.org/ticket/37943&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;WordPress Core Trac (#37943)&lt;/a&gt; with a
patch. After a few rounds of feedback from other committers, a refined version
&lt;a href=&quot;https://core.trac.wordpress.org/changeset/38704&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;landed in WordPress 4.7&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That’s the whole loop, and it’s smaller than people expect: notice something
broken, fix it locally, then take the extra step of writing it up for everyone else.&lt;/p&gt;
&lt;h2 id=&quot;if-you-want-to-start&quot;&gt;If you want to start&lt;/h2&gt;
&lt;p&gt;You don’t need to be a core committer to help. A good first step is
&lt;a href=&quot;https://make.wordpress.org/&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Get involved&lt;/a&gt; — the Make WordPress hub, where every
team (core, docs, polyglots, support, design) lists ways to jump in. Plenty of
contributions never touch PHP at all.&lt;/p&gt;
&lt;p&gt;The contribution itself was tiny. The lesson wasn’t: the projects we all depend on
are only as healthy as the people willing to file the ticket instead of quietly
patching their own copy. Thanks to WordPress and the community around it for making
that first step so welcoming.&lt;/p&gt;
&lt;p&gt;— JJ&lt;/p&gt;</content:encoded><category>Software</category><category>wordpress</category><category>open-source</category><category>contributing</category><author>support@foxelabs.com (Joel James)</author></item><item><title>What the 404 to 301 tracking incident taught me about plugin security</title><link>https://foxelabs.com/blog/404-to-301-tracking-incident-lessons/</link><guid isPermaLink="true">https://foxelabs.com/blog/404-to-301-tracking-incident-lessons/</guid><description>In 2016 my most popular plugin shipped a usage-tracking feature that a partner turned into cloaked ads for search-engine crawlers. Here&apos;s a level-headed account of what went wrong and the lessons that still guide how I build plugins.</description><pubDate>Wed, 17 Aug 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Back in 2016, &lt;a href=&quot;/software/plugins/404-to-301&quot;&gt;404 to 301&lt;/a&gt; — my most-used plugin,
then on 100,000+ sites — was flagged publicly for shipping a feature that
misbehaved. It was one of the hardest weeks of my open-source life, and also one of
the most instructive. I’ve kept the original apology on my old blog, but what’s
actually worth carrying forward isn’t the drama — it’s the engineering lessons.
So here’s the level-headed version, years later.&lt;/p&gt;
&lt;h2 id=&quot;what-actually-happened&quot;&gt;What actually happened&lt;/h2&gt;
&lt;p&gt;The plugin had an optional &lt;strong&gt;usage-tracking feature&lt;/strong&gt;. It came out of a partnership:
another developer wanted anonymous usage statistics (visitor IP and user-agent), and
in exchange the plugin would display a small third-party “credit” text link. On
install, users were asked to opt in.&lt;/p&gt;
&lt;p&gt;Two things made that a bad design, and I own both:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The credit links were remote content.&lt;/strong&gt; The text to display was fetched from
the partner’s server, so what actually got served wasn’t fixed in my plugin’s
code — it could change at any time, without a new plugin release for anyone to
review.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;That’s exactly what happened.&lt;/strong&gt; The partner later changed their server to
return &lt;strong&gt;cloaked ad links&lt;/strong&gt; — injected near the top of the page, but only when a
search-engine crawler requested it, so a normal visitor never saw them. That’s
cloaking, and it can damage a site’s search rankings. A security vendor found it,
published a report, and a wave of negative reviews followed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I removed the tracking feature completely and shipped an update within hours of
learning about it. The WordPress.org plugins team reviewed the situation, and the
takeaway they reinforced has stuck with me: &lt;strong&gt;third-party ads must be opt-in and
must never track users.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-three-mistakes&quot;&gt;The three mistakes&lt;/h2&gt;
&lt;p&gt;Stripped of everything else, I made three concrete engineering errors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;I shared commit access without keeping control.&lt;/strong&gt; The partner’s code went out
under my account, and I treated their contributions with less scrutiny than my own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I misread the guidelines on remote content.&lt;/strong&gt; I thought a small credit link was
harmless. Loading anything from a third-party server into someone else’s site is
not harmless — it’s a decision with security and privacy weight.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I didn’t keep verifying the remote response.&lt;/strong&gt; I tested it once, saw the small
credit text, and trusted it. A response you fetch from someone else’s server is not
a thing you verify once; it’s a thing that can change the day after you look away.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-this-is-a-supply-chain-problem-not-a-one-off&quot;&gt;Why this is a supply-chain problem, not a one-off&lt;/h2&gt;
&lt;p&gt;The reason this is worth retelling is that it’s the same shape as the supply-chain
attacks that keep hitting the ecosystem: &lt;strong&gt;code or content you don’t fully control,
running inside software people trust.&lt;/strong&gt; It doesn’t matter whether it arrives as an
npm dependency, a “partner” SDK, or a friendly credit link — the moment a remote
party can change what your software does after review, you’ve handed them your
users’ trust.&lt;/p&gt;
&lt;p&gt;The uncomfortable part is that the popularity of the plugin made it &lt;em&gt;more&lt;/em&gt;
attractive as a vector, not less. Reach is exactly what makes a plugin a target.&lt;/p&gt;
&lt;h2 id=&quot;what-i-do-differently-now&quot;&gt;What I do differently now&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No shared commit access without review.&lt;/strong&gt; Contributions get read and understood
before they ship, no matter who they’re from.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote content is an attack surface.&lt;/strong&gt; If a feature loads anything from an
external server, it gets treated as untrusted — verified, constrained, and
monitored, not tested once and forgotten.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opt-in, and no tracking.&lt;/strong&gt; Anything touching a user’s visitors is opt-in by
default and collects nothing that identifies them. When in doubt, it doesn’t ship.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fix first, explain fully.&lt;/strong&gt; Remove the offending code immediately, then be
transparent about what happened. People are remarkably forgiving of an honest,
fast response.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;404 to 301 has carried no third-party tracking or remote ad code since that 2016
update, and it never will again. The incident was avoidable, and owning that is the
point — but the reason to write it down is so the next developer weighing a
too-good partnership offer recognises the shape of it before signing on.&lt;/p&gt;
&lt;p&gt;— JJ&lt;/p&gt;</content:encoded><category>Software</category><category>wordpress</category><category>security</category><category>plugins</category><author>support@foxelabs.com (Joel James)</author></item><item><title>Countdown redirect using JavaScript</title><link>https://foxelabs.com/blog/countdown-redirect-using-javascript/</link><guid isPermaLink="true">https://foxelabs.com/blog/countdown-redirect-using-javascript/</guid><description>How to send a visitor to another page after a visible countdown — a clean, accessible JavaScript version, plus a no-JavaScript fallback and the setTimeout pitfall to avoid.</description><pubDate>Sat, 05 Mar 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Sometimes you want a redirect that pauses for a few seconds — showing a “redirecting
in 3… 2… 1…” message — before sending the visitor on. It’s the pattern you see on
“thank you” pages, interstitials, and link shorteners: give the reader a moment to
notice a message, then move them along.&lt;/p&gt;
&lt;p&gt;The idea is simple: show the remaining seconds, count down once per second, and
navigate away when the timer hits zero. Here’s a clean, modern version.&lt;/p&gt;
&lt;h2 id=&quot;the-markup&quot;&gt;The markup&lt;/h2&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;HTML&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;  Redirecting in &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;span&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; id=&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;quot;countdown&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; aria-live=&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;quot;polite&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;&amp;gt;10&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;span&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;&amp;gt; seconds.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;  &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;a&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; id=&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;quot;go-now&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; href=&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;quot;https://example.com&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;&amp;gt;Go now&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;a&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;&amp;gt;.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Two small things earn their keep here. The &lt;code&gt;aria-live=&amp;quot;polite&amp;quot;&lt;/code&gt; attribute means
screen readers announce each new number as it changes, and the &lt;strong&gt;“Go now”&lt;/strong&gt; link is
a plain anchor — so the redirect still works if JavaScript never runs, and impatient
visitors can skip the wait.&lt;/p&gt;
&lt;h2 id=&quot;the-javascript&quot;&gt;The JavaScript&lt;/h2&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;JS&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; target&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;https://example.com&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; el&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;document&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;getElementById&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;countdown&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;let&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; seconds&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;Number&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;el&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;textContent&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;); &lt;/span&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// start from whatever the HTML shows&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt; timer&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setInterval&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(() &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;  seconds&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; -= &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;seconds&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; &amp;lt;= &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    clearInterval&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;timer&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;    window&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;location&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;assign&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;target&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;  } &lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;else&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;    el&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;textContent&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;seconds&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;}, &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;1000&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s the whole thing. &lt;code&gt;setInterval&lt;/code&gt; fires the callback once a second; we drop the
counter, update the visible number, and when it reaches zero we clear the interval
and navigate. Reading the starting value from the HTML means the number you write in
the markup is the single source of truth — change &lt;code&gt;10&lt;/code&gt; in one place and everything
follows.&lt;/p&gt;
&lt;h2 id=&quot;avoid-the-classic-settimeout-mistake&quot;&gt;Avoid the classic setTimeout mistake&lt;/h2&gt;
&lt;p&gt;Older examples (including the one this post replaces) used a string argument:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;JS&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// Don&amp;#39;t do this — the string is evaluated like eval().&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setTimeout&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;quot;countdown()&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;1000&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Passing a &lt;strong&gt;string&lt;/strong&gt; to &lt;code&gt;setTimeout&lt;/code&gt; or &lt;code&gt;setInterval&lt;/code&gt; makes the browser evaluate it
as code, which is slower, breaks under a Content Security Policy, and hides bugs from
your tooling. Always pass a &lt;strong&gt;function reference&lt;/strong&gt; instead, as in the example above.&lt;/p&gt;
&lt;h2 id=&quot;assign-vs-replace&quot;&gt;assign vs replace&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;window.location.assign(target)&lt;/code&gt; keeps the countdown page in the browser history, so
pressing Back returns to it. For an interstitial you’d rather users &lt;em&gt;not&lt;/em&gt; land back
on, use &lt;code&gt;replace()&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;JS&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;window&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;location&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;target&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;); &lt;/span&gt;&lt;span style=&quot;color:#8A8D94&quot;&gt;// Back skips the countdown page&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;a-no-javascript-fallback&quot;&gt;A no-JavaScript fallback&lt;/h2&gt;
&lt;p&gt;If you don’t need a live counter at all, HTML can do a timed redirect on its own with
a meta refresh — handy as a fallback, or for pages where you can’t run scripts:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;HTML&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;html&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;meta&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; http-equiv=&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;quot;refresh&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; content=&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;quot;10; url=https://example.com&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It can’t show the seconds ticking, but it fires even with JavaScript disabled. Pair
it with the script above and you’ve covered both cases.&lt;/p&gt;
&lt;p&gt;That’s all it takes — a visible number, a one-second tick, and a navigation call at
zero. Keep coding.&lt;/p&gt;
&lt;p&gt;— JJ&lt;/p&gt;</content:encoded><category>Software</category><category>javascript</category><category>redirects</category><category>web</category><author>support@foxelabs.com (Joel James)</author></item><item><title>Laravel bail: stop validation on the first failure</title><link>https://foxelabs.com/blog/laravel-bail-stop-on-first-validation-failure/</link><guid isPermaLink="true">https://foxelabs.com/blog/laravel-bail-stop-on-first-validation-failure/</guid><description>A single validation rule kept firing every check on a field and throwing a database error along the way. The fix was a one-word rule called bail — one I ended up contributing to Laravel itself.</description><pubDate>Mon, 01 Feb 2016 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Laravel ships with a deep set of validation rules, and most of the time they’re
all you need. But the docs don’t surface everything — now and then the feature
you want is sitting right there in the source, just undocumented. This is the
story of one of those, and how it ended up in the framework.&lt;/p&gt;
&lt;h2 id=&quot;the-bug-my-tests-didnt-catch&quot;&gt;The bug my tests didn’t catch&lt;/h2&gt;
&lt;p&gt;I had a &lt;code&gt;user_id&lt;/code&gt; field with what looked like sensible rules: it had to be
present, an integer, and unique in the &lt;code&gt;users&lt;/code&gt; table.&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;validate&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;($&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    &amp;#39;user_id&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&amp;gt; &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;required|integer|unique:users,user_id&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;]);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It worked in my own testing. Then QA typed letters into the field and got a
&lt;strong&gt;database error&lt;/strong&gt; instead of a clean validation message.&lt;/p&gt;
&lt;p&gt;Here’s why. By default Laravel runs &lt;em&gt;every&lt;/em&gt; rule on an attribute and collects all
the failures. So with a non-integer value, &lt;code&gt;required&lt;/code&gt; passed, &lt;code&gt;integer&lt;/code&gt; failed —
and &lt;code&gt;unique&lt;/code&gt; still ran anyway, firing a &lt;code&gt;SELECT&lt;/code&gt; against a column the schema only
allows integers in. The query blew up before validation could return a tidy
message. My first instinct was a custom rule to work around it.&lt;/p&gt;
&lt;h2 id=&quot;the-one-word-fix&quot;&gt;The one-word fix&lt;/h2&gt;
&lt;p&gt;What I actually wanted was for validation to &lt;strong&gt;stop at the first failure&lt;/strong&gt; on that
attribute, so a broken value never reaches the &lt;code&gt;unique&lt;/code&gt; query. At the time the
clean way to do that wasn’t in the documentation — so I built it and opened a &lt;a href=&quot;https://github.com/laravel/framework/pull/11986&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;pull
request against Laravel&lt;/a&gt;. It was
merged, and shipped in &lt;strong&gt;Laravel 5.2&lt;/strong&gt; as the &lt;code&gt;bail&lt;/code&gt; rule:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;this&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;validate&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;($&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    &amp;#39;user_id&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&amp;gt; &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;bail|required|integer|unique:users,user_id&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;]);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With &lt;code&gt;bail&lt;/code&gt; first, the moment &lt;code&gt;required&lt;/code&gt; or &lt;code&gt;integer&lt;/code&gt; fails, Laravel stops and
never runs &lt;code&gt;unique&lt;/code&gt; — no stray query, no database error. Two things to remember:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Order matters.&lt;/strong&gt; Rules run top to bottom, so put the cheap, structural checks
(&lt;code&gt;required&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt;) before the ones that hit the database (&lt;code&gt;unique&lt;/code&gt;,
&lt;code&gt;exists&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;bail&lt;/code&gt; is per-attribute.&lt;/strong&gt; It only short-circuits the field it’s on; other
fields still validate normally.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;still-true-today&quot;&gt;Still true today&lt;/h2&gt;
&lt;p&gt;Ten years on, &lt;code&gt;bail&lt;/code&gt; is still the idiomatic answer, and it reads even better with
the array syntax most Laravel code uses now:&lt;/p&gt;
&lt;div class=&quot;code&quot;&gt;&lt;div class=&quot;code__head&quot;&gt;&lt;span&gt;PHP&lt;/span&gt;&lt;span class=&quot;code__dots&quot; aria-hidden=&quot;true&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code foxe-code&quot; style=&quot;background-color:#1E1F21;color:#DEE0E4;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;php&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C88FE8&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; Illuminate\Validation\&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;Rule&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#FFB84D&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;validate&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;([&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;    &amp;#39;user_id&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt; =&amp;gt; [&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;bail&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;required&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;integer&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;Rule&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;::&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;unique&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;users&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A5CD6B&quot;&gt;&amp;#39;user_id&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;)],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#DEE0E4&quot;&gt;]);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s the whole trick — one small rule that turns a leaky “run everything” pass
into a clean “stop when it breaks” one. It’s also a nice reminder that the
frameworks we lean on every day are just code, and open enough to fix when they
come up short.&lt;/p&gt;
&lt;p&gt;Love Laravel. Keep coding.&lt;/p&gt;
&lt;p&gt;— JJ&lt;/p&gt;</content:encoded><category>Software</category><category>laravel</category><category>php</category><category>validation</category><author>support@foxelabs.com (Joel James)</author></item></channel></rss>