{
    "name": "R2 Onboard",
    "slug": "r2-onboard",
    "version": "1.0.1",
    "requires": "6.0",
    "tested": "6.7",
    "requires_php": "8.0",
    "author": "<a href='https://rise2.studio'>RISE2 Studio</a>",
    "author_profile": "https://rise2.studio",
    "homepage": "https://rise2.studio",
    "last_updated": "2026-10-02",
    "download_url": "https://plugins.rise2.studio/r2-onboard/r2-onboard-1.0.1.zip",
    "sections": {
        "description": "<p>Front-end user accounts: sign in, register, reset a password and edit a profile on your own pages, styled by your own theme. Five shortcodes, five ordinary pages, and no <code>wp-login.php</code> in sight for your visitors.</p><h4>What you get</h4><ul><li><code>[r2ob_login]</code> &mdash; sign-in form</li><li><code>[r2ob_register]</code> &mdash; registration, with optional e-mail activation</li><li><code>[r2ob_forgot_password]</code> &mdash; request a reset link</li><li><code>[r2ob_reset_password]</code> &mdash; choose a new password</li><li><code>[r2ob_profile]</code> &mdash; name, e-mail and password for the signed-in user</li></ul><p>The pages are created for you on activation. The markup is plain and class-named, and the stylesheet does layout and focus rings only &mdash; typography and colour come from the theme.</p><h4>Safe by default</h4><ul><li><strong>Rate limited.</strong> Sign-in, registration, password reset and activation each allow 8 attempts per 15 minutes per IP, cleared on success. The address is never stored &mdash; the key is an HMAC under your site&rsquo;s own salt.</li><li><strong>Your other tools still see failures.</strong> A failed sign-in fires <code>wp_login_failed</code>, so Limit Login Attempts, a firewall rule or R2 Security Hardening&rsquo;s Login Guard keep working.</li><li><strong>No account enumeration.</strong> Registering with an address that already exists returns exactly what a successful registration returns, and the real owner is e-mailed that someone tried.</li><li><strong>Fixed messages.</strong> Responses travel as codes and are resolved server-side, so nobody can put words of their own on your login page with a crafted link.</li><li><strong>Activation keys are stored hashed</strong> and compared in constant time.</li><li><strong>Redirect settings are validated against your site</strong> on save, so they cannot become an open redirect, and the role selector never offers administrator.</li></ul><h4>wp-login.php stays yours</h4><p>Redirecting visitors away from <code>wp-login.php</code> is optional and <strong>off by default</strong>. Even when on, administrators are never redirected and it refuses to act if the login page is missing, so lockout is impossible; <code>define( 'R2OB_DISABLE_LOGIN_BLOCK', true )</code> switches it off from outside the admin. Worth knowing before enabling it: moving sign-in traffic off <code>wp-login.php</code> also moves it out of view of anything watching that URL.</p><h4>No dependencies</h4><p>No ACF, no Composer, no bundled SMTP. Configuration is the plain WordPress Settings API, and mail goes through <code>wp_mail()</code> &mdash; pair it with FluentSMTP or WP Mail SMTP, which give you connection tests, delivery logs and retries that a bundled transport never does.</p>",
        "changelog": "<h4>1.0.1</h4><ul><li>The forms&rsquo; CSS and JavaScript tags carry <code>data-no-minify</code>: WP Rocket names its minified copy after the file path, not the version, and could keep serving the previous version&rsquo;s file after an update.</li></ul><h4>1.0.0</h4><ul><li><strong>First release.</strong> Front-end sign-in, registration with optional e-mail activation, password reset and profile editing, as five shortcodes with generated pages and a Settings API screen.</li><li><strong>Per-IP throttling</strong> on every credential endpoint (8 attempts / 15 minutes, cleared on success), with the address stored only as an HMAC under the site salt, and <code>wp_login_failed</code> fired so third-party guards still see failures.</li><li><strong>Extracted from the RISE2 starter theme</strong>, where it shipped disabled behind a commented-out require. This is not a copy &mdash; the extraction fixed the following, and none of it should be reintroduced.</li><li><strong>Security &mdash; authentication bypass.</strong> The theme version called <code>wp_signon()</code> and only then checked whether the account was still awaiting activation, meaning to log it back out. That check was unreachable: <code>wp_signon()</code> fires <code>wp_login</code>, the post-login redirect ran <code>wp_safe_redirect(); exit;</code> inside it, and execution never returned &mdash; so an unactivated account was signed in and stayed signed in. The check now runs on <code>authenticate</code> and refuses the credentials outright.</li><li><strong>Security &mdash; unauthenticated page creation.</strong> Five <code>wp_insert_post()</code> calls plus <code>flush_rewrite_rules()</code> were hooked to <code>init</code>, so they ran for whichever anonymous visitor arrived first, with a race that could double-create. Page creation now happens only on activation, adopts an existing page at the same slug, and flushes nothing.</li><li><strong>Security &mdash; no rate limiting on an endpoint that had displaced a protected one.</strong> The AJAX login called <code>wp_signon()</code> with no counter while the plugin redirected <code>wp-login.php</code> away, so every guard scoped to that URL was bypassed and the real credential surface moved to <code>admin-ajax.php</code>. Fixed, and that redirect now defaults to off, never applies to administrators, and has a wp-config escape hatch.</li><li><strong>Security &mdash; account enumeration</strong> on registration, <strong>reflected message text</strong> in the non-JS fallback (arbitrary words on the client&rsquo;s own login page), a <strong>missing <code>wp_unslash()</code></strong> throughout (passwords containing quotes were corrupted), a <strong>PHP 8.2 dynamic property</strong> plus <code>$_POST</code>-driven method dispatch, and <strong>sessions left alive across a password change</strong> &mdash; all fixed.</li><li><strong>Removed the ACF PRO dependency</strong> (531 lines of field groups replaced by the Settings API) and <strong>the built-in SMTP transport</strong>, which had shipped RISE2&rsquo;s own mail server as an enabled-by-default value on every install.</li><li><strong>Form CSS rescaled to be theme-relative</strong> (em/px, never rem) &mdash; the theme version assumed the starter&rsquo;s 100px root font-size and rendered absurdly anywhere else.</li></ul>"
    }
}
