    @prefix sioc: <http://rdfs.org/sioc/ns#> .
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix content: <http://purl.org/rss/1.0/modules/content/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

<https://lhero.org/?post_type=lh-portfolio&#038;p=145741>
  a sioc:Post ;
  dc:title "Changelog" ;
  dcterms:identifier 145741 ;
  dc:modified "2026-08-22T21:58:50Z"^^xsd:dateTime ;
  dc:created "2026-08-22T21:58:50Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-membership/changelog/> ;
  sioc:has_creator <https://lhero.org/author/1015845/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<p>All notable changes to LH Membership are documented here. This entry is canonical and append-only.</p>
<p><strong>2.71 2026-08-18</strong><br />
Added a sixth lh-competitions-family integration for restrict, this time into lh-buddypress-check-ins: new method <code>add_restricted_to_check_ins_event_statuses()</code> hooks the new <code>lh_bp_check_ins_event_post_status</code> filter (lh-buddypress-check-ins 3.03) so restricted events are included in the check-ins plugin&#8217;s next-event and listing shortcodes for users who can view them (<code>current_user_can_view()</code> — same gated pattern as round-nav, event-listing, and game-edit-event). This closes the root cause of <code>lh_bp_check_ins_display_next</code> surfacing the wrong event on Princes Park Touch: the shortcode&#8217;s hardcoded <code>post_status =&gt; 'publish'</code> was skipping every restricted-status Wednesday-night round in favour of whichever publish-status round happened to be furthest in the future.</p>
<p><strong>2.69 2026-08-17</strong><br />
Added a filterable capability-based exemption from the member-onboarding BuddyPress gate: <code>LH_Membership_private_buddypress_class::non_member_requires_access()</code> now returns false immediately for any user with the <code>promote_users</code> capability (default, filterable via <code>lh_membership_return_gate_exempt_capability</code>), regardless of their own membership/approval status. New method <code>return_gate_exempt_capability()</code> added alongside the class&#8217;s other filterable whitelists.</p>
<p><strong>2.68 2026-08-03</strong><br />
Added a sixth restrict integration, this time into lh-buddypress-log-entry rather than lh-competitions: hooks the new lh_bp_log_entry_return_event_post_statuses filter (lh-buddypress-log-entry 1.03) so restricted events are included in the log entry form&#8217;s event lookup, the open/closed shortcodes, and the past-events listing. Per Peter&#8217;s explicit instruction, this integration is unconditional &#8212; not gated by current_user_can_view() &#8212; same rationale as the standings integration: it&#8217;s just adding restrict to the set of statuses queried, not deciding what a particular viewer gets to see.</p>
<p><strong>2.67 2026-07-18</strong><br />
Added a fifth lh-competitions integration for restrict: hooks the new lh_comps_latest_scoresheet_event_query_args filter, so the &#8220;Latest {Night} Scoresheet&#8221; dynamic menu item can resolve to a restricted event for users who can view them (current_user_can_view() &#8212; same gated pattern as round-nav, event-listing, and game-edit-event; unlike standings, which is deliberately unconditional). Default query status (&#8216;publish&#8217;, a string rather than an array here) is normalised to an array before appending restrict.</p>
<p><strong>2.66 2026-07-18</strong><br />
Added a fourth lh-competitions integration for restrict: hooks the new lh_comps_standings_event_query_args filter (lh-competitions 1.80, applied without a version bump per Peter&#8217;s 2026-07-21 instruction &#8212; see that plugin&#8217;s own changelog) so events with status restrict are included in standings table calculations alongside publish. Unlike the other three restrict integrations (round-nav, event-listing, game-edit-event), this one is deliberately unconditional &#8212; not gated by current_user_can_view() &#8212; since the standings table is a computed aggregate (wins/points) rather than per-viewer visible content; gating which events feed into the calculation by the current viewer&#8217;s access would make the resulting numbers inconsistent depending on who&#8217;s looking. Flagged this deviation from the established gated pattern to Peter before implementing; proceeded on this assumption pending confirmation.</p>
<p><strong>2.65 2026-07-18</strong><br />
Added a third lh-competitions integration for restrict, matching the existing round-nav and event-listing ones: hooks the new lh_comps_game_edit_event_post_status filter (lh-competitions v1.79+) so restricted events are selectable in the Event dropdown on the game edit screen&#8217;s Game Data meta box, for users who can view restricted content (self::current_user_can_view() &#8212; same check as the other two integrations). Not extended to confidential/logged_in, per the standing decision to keep this integration restrict-only.</p>
<p><strong>2.64 2026-07-18</strong><br />
Reworded the BuddyPress access-denied flash message. Since the person only sees it after being redirected to their own profile, &#8220;This area is only visible to current members&#8221; reads as if it&#8217;s describing the page they just landed on. Changed to &#8220;You tried to access an area that is only visible to current members&#8221; to make clear it&#8217;s referring to where they came from.</p>
<p><strong>2.63 2026-07-18</strong><br />
Fixed 2.62&#8217;s BuddyPress no-access page: the title correctly changed to &#8220;No Access&#8221; but the actual screen content (groups directory, members directory, etc.) kept rendering underneath it. Root cause: BuddyPress doesn&#8217;t render through WordPress&#8217;s normal template/404 flow at all &#8212; it has its own screen-rendering system that runs regardless of is_404() or the 404_the_content filter. LH_Membership_access_control_class::show_no_access() only ever affects the page <title> (a global filter, unrelated to the template system) for a BuddyPress URL; the screen content itself is entirely unaffected by it.</p>
<p>handle_no_access() for BuddyPress now redirects the non-member to their own profile (bp_members_get_user_url()) instead, with a bp_core_add_message() flash notice explaining why &#8212; this is what the retired no_access_class&#8217;s redirect_for_buddypress() always did, and 2.62 replaced it with the in-place 403 overlay that doesn&#8217;t work for BuddyPress specifically. Every other gate (restrict/confidential/logged_in/private/private_site) is unaffected &#8212; they render through the normal WP template flow, where show_no_access() does work correctly. Redirecting to the user&#8217;s own profile is safe against a redirect loop, since is_own_profile() will be true on the next request.</p>
<p><strong>2.62 2026-07-18</strong><br />
Fixed BuddyPress access gating: logged-in non-members could view every BuddyPress component, not just their own profile and its subscreens as intended. Two separate root causes, both pre-existing &#8212; neither introduced by the 2.59 unification:</p>
<p>1. LoginRequired() never checked actual club membership at all &#8212; only whether the visitor was logged in and the component wasn&#8217;t whitelisted. The real &#8220;is this user a current member&#8221; check (LH_Membership_member_type_class::get_membership_type_details()) was only ever wired up to the own-profile exemption, never to gating the Activity feed, Groups directory, Members directory, or any other component in general.</p>
<p>2. The logged-in branch of force_login_if_required() only ever ran its check inside <code>if (is_404() &amp;&amp; ...)</code> &#8212; but BuddyPress component pages are normal 200 responses via BuddyPress&#8217;s own rewrite rules, not 404s, so that condition was never true in practice.</p>
<p>Added is_member() (wired to the real membership-type check) and two new audience-decision methods, visitor_requires_login() and non_member_requires_access(), which combine it with the own-profile exemption and the whitelist(s). force_login_if_required() and hijack_bp_no_access() now both gate on these directly.</p>
<p>Also added two new filterable whitelists, split out from the existing general one (lh_membership_return_white_labelled_components): lh_membership_return_visitor_accessible_components (visitors only) and lh_membership_return_non_member_accessible_components (logged-in non-members only).</p>
<p><strong>2.61 2026-07-18</strong><br />
Fixed the root cause of restricted-status content being visible to users who shouldn&#8217;t have access. Root cause was a long-standing bug in create_role_and_assign_capability()&#8217;s auto-grant loop: it iterated $wp_roles-&gt;roles checking $role[&#8216;role&#8217;], but that array is keyed by role slug with no &#8216;role&#8217; sub-key, so the condition was always empty and the loop never ran &#8212; effectively dead code. Fixed to iterate correctly by role slug using get_role(), and made it self-healing: a role that has the capability but doesn&#8217;t qualify now has it actively removed. Only runs on plugin activation.</p>
<p><strong>2.60 2026-07-18</strong><br />
Fixed a fatal error on activation: LH_Membership_access_control_class defined a static redirect_to_login() method colliding with the main plugin class&#8217;s non-static instance method of the same name. Renamed to send_to_login_for_access_denied() throughout. No behavioural change.</p>
<p><strong>2.59 2026-07-18</strong><br />
Unified non-public content access handling. Previously seven classes independently implemented pieces of &#8220;decide who gets in, tell them why if not&#8221; &#8212; three post-status classes (restrict/confidential/logged_in), two content-redirect classes, and two whole-site/whole-section gates (private_site/private_buddypress) &#8212; plus an eighth undocumented class (LH_Membership_no_access_class) handling logged-in-but-insufficient-access 403s.</p>
<p>New canonical class, LH_Membership_access_control_class, is the single home for both outcomes: not-logged-in -&gt; redirect to login with a reason-coded query arg; logged-in-insufficient-access -&gt; 403 &#8220;No Access&#8221; page. Three filterable lookups (lh_membership_return_non_public_statuses, lh_membership_view_capability_for_status, and message filters) let any status/gate participate without lh-membership knowing about them.</p>
<p>New LH_Membership_post_status_gate_class is the single template_redirect trigger for every post-status-based gate. New LH_Membership_post_status_base_class removes duplicated boilerplate across restrict/confidential/logged_in. LH_Membership_private_content_class reduced to a policy class. LH_Membership_private_site_class and LH_Membership_private_buddypress_class now call the shared outcome methods. LH_Membership_no_access_class retired.</p>
<p><strong>Action needed:</strong> the query-arg rename means any bookmarked/cached URL using the old ?lh_membership-login_required=true flag no longer triggers the login message &#8212; only affects single-use mid-flow redirect URLs, so no stored data affected.</p>
<p><strong>2.58 2026-07-18</strong><br />
Brought the standalone &#8220;LH Private BuddyPress&#8221; plugin (v1.12) into lh-membership as LH_Membership_private_buddypress_class &#8212; gates whole BuddyPress component pages from logged-out visitors via the &#8216;wp&#8217; action and BuddyPress&#8217;s bp_core_no_access filter. Faithful port; fixed a spelling typo (return_exluded_page_ids -&gt; return_excluded_page_ids), internal only.</p>
<p><strong>Action needed:</strong> deactivate and remove the standalone lh-private-buddypress plugin.</p>
<p><strong>2.57 2026-07-18</strong><br />
Removed LH_Membership_sso_class::return_rest_namespace() &#8212; orphan flagged in 2.56, confirmed zero remaining callers. No functional changes.</p>
<p><strong>2.56 2026-07-18</strong><br />
Three bug fixes: (1) private_content_class&#8217;s attachment_id branch was re-fetching the original queried object instead of the attachment specified in $_GET[&#8216;attachment_id&#8217;] &#8212; fixed. (2) Fixed a typo in a class_exists guard (resticted -&gt; restricted) &#8212; guard was always false, harmless but dead. (3) Removed three confirmed-orphaned SSO methods.</p>
<p><strong>2.55 2026-07-18</strong><br />
Brought the standalone &#8220;LH Private Content Login&#8221; plugin (v1.05) into lh-membership as LH_Membership_private_content_login_class. Faithful, minimal-change port, deliberately not reconciled with the heavily-overlapping LH_Membership_private_content_class &#8212; that reconciliation was planned as a later overhaul. Documented six areas of overlap/divergence between the two classes for that future work. No functional changes in this release.</p>
<p><strong>Action needed:</strong> deactivate and remove the standalone lh-private-content-login plugin.</p>
<p><strong>2.54 2026-07-16</strong><br />
Added a second lh-competitions integration alongside round-nav: hooked lh_comps_event_listing_post_status and include &#8216;restrict&#8217; for users with read_restricted_posts, so restricted events surface in the Event Listing block.</p>
<p><strong>2.53 2026-07-16</strong><br />
Brought the standalone &#8220;LH Logged in Post status&#8221; plugin (v1.09) into lh-membership as LH_Membership_logged_in_post_status_class, following the restricted/confidential post-status pattern. Fixed a real bug in the process: the standalone plugin&#8217;s filter name for its private_content_class integration never matched the actual filter, so it never fired on any site running both plugins.</p>
<p><strong>2.52 2026-07-12</strong><br />
Refactor: generic utilities (IsResourceLocal, url_to_relative_path, curpageurl, uri_to_array, http_parse_query, build_url, generate_post_select) moved to new LH_Membership_generic_helpers_class. xprofile_form_output() and handle_bp_edit() moved into LH_Membership_confirm_details_class. Main plugin class down to 47,369 bytes from 87,013 at refactor start (46% reduction). No functional changes.</p>
<p><strong>2.51 2026-07-12</strong><br />
Refactor (step 5, final): split handle_form_post() out &#8212; login branch to LH_Membership_login_class, password-change branch to LH_Membership_change_password_class, each self-registering on wp_loaded. Also moved simple_login_user(), do_login_setup(), add_login_fail_message() to login_class. Completes the planned 5-step refactor: main class down 32% across all steps. No functional changes.</p>
<p><strong>2.50 2026-07-12</strong><br />
Dead code sweep: removed 11 confirmed-unreferenced methods across the main class and three other classes, verified individually to rule out framework-invoked methods. No functional changes.</p>
<p><strong>2.49 2026-07-12</strong><br />
Refactor (step 4, narrowed scope): removed four duplicate page-slug/URL methods from the main class in favour of the equivalents already in login_class/recover_account_class/change_password_class. Two other originally-scoped items turned out not to be real duplication and were dropped. No functional changes.</p>
<p><strong>2.48 2026-07-12</strong><br />
Refactor (step 3): moved the &#8220;use email addresses&#8221; login setting UI out of the main class into LH_Membership_login_class. No functional changes.</p>
<p><strong>2.47 2026-07-12</strong><br />
Added lh-competitions integration: hook lh_comps_round_nav_post_status, include &#8216;restrict&#8217; in round navigation for users with read_restricted_posts.</p>
<p><strong>2.46 2026-07-12</strong><br />
Refactor: extracted the OTC/passkey subsystem into a new LH_Membership_otc_class, including the hourly cleanup cron. No functional changes.</p>
<p><strong>2.45 2026-07-12</strong><br />
Refactor: moved site-invitation methods into LH_Membership_user_invitations_class. Removed a dead unimplemented shortcode registration. No functional changes.</p>
<p><strong>2.44 2026-06-23</strong><br />
Switched get_user_by_email() fallback from a per-plugin filter to the canonical lh_user_identity_get_user_by_email_fallback.</p>
<p><strong>2.43 2026-06-23</strong><br />
Registration: use LH_membership_plugin::get_user_by_email() in ensure_user_is_added() so alias email addresses are recognised as existing accounts.</p>
<p><strong>2.31 May 07, 2026</strong><br />
Cleanup: removed duplicate user invitations include, replaced authentication codes if-chain with loop, removed dead rest_login_user method, removed SSO debug logging.</p>
<p><strong>2.30 May 07, 2026</strong> &#8212; SSO: renamed parameter to lh_membership-sso_return.<br />
<strong>2.29 May 07, 2026</strong> &#8212; SSO: renamed parameter to lh-membership-sso_return.<br />
<strong>2.28 May 07, 2026</strong> &#8212; SSO: prefixed parameter with plugin namespace.<br />
<strong>2.27 May 07, 2026</strong> &#8212; SSO: renamed return_url parameter to lh_sso_return.<br />
<strong>2.26 May 07, 2026</strong> &#8212; SSO: added missing base64url_encode/decode methods.<br />
<strong>2.25 May 07, 2026</strong> &#8212; SSO: added debug logging, fixed base64url encoding.<br />
<strong>2.24 May 07, 2026</strong> &#8212; SSO: replaced sanitize_text_field with regex whitelist for token parameters.<br />
<strong>2.23 May 07, 2026</strong> &#8212; SSO: capture raw session token via set_auth_cookie action for same-request logins.<br />
<strong>2.22 May 07, 2026</strong> &#8212; SSO: converted sso-receive to template_redirect.<br />
<strong>2.21 May 07, 2026</strong> &#8212; SSO: converted all three endpoints to template_redirect, removed REST routes.<br />
<strong>2.20 May 07, 2026</strong> &#8212; SSO: attempted nonce-based REST auth (reverted).<br />
<strong>2.19 May 07, 2026</strong> &#8212; SSO: fixed plugin folder name in zip.<br />
<strong>2.18 May 07, 2026</strong> &#8212; SSO: converted endpoints to REST routes.<br />
<strong>2.17 May 07, 2026</strong> &#8212; SSO: route OTC logins through main site sync via return_sync_redirect_url().<br />
<strong>2.16 May 07, 2026</strong> &#8212; Added auth page exclusion list to SSO logged-out script.<br />
<strong>2.15 May 07, 2026</strong> &#8212; OTC security hardening: brute force protection (10 attempt limit), expiry enforcement, cron cleanup of expired OTCs, encryption keys derived from wp_salt(), removed debug email, fixed do_login_extension for all users, fixed broken string concatenation, fixed rest_login_user undefined variable.</p>
<p><strong>2.14 May 24, 2019</strong> &#8212; Bug fix.<br />
<strong>2.13 May 08, 2019</strong> &#8212; Direct access check.<br />
<strong>2.12 June 06, 2018</strong> &#8212; filemtime and much more.<br />
<strong>2.11 November 02, 2017</strong> &#8212; Translatable strings.<br />
<strong>2.10 October 25, 2017</strong> &#8212; Latest web credential standards.<br />
<strong>2.09 June 10, 2017</strong> &#8212; Better option settings.<br />
<strong>2.08 June 08, 2017</strong> &#8212; Web credentials support.<br />
<strong>2.07 May 10, 2017</strong> &#8212; Improved settings screen, bumped to force update.<br />
<strong>2.05 April 20, 2017</strong> &#8212; Added woocommerce support.<br />
<strong>2.04 April 10, 2017</strong> &#8212; Add login link shortcode.<br />
<strong>2.02 March 24, 2017</strong> &#8212; Use isset.<br />
<strong>2.01 March 20, 2017</strong> &#8212; Input focus.<br />
<strong>1.99 December 02, 2016</strong> &#8212; Support redirect attribute.<br />
<strong>1.98 September 30, 2016</strong> &#8212; Better redirects.<br />
<strong>1.96 December 17, 2015</strong> &#8212; Error fix.<br />
<strong>1.96 April 12, 2016</strong> &#8212; Better multisite support.<br />
<strong>1.94 December 07, 2015</strong> &#8212; Add pages across network.<br />
<strong>1.92 November 18, 2015</strong> &#8212; Redirect fix.<br />
<strong>1.92 November 31, 2015</strong> &#8212; Fixed logout.<br />
<strong>1.90 November 18, 2015</strong> &#8212; Support SSL.<br />
<strong>1.81 November 18, 2015</strong> &#8212; Minor fix.<br />
<strong>1.80 November 18, 2015</strong> &#8212; Allow redirects to be blocked.<br />
<strong>1.62 October 27, 2015</strong> &#8212; Bug fix.<br />
<strong>1.60 October 27, 2015</strong> &#8212; Removes title on login page and styles form.<br />
<strong>1.52 September 05, 2015</strong> &#8212; Fixed row meta.<br />
<strong>1.51 September 05, 2015</strong> &#8212; Fixed redirect bug.<br />
<strong>1.21 August 20, 2015</strong> &#8212; Improved menu.<br />
<strong>1.7 October 27, 2015</strong> &#8212; Added login_form hook.<br />
<strong>1.5 September 05, 2015</strong> &#8212; Use email addresses or user names.<br />
<strong>1.4 August 31, 2015</strong> &#8212; Automatically create page.<br />
<strong>1.3 August 25, 2015</strong> &#8212; Added filters.<br />
<strong>1.1 July 17, 2015</strong> &#8212; Settings link.<br />
<strong>1.00 July 13, 2015</strong> &#8212; Initial release.</p>
"""^^rdf:XMLLiteral ;
  sioc:content """All notable changes to LH Membership are documented here. This entry is canonical and append-only.
2.71 2026-08-18
Added a sixth lh-competitions-family integration for restrict, this time into lh-buddypress-check-ins: new method add_restricted_to_check_ins_event_statuses() hooks the new lh_bp_check_ins_event_post_status filter (lh-buddypress-check-ins 3.03) so restricted events are included in the check-ins plugin&#8217;s next-event and listing shortcodes for users who can view them (current_user_can_view() — same gated pattern as round-nav, event-listing, and game-edit-event). This closes the root cause of lh_bp_check_ins_display_next surfacing the wrong event on Princes Park Touch: the shortcode&#8217;s hardcoded post_status =&gt; 'publish' was skipping every restricted-status Wednesday-night round in favour of whichever publish-status round happened to be furthest in the future.
2.69 2026-08-17
Added a filterable capability-based exemption from the member-onboarding BuddyPress gate: LH_Membership_private_buddypress_class::non_member_requires_access() now returns false immediately for any user with the promote_users capability (default, filterable via lh_membership_return_gate_exempt_capability), regardless of their own membership/approval status. New method return_gate_exempt_capability() added alongside the class&#8217;s other filterable whitelists.
2.68 2026-08-03
Added a sixth restrict integration, this time into lh-buddypress-log-entry rather than lh-competitions: hooks the new lh_bp_log_entry_return_event_post_statuses filter (lh-buddypress-log-entry 1.03) so restricted events are included in the log entry form&#8217;s event lookup, the open/closed shortcodes, and the past-events listing. Per Peter&#8217;s explicit instruction, this integration is unconditional &#8212; not gated by current_user_can_view() &#8212; same rationale as the standings integration: it&#8217;s just adding restrict to the set of statuses queried, not deciding what a particular viewer gets to see.
2.67 2026-07-18
Added a fifth lh-competitions integration for restrict: hooks the new lh_comps_latest_scoresheet_event_query_args filter, so the &#8220;Latest {Night} Scoresheet&#8221; dynamic menu item can resolve to a restricted event for users who can view them (current_user_can_view() &#8212; same gated pattern as round-nav, event-listing, and game-edit-event; unlike standings, which is deliberately unconditional). Default query status (&#8216;publish&#8217;, a string rather than an array here) is normalised to an array before appending restrict.
2.66 2026-07-18
Added a fourth lh-competitions integration for restrict: hooks the new lh_comps_standings_event_query_args filter (lh-competitions 1.80, applied without a version bump per Peter&#8217;s 2026-07-21 instruction &#8212; see that plugin&#8217;s own changelog) so events with status restrict are included in standings table calculations alongside publish. Unlike the other three restrict integrations (round-nav, event-listing, game-edit-event), this one is deliberately unconditional &#8212; not gated by current_user_can_view() &#8212; since the standings table is a computed aggregate (wins/points) rather than per-viewer visible content; gating which events feed into the calculation by the current viewer&#8217;s access would make the resulting numbers inconsistent depending on who&#8217;s looking. Flagged this deviation from the established gated pattern to Peter before implementing; proceeded on this assumption pending confirmation.
2.65 2026-07-18
Added a third lh-competitions integration for restrict, matching the existing round-nav and event-listing ones: hooks the new lh_comps_game_edit_event_post_status filter (lh-competitions v1.79+) so restricted events are selectable in the Event dropdown on the game edit screen&#8217;s Game Data meta box, for users who can view restricted content (self::current_user_can_view() &#8212; same check as the other two integrations). Not extended to confidential/logged_in, per the standing decision to keep this integration restrict-only.
2.64 2026-07-18
Reworded the BuddyPress access-denied flash message. Since the person only sees it after being redirected to their own profile, &#8220;This area is only visible to current members&#8221; reads as if it&#8217;s describing the page they just landed on. Changed to &#8220;You tried to access an area that is only visible to current members&#8221; to make clear it&#8217;s referring to where they came from.
2.63 2026-07-18
Fixed 2.62&#8217;s BuddyPress no-access page: the title correctly changed to &#8220;No Access&#8221; but the actual screen content (groups directory, members directory, etc.) kept rendering underneath it. Root cause: BuddyPress doesn&#8217;t render through WordPress&#8217;s normal template/404 flow at all &#8212; it has its own screen-rendering system that runs regardless of is_404() or the 404_the_content filter. LH_Membership_access_control_class::show_no_access() only ever affects the page  (a global filter, unrelated to the template system) for a BuddyPress URL; the screen content itself is entirely unaffected by it.
handle_no_access() for BuddyPress now redirects the non-member to their own profile (bp_members_get_user_url()) instead, with a bp_core_add_message() flash notice explaining why &#8212; this is what the retired no_access_class&#8217;s redirect_for_buddypress() always did, and 2.62 replaced it with the in-place 403 overlay that doesn&#8217;t work for BuddyPress specifically. Every other gate (restrict/confidential/logged_in/private/private_site) is unaffected &#8212; they render through the normal WP template flow, where show_no_access() does work correctly. Redirecting to the user&#8217;s own profile is safe against a redirect loop, since is_own_profile() will be true on the next request.
2.62 2026-07-18
Fixed BuddyPress access gating: logged-in non-members could view every BuddyPress component, not just their own profile and its subscreens as intended. Two separate root causes, both pre-existing &#8212; neither introduced by the 2.59 unification:
1. LoginRequired() never checked actual club membership at all &#8212; only whether the visitor was logged in and the component wasn&#8217;t whitelisted. The real &#8220;is this user a current member&#8221; check (LH_Membership_member_type_class::get_membership_type_details()) was only ever wired up to the own-profile exemption, never to gating the Activity feed, Groups directory, Members directory, or any other component in general.
2. The logged-in branch of force_login_if_required() only ever ran its check inside if (is_404() &amp;&amp; ...) &#8212; but BuddyPress component pages are normal 200 responses via BuddyPress&#8217;s own rewrite rules, not 404s, so that condition was never true in practice.
Added is_member() (wired to the real membership-type check) and two new audience-decision methods, visitor_requires_login() and non_member_requires_access(), which combine it with the own-profile exemption and the whitelist(s). force_login_if_required() and hijack_bp_no_access() now both gate on these directly.
Also added two new filterable whitelists, split out from the existing general one (lh_membership_return_white_labelled_components): lh_membership_return_visitor_accessible_components (visitors only) and lh_membership_return_non_member_accessible_components (logged-in non-members only).
2.61 2026-07-18
Fixed the root cause of restricted-status content being visible to users who shouldn&#8217;t have access. Root cause was a long-standing bug in create_role_and_assign_capability()&#8217;s auto-grant loop: it iterated $wp_roles-&gt;roles checking $role[&#8216;role&#8217;], but that array is keyed by role slug with no &#8216;role&#8217; sub-key, so the condition was always empty and the loop never ran &#8212; effectively dead code. Fixed to iterate correctly by role slug using get_role(), and made it self-healing: a role that has the capability but doesn&#8217;t qualify now has it actively removed. Only runs on plugin activation.
2.60 2026-07-18
Fixed a fatal error on activation: LH_Membership_access_control_class defined a static redirect_to_login() method colliding with the main plugin class&#8217;s non-static instance method of the same name. Renamed to send_to_login_for_access_denied() throughout. No behavioural change.
2.59 2026-07-18
Unified non-public content access handling. Previously seven classes independently implemented pieces of &#8220;decide who gets in, tell them why if not&#8221; &#8212; three post-status classes (restrict/confidential/logged_in), two content-redirect classes, and two whole-site/whole-section gates (private_site/private_buddypress) &#8212; plus an eighth undocumented class (LH_Membership_no_access_class) handling logged-in-but-insufficient-access 403s.
New canonical class, LH_Membership_access_control_class, is the single home for both outcomes: not-logged-in -&gt; redirect to login with a reason-coded query arg; logged-in-insufficient-access -&gt; 403 &#8220;No Access&#8221; page. Three filterable lookups (lh_membership_return_non_public_statuses, lh_membership_view_capability_for_status, and message filters) let any status/gate participate without lh-membership knowing about them.
New LH_Membership_post_status_gate_class is the single template_redirect trigger for every post-status-based gate. New LH_Membership_post_status_base_class removes duplicated boilerplate across restrict/confidential/logged_in. LH_Membership_private_content_class reduced to a policy class. LH_Membership_private_site_class and LH_Membership_private_buddypress_class now call the shared outcome methods. LH_Membership_no_access_class retired.
Action needed: the query-arg rename means any bookmarked/cached URL using the old ?lh_membership-login_required=true flag no longer triggers the login message &#8212; only affects single-use mid-flow redirect URLs, so no stored data affected.
2.58 2026-07-18
Brought the standalone &#8220;LH Private BuddyPress&#8221; plugin (v1.12) into lh-membership as LH_Membership_private_buddypress_class &#8212; gates whole BuddyPress component pages from logged-out visitors via the &#8216;wp&#8217; action and BuddyPress&#8217;s bp_core_no_access filter. Faithful port; fixed a spelling typo (return_exluded_page_ids -&gt; return_excluded_page_ids), internal only.
Action needed: deactivate and remove the standalone lh-private-buddypress plugin.
2.57 2026-07-18
Removed LH_Membership_sso_class::return_rest_namespace() &#8212; orphan flagged in 2.56, confirmed zero remaining callers. No functional changes.
2.56 2026-07-18
Three bug fixes: (1) private_content_class&#8217;s attachment_id branch was re-fetching the original queried object instead of the attachment specified in $_GET[&#8216;attachment_id&#8217;] &#8212; fixed. (2) Fixed a typo in a class_exists guard (resticted -&gt; restricted) &#8212; guard was always false, harmless but dead. (3) Removed three confirmed-orphaned SSO methods.
2.55 2026-07-18
Brought the standalone &#8220;LH Private Content Login&#8221; plugin (v1.05) into lh-membership as LH_Membership_private_content_login_class. Faithful, minimal-change port, deliberately not reconciled with the heavily-overlapping LH_Membership_private_content_class &#8212; that reconciliation was planned as a later overhaul. Documented six areas of overlap/divergence between the two classes for that future work. No functional changes in this release.
Action needed: deactivate and remove the standalone lh-private-content-login plugin.
2.54 2026-07-16
Added a second lh-competitions integration alongside round-nav: hooked lh_comps_event_listing_post_status and include &#8216;restrict&#8217; for users with read_restricted_posts, so restricted events surface in the Event Listing block.
2.53 2026-07-16
Brought the standalone &#8220;LH Logged in Post status&#8221; plugin (v1.09) into lh-membership as LH_Membership_logged_in_post_status_class, following the restricted/confidential post-status pattern. Fixed a real bug in the process: the standalone plugin&#8217;s filter name for its private_content_class integration never matched the actual filter, so it never fired on any site running both plugins.
2.52 2026-07-12
Refactor: generic utilities (IsResourceLocal, url_to_relative_path, curpageurl, uri_to_array, http_parse_query, build_url, generate_post_select) moved to new LH_Membership_generic_helpers_class. xprofile_form_output() and handle_bp_edit() moved into LH_Membership_confirm_details_class. Main plugin class down to 47,369 bytes from 87,013 at refactor start (46% reduction). No functional changes.
2.51 2026-07-12
Refactor (step 5, final): split handle_form_post() out &#8212; login branch to LH_Membership_login_class, password-change branch to LH_Membership_change_password_class, each self-registering on wp_loaded. Also moved simple_login_user(), do_login_setup(), add_login_fail_message() to login_class. Completes the planned 5-step refactor: main class down 32% across all steps. No functional changes.
2.50 2026-07-12
Dead code sweep: removed 11 confirmed-unreferenced methods across the main class and three other classes, verified individually to rule out framework-invoked methods. No functional changes.
2.49 2026-07-12
Refactor (step 4, narrowed scope): removed four duplicate page-slug/URL methods from the main class in favour of the equivalents already in login_class/recover_account_class/change_password_class. Two other originally-scoped items turned out not to be real duplication and were dropped. No functional changes.
2.48 2026-07-12
Refactor (step 3): moved the &#8220;use email addresses&#8221; login setting UI out of the main class into LH_Membership_login_class. No functional changes.
2.47 2026-07-12
Added lh-competitions integration: hook lh_comps_round_nav_post_status, include &#8216;restrict&#8217; in round navigation for users with read_restricted_posts.
2.46 2026-07-12
Refactor: extracted the OTC/passkey subsystem into a new LH_Membership_otc_class, including the hourly cleanup cron. No functional changes.
2.45 2026-07-12
Refactor: moved site-invitation methods into LH_Membership_user_invitations_class. Removed a dead unimplemented shortcode registration. No functional changes.
2.44 2026-06-23
Switched get_user_by_email() fallback from a per-plugin filter to the canonical lh_user_identity_get_user_by_email_fallback.
2.43 2026-06-23
Registration: use LH_membership_plugin::get_user_by_email() in ensure_user_is_added() so alias email addresses are recognised as existing accounts.
2.31 May 07, 2026
Cleanup: removed duplicate user invitations include, replaced authentication codes if-chain with loop, removed dead rest_login_user method, removed SSO debug logging.
2.30 May 07, 2026 &#8212; SSO: renamed parameter to lh_membership-sso_return.
2.29 May 07, 2026 &#8212; SSO: renamed parameter to lh-membership-sso_return.
2.28 May 07, 2026 &#8212; SSO: prefixed parameter with plugin namespace.
2.27 May 07, 2026 &#8212; SSO: renamed return_url parameter to lh_sso_return.
2.26 May 07, 2026 &#8212; SSO: added missing base64url_encode/decode methods.
2.25 May 07, 2026 &#8212; SSO: added debug logging, fixed base64url encoding.
2.24 May 07, 2026 &#8212; SSO: replaced sanitize_text_field with regex whitelist for token parameters.
2.23 May 07, 2026 &#8212; SSO: capture raw session token via set_auth_cookie action for same-request logins.
2.22 May 07, 2026 &#8212; SSO: converted sso-receive to template_redirect.
2.21 May 07, 2026 &#8212; SSO: converted all three endpoints to template_redirect, removed REST routes.
2.20 May 07, 2026 &#8212; SSO: attempted nonce-based REST auth (reverted).
2.19 May 07, 2026 &#8212; SSO: fixed plugin folder name in zip.
2.18 May 07, 2026 &#8212; SSO: converted endpoints to REST routes.
2.17 May 07, 2026 &#8212; SSO: route OTC logins through main site sync via return_sync_redirect_url().
2.16 May 07, 2026 &#8212; Added auth page exclusion list to SSO logged-out script.
2.15 May 07, 2026 &#8212; OTC security hardening: brute force protection (10 attempt limit), expiry enforcement, cron cleanup of expired OTCs, encryption keys derived from wp_salt(), removed debug email, fixed do_login_extension for all users, fixed broken string concatenation, fixed rest_login_user undefined variable.
2.14 May 24, 2019 &#8212; Bug fix.
2.13 May 08, 2019 &#8212; Direct access check.
2.12 June 06, 2018 &#8212; filemtime and much more.
2.11 November 02, 2017 &#8212; Translatable strings.
2.10 October 25, 2017 &#8212; Latest web credential standards.
2.09 June 10, 2017 &#8212; Better option settings.
2.08 June 08, 2017 &#8212; Web credentials support.
2.07 May 10, 2017 &#8212; Improved settings screen, bumped to force update.
2.05 April 20, 2017 &#8212; Added woocommerce support.
2.04 April 10, 2017 &#8212; Add login link shortcode.
2.02 March 24, 2017 &#8212; Use isset.
2.01 March 20, 2017 &#8212; Input focus.
1.99 December 02, 2016 &#8212; Support redirect attribute.
1.98 September 30, 2016 &#8212; Better redirects.
1.96 December 17, 2015 &#8212; Error fix.
1.96 April 12, 2016 &#8212; Better multisite support.
1.94 December 07, 2015 &#8212; Add pages across network.
1.92 November 18, 2015 &#8212; Redirect fix.
1.92 November 31, 2015 &#8212; Fixed logout.
1.90 November 18, 2015 &#8212; Support SSL.
1.81 November 18, 2015 &#8212; Minor fix.
1.80 November 18, 2015 &#8212; Allow redirects to be blocked.
1.62 October 27, 2015 &#8212; Bug fix.
1.60 October 27, 2015 &#8212; Removes title on login page and styles form.
1.52 September 05, 2015 &#8212; Fixed row meta.
1.51 September 05, 2015 &#8212; Fixed redirect bug.
1.21 August 20, 2015 &#8212; Improved menu.
1.7 October 27, 2015 &#8212; Added login_form hook.
1.5 September 05, 2015 &#8212; Use email addresses or user names.
1.4 August 31, 2015 &#8212; Automatically create page.
1.3 August 25, 2015 &#8212; Added filters.
1.1 July 17, 2015 &#8212; Settings link.
1.00 July 13, 2015 &#8212; Initial release.
""" ;
  sioc:topic <https://lhero.org/lh_portfolio-type/doc-section/>, <https://lhero.org/?taxonomy=author&term=cap-1015845> .

<https://lhero.org/author/1015845/#account> rdfs:seeAlso <https://lhero.org/author/1015845/?feed=lhrdf&format=turtle> .
<https://lhero.org/lh_portfolio-type/doc-section/> rdfs:seeAlso <https://lhero.org/lh_portfolio-type/doc-section/?feed=lhrdf&format=turtle> .
<https://lhero.org/?taxonomy=author&term=cap-1015845> rdfs:seeAlso <https://lhero.org/?taxonomy=author&term=cap-1015845&feed=lhrdf&format=turtle> .
