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’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’s hardcoded post_status => '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’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’s event lookup, the open/closed shortcodes, and the past-events listing. Per Peter’s explicit instruction, this integration is unconditional — not gated by current_user_can_view() — same rationale as the standings integration: it’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 “Latest {Night} Scoresheet” dynamic menu item can resolve to a restricted event for users who can view them (current_user_can_view() — same gated pattern as round-nav, event-listing, and game-edit-event; unlike standings, which is deliberately unconditional). Default query status (‘publish’, 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’s 2026-07-21 instruction — see that plugin’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 — not gated by current_user_can_view() — 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’s access would make the resulting numbers inconsistent depending on who’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’s Game Data meta box, for users who can view restricted content (self::current_user_can_view() — 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, “This area is only visible to current members” reads as if it’s describing the page they just landed on. Changed to “You tried to access an area that is only visible to current members” to make clear it’s referring to where they came from.
2.63 2026-07-18
Fixed 2.62’s BuddyPress no-access page: the title correctly changed to “No Access” but the actual screen content (groups directory, members directory, etc.) kept rendering underneath it. Root cause: BuddyPress doesn’t render through WordPress’s normal template/404 flow at all — 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
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 — this is what the retired no_access_class’s redirect_for_buddypress() always did, and 2.62 replaced it with the in-place 403 overlay that doesn’t work for BuddyPress specifically. Every other gate (restrict/confidential/logged_in/private/private_site) is unaffected — they render through the normal WP template flow, where show_no_access() does work correctly. Redirecting to the user’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 — neither introduced by the 2.59 unification:
1. LoginRequired() never checked actual club membership at all — only whether the visitor was logged in and the component wasn’t whitelisted. The real “is this user a current member” 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() && ...) — but BuddyPress component pages are normal 200 responses via BuddyPress’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’t have access. Root cause was a long-standing bug in create_role_and_assign_capability()’s auto-grant loop: it iterated $wp_roles->roles checking $role[‘role’], but that array is keyed by role slug with no ‘role’ sub-key, so the condition was always empty and the loop never ran — 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’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’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 “decide who gets in, tell them why if not” — three post-status classes (restrict/confidential/logged_in), two content-redirect classes, and two whole-site/whole-section gates (private_site/private_buddypress) — 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 -> redirect to login with a reason-coded query arg; logged-in-insufficient-access -> 403 “No Access” 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 — only affects single-use mid-flow redirect URLs, so no stored data affected.
2.58 2026-07-18
Brought the standalone “LH Private BuddyPress” plugin (v1.12) into lh-membership as LH_Membership_private_buddypress_class — gates whole BuddyPress component pages from logged-out visitors via the ‘wp’ action and BuddyPress’s bp_core_no_access filter. Faithful port; fixed a spelling typo (return_exluded_page_ids -> 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() — orphan flagged in 2.56, confirmed zero remaining callers. No functional changes.
2.56 2026-07-18
Three bug fixes: (1) private_content_class’s attachment_id branch was re-fetching the original queried object instead of the attachment specified in $_GET[‘attachment_id’] — fixed. (2) Fixed a typo in a class_exists guard (resticted -> restricted) — guard was always false, harmless but dead. (3) Removed three confirmed-orphaned SSO methods.
2.55 2026-07-18
Brought the standalone “LH Private Content Login” 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 — 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 ‘restrict’ for users with read_restricted_posts, so restricted events surface in the Event Listing block.
2.53 2026-07-16
Brought the standalone “LH Logged in Post status” 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’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 — 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 “use email addresses” 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 ‘restrict’ 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 — SSO: renamed parameter to lh_membership-sso_return.
2.29 May 07, 2026 — SSO: renamed parameter to lh-membership-sso_return.
2.28 May 07, 2026 — SSO: prefixed parameter with plugin namespace.
2.27 May 07, 2026 — SSO: renamed return_url parameter to lh_sso_return.
2.26 May 07, 2026 — SSO: added missing base64url_encode/decode methods.
2.25 May 07, 2026 — SSO: added debug logging, fixed base64url encoding.
2.24 May 07, 2026 — SSO: replaced sanitize_text_field with regex whitelist for token parameters.
2.23 May 07, 2026 — SSO: capture raw session token via set_auth_cookie action for same-request logins.
2.22 May 07, 2026 — SSO: converted sso-receive to template_redirect.
2.21 May 07, 2026 — SSO: converted all three endpoints to template_redirect, removed REST routes.
2.20 May 07, 2026 — SSO: attempted nonce-based REST auth (reverted).
2.19 May 07, 2026 — SSO: fixed plugin folder name in zip.
2.18 May 07, 2026 — SSO: converted endpoints to REST routes.
2.17 May 07, 2026 — SSO: route OTC logins through main site sync via return_sync_redirect_url().
2.16 May 07, 2026 — Added auth page exclusion list to SSO logged-out script.
2.15 May 07, 2026 — 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 — Bug fix.
2.13 May 08, 2019 — Direct access check.
2.12 June 06, 2018 — filemtime and much more.
2.11 November 02, 2017 — Translatable strings.
2.10 October 25, 2017 — Latest web credential standards.
2.09 June 10, 2017 — Better option settings.
2.08 June 08, 2017 — Web credentials support.
2.07 May 10, 2017 — Improved settings screen, bumped to force update.
2.05 April 20, 2017 — Added woocommerce support.
2.04 April 10, 2017 — Add login link shortcode.
2.02 March 24, 2017 — Use isset.
2.01 March 20, 2017 — Input focus.
1.99 December 02, 2016 — Support redirect attribute.
1.98 September 30, 2016 — Better redirects.
1.96 December 17, 2015 — Error fix.
1.96 April 12, 2016 — Better multisite support.
1.94 December 07, 2015 — Add pages across network.
1.92 November 18, 2015 — Redirect fix.
1.92 November 31, 2015 — Fixed logout.
1.90 November 18, 2015 — Support SSL.
1.81 November 18, 2015 — Minor fix.
1.80 November 18, 2015 — Allow redirects to be blocked.
1.62 October 27, 2015 — Bug fix.
1.60 October 27, 2015 — Removes title on login page and styles form.
1.52 September 05, 2015 — Fixed row meta.
1.51 September 05, 2015 — Fixed redirect bug.
1.21 August 20, 2015 — Improved menu.
1.7 October 27, 2015 — Added login_form hook.
1.5 September 05, 2015 — Use email addresses or user names.
1.4 August 31, 2015 — Automatically create page.
1.3 August 25, 2015 — Added filters.
1.1 July 17, 2015 — Settings link.
1.00 July 13, 2015 — Initial release.