- 3.7.2: Profile fields filled in on LH Buddypress Group Invites' invite-by-email form (phone, gender, birthdate, postcode and names) are now saved to the person being invited, using the same rules as CRM enquiry forms: a value is only written where the invitee has none, unless the person sending the invite may edit the invitee, in which case it replaces the stored value. Before, those values were never saved to the invitee. This needs the matching lh-buddypress-group-invites release, which stops the form from being addressed to the person sending the invite; until then the form's inputs still name the inviter.
- 3.7.1: LH CRM enquiry forms and the LH Buddypress Check Ins / Log Entry unknown-user form now save profile fields through the same single save path as the profile screens. For someone allowed to edit the member, supplied values replace stored ones; for everyone else (typically the member themselves filling in a public form) a value is only written where the member has none, and the postal address is only written when all of it is empty, so a postcode from an enquiry form is no longer added to an address that already has a street or town. Phone numbers that cannot be parsed are now rejected instead of deleting the stored number.
- 3.7.0: Profile fields now have a single save path (LH_Profile_Field_Saver) with two explicit modes: overwrite, for a member or someone allowed to edit them changing a profile, and fill_empty, for data arriving from forms and imports, which only fills what is missing and treats the postal address as one unit. The front-end profile edit save and the wp-admin Profile Data save now use it. Every value is validated and saved through its field's own save path, and an invalid value (for example a phone number that cannot be parsed) is rejected without deleting what was already stored; before, an invalid phone number entered on the front end wiped the saved number. Clearing a field by emptying it now only happens on the wp-admin Profile Data form, for users who can edit all users. Also fixes the front-end display name save, which passed the wrong user to WordPress.
- 3.6.2: Two fixes on the wp-admin user screens. The Users list search (which also matches phone numbers and additional email addresses) no longer replaces the whole search query: before, it rebuilt the query's conditions from scratch, which could drop other filters such as the site's role and membership restrictions or a role filter chosen on the screen. It now adds the phone and additional-email columns to WordPress's own user search instead. On the wp-admin user profile screen, Town and Postcode no longer appear twice: they were added once as contact methods and once in this plugin's own Profile Data section, and whichever came later in the form overwrote the other when saving. Only the Profile Data inputs remain.
- 3.6.1: Fix: the First Name, Last Name and Display Name profile field types now show the member's real names. They read from user meta keys that are never written (lh_profile-first_name and so on), so anything reading those fields through BuddyPress – the profile loop, xprofile_get_field_data() and the xProfile data export – got a blank value. They now read WordPress's own first name, last name and display name. The edit screen was unaffected, as it already rendered the real values. The xProfile data export also now returns the real value for every one of this plugin's field types, including the name fields.
- 3.6.0: Fix: profile fields from this plugin (street address, town, postcode, gender, birthdate, phone and the name fields) can now be edited and saved on the wp-admin Extended Profile screen (Users > Edit > Extended Profile). Before, changes there were silently discarded; only the front-end profile edit screen saved them. The BuddyPress xProfile integration now lives in a class of its own. Other fixes that came with the move: reading one of these fields through BuddyPress no longer runs a full profile loop on every call; profile field values now come from the member whose profile is being shown, so the wp-admin Extended Profile screen shows the member rather than the admin editing them; the gender field's default setting only applies to gender fields and must be a valid gender; the email shown to admins on profile edit is escaped.
- 3.5.1: Removed code that nothing on the network used: the [lh_profile_admin_form] and [lh_profile_listing_table] shortcodes (no page, template or widget contains either), the onboarding redirect helpers behind them, a group registration form hook, and several other functions whose only callers had already been commented out. No change in behaviour for any live page.
- 3.5.0: Internal restructuring, continued: the gendered avatar fallbacks and the admin Users list additions now live in classes of their own. Avatar output is now escaped, and the columns added to the Users screen (Gender, Additional Emails) are escaped and hidden by default only on the Users screens. Filtering the Users list by gender now checks the value is a real gender and combines with other filters instead of replacing them. The extra user search on phone numbers and additional emails uses prepared queries, and its list of searched fields can be changed with the lh_profile_return_searchable_fields filter. Removed the Google Places API key field from Network Settings: it was never saved or used.
- 3.4.0: Internal restructuring, continued: the LH Buddypress Check Ins / Log Entry integration and the LH CRM integration now live in classes of their own. Fixes that came with the move: saving profile fields from a CRM enquiry no longer overwrites the person's display name every time (it is only set when it is still a placeholder such as an email address, or when the current user may edit them); CRM saves now validate gender, store phone numbers with the selected country and write birthdates the same way the profile form does; the extra fields on the check-in unknown-user form no longer read undefined values; the mobile and gender columns in the check-in table are escaped and the text-message link is well formed. Two hook listeners that no plugin on the network fires were removed: the group registration hook and the CSV importer hooks.
- 3.3.0: Internal restructuring: the code that connects profile fields to other plugins now lives in three classes of its own instead of the main plugin class – lh-vcard (profile fields in member vCards), Users Insights (address, gender and birthdate columns and filters), and LH Buddypress Group Reports plus the users CSV export. Two small fixes came with the move: phone links in group reports now include the international + prefix so they dial correctly, and the link text is escaped. The group whose reports also show address columns (previously hard-coded) is now set through the lh_profile_return_address_report_group_ids filter, which still defaults to the same group. No other change in behaviour.
- 3.2.1: Security fix: the front-end profile save now checks the security token (nonce) on the street address and town fields, as it already did for postcode and the other profile fields. Previously a crafted request from another site could change a member's street address or town if someone allowed to edit that member visited it while logged in. The address inputs already included the token, so profile edit forms and the onboarding form keep working unchanged.
- 3.2.0: The create-or-fill-user ability now reports action unchanged when it is given an existing user and nothing about them changes: every supplied field was already set or was rejected, and the user already had a role on the current site. Before, it reported filled even when nothing was written, which made it impossible to tell from the result whether anything happened. filled and would_fill are still reported whenever a field is (or would be) written or the user is added to the site. Email addresses are now lowercased before the ability looks up or creates a user, so an address typed with capitals, such as Jane@Example.com, is stored the same way as every other address on the network. This affects both the ability and the Add User profile tab, which uses it.
- 3.1.2: Internal tidy-up of the shared class that fills in missing profile fields for the create-or-fill-user ability. Its field definitions named the user meta key under an array key called meta_key, which static analysis mistakes for a slow WP_Query meta_key lookup; it is now called usermeta_key. The code that reads the definitions is updated to match. Anything hooked to the lh_profile_return_fill_field_definitions filter would need the same rename, but nothing on the network uses it. No change in behaviour.
- 3.1.1: Housekeeping after 3.1.0. The plugin header now has an accurate description and declares its requirements and licence: WordPress 6.9 or later (for the Abilities API), PHP 8.0 or later, and GPL-2.0-or-later. Removed two leftover methods and their hooks that added phone and gender inputs to the retired LH Buddypress Add User form; the Add User tab introduced in 3.1.0 handles those fields itself, so nothing called them any more. The shared fill-fields class, the create-or-fill-user ability and the Add User tab are now loaded from the plugin's own start-up routine alongside its other classes, instead of from the end of the search-users ability file. No change in behaviour.
- 3.1.0: Adds an Add User tab to BuddyPress profiles, replacing the one from the retired LH Buddypress Add User plugin. It appears only on your own profile, and only if you can promote users (the same permission as the create-or-fill-user ability). The form takes first name, last name and email, plus optional phone, gender and a group to add the new person to. The group list shows every group to site moderators and otherwise only groups you administer or moderate. New users are created through the same code path as the create-or-fill-user ability: they get the unclaimed role on the current site, and their phone number is stored in international format. The form will not change an existing account: an email address that already belongs to someone, including a registered email alias, is refused with a message. Invalid phone numbers, unknown genders and groups you cannot manage are reported on the form without creating anything. The tab slug, its menu position and the group list are filterable.
- 3.0.1: Housekeeping after 3.0.0: added index.php guards to the assets (and its images, scripts and styles subdirectories) and includes directories, so their contents cannot be listed if directory indexing is ever enabled on the server, and corrected a code comment in the birthdate field class that still referred to the back-compat proxy methods removed in 3.0.0. No change in behaviour.
- 3.0.0: Default avatar icons are now drawn as SVG on front-end and admin pages: male, female, organisation and robot (traced from the existing PNGs) plus a new unknown icon for users with no gender set, which replaces LH Gravatar Cache's unknown_user.svg. Every avatar URL (get_avatar_url, BuddyPress avatar URLs, the Gravatar fallback, REST, feeds, email, vCard and push) stays the PNG, and the swap only ever replaces a default icon, never a photo. Breaking change: the back-compat proxy methods on LH_profile_page_plugin for the phone, gender, birthdate, address and name fields have been removed. Call the LH_Profile_*_Field classes directly instead (LH Buddypress Check Ins 3.3.2, PPT Buddypress Extender 1.0.1 and LH Multisite Importer 1.0.1 already do). New filter: lh_profile_return_default_avatar_url.
- 2.17.0: The create-or-fill-user ability can now set the core WordPress user fields user_url (website), nickname, display_name and description (biographical info), as top-level inputs alongside first_name and last_name, so records such as suppliers can carry their website and trading name. They follow the ability's fill-if-empty rule: for an existing user a field is only filled when it is empty, and a nickname or display name that is still a placeholder (empty, an email address, or the login) counts as empty, matching the test LH User Provisioning uses before it builds one from the names. For a user created by the call they are always written, so a supplied display_name replaces the name built from first and last name. The website must be an http or https address (a bare domain is given https://) of at most 100 characters; the display name is limited to 250 characters; the description keeps line breaks and has HTML removed. Invalid values are reported per field and never stored. The fields are written in one wp_update_user call, so core's own sanitising filters and hooks, including BuddyPress's display name sync, run as for a profile edit, and the reported value is what WordPress stored. user_nicename is deliberately not accepted, since on LocalHero it is always the user ID. The returned display_name is now read back after all writes, so it reflects what was actually saved.
- 2.16.2: Load the international phone input (intlTelInput.js, its stylesheet and utils.js) only when a phone field is actually on the page, and make it work in fields added after page load. Previously, rendering a phone field enqueued the stylesheet and intlTelInput.js server-side, so any page that printed one into an inert template (such as the membership registration template printed on every logged-out page) downloaded about 95 KB it never used. The phone field renderer now enqueues only the small lh-profile-page.js, which loads the stylesheet and intlTelInput.js on demand, once, when it finds a phone input – at page load, or later when one is inserted (for example when the registration dialog opens). This also fixes the phone input in dialogs built from a template: it was never initialised, so the country selector and formatting were missing and the hidden lh_profile-phone and country_code fields were not submitted, meaning phone numbers entered there were not saved. Other fixes: utils.js is now loaded from this site instead of a hardcoded localhero.biz URL, the initial country comes from the site's default country (falling back to AU), the unused ipapi.co lookup is removed, trim-on-blur now also applies to fields added after load, and a leaked global loop variable is gone.
- 2.16.1: create-or-fill-user now gives an existing user the unclaimed role on the current site when they have an account there but no role on it (for example after their role was removed in wp-admin). Previously such a user counted as already on the site and was left without a role. Users who already have a role on the site keep it unchanged. unclaimed remains the only role the ability can assign; there is no role input. The site field in the result is now one of added, would_add, role_added, would_add_role, already_on_site or failed; already_on_site replaces already_member, since a site account and role have nothing to do with organisation membership.
- 2.16.0: New MCP ability lh-profile-page/create-or-fill-user, replacing lh-buddypress-add-user/create-user (that plugin is being retired). It creates a user with the unclaimed role on the current site from a name, an email and typed profile fields (phone, secondary phone, gender, birthdate, street address, town, postcode). When existing_user is fill_missing and the email or a registered email alias already belongs to an account, it fills only what that account is missing and adds it to the current site if it is not already there, without changing any role, membership or group. No existing value is ever overwritten, and the address is filled as one unit, only when street address, town and postcode are all empty. Phones are stored as E.164, birthdates must be a real past date in Y-m-d, and gender must be one of the stored values; anything invalid is reported per field and never stored. dry_run defaults to true, and the result reports per field what was actually stored (filled, skipped, rejected or failed). The fill rules live in a new shared class, LH_Profile_Fill_Fields, so other importers can use the same logic. The new classes are loaded alongside the existing search-users ability.
- 2.15: Fix: lh-profile-page/search-users registered but wasn't exposed via MCP (mcp.public defaults false). Add meta.mcp.public=true to the wp_register_ability() args, matching every other public LH ability.
- 2.04: Extracted phone (and secondary phone) rendering/validation/storage logic into LH_Profile_Phone_Field. Added back-compat proxy shims on the god class for every extracted method, after the initial extraction assumption ("nothing external calls this") turned out wrong – ppt-lh-membership-extender called render_phone_input() directly and fatalled the site.
- 2.13: Add a filter (lh_profile_birthdate_minimum_age_years, default 0) that lets another plugin restrict the birthdate input's client-side max attribute to "today minus N years" instead of always today – purely a native HTML5 constraint (browser blocks submission of a too-recent value, calendar UI itself stays fully navigable/visible), no server-side validation added. Default of 0 preserves exactly today's existing behaviour (max=today) for anyone not hooking the filter. The UI for configuring the actual age value lives in a separate plugin – out of scope here.
- 2.12: Switch LH_Profile_Birthdate_Field from UNIX-timestamp storage to a plain ISO date string (Y-m-d), matching what natively submits/reads and having no spurious time-of-day. register_meta_fields() now registers type=string with a sanitize_callback normalising to Y-m-d (via a new shared normalise_to_ymd() helper) instead of type=integer normalising to a timestamp. render_birthdate_input() and handle_birthdate_update() simplified to use the same helper. is_valid_time_stamp() is kept (used internally by normalise_to_ymd() as a legacy-format detector, and still called externally via the god class's add_vcard_data() through its back-compat shim). This follows the full data migration already completed this session: backup taken, corrupted/unrecoverable rows identified and cleared, one row recovered from its readable companion, test accounts removed, and all remaining rows converted to Y-m-d.
- 2.11: Extracted name fields (first, last, display name) into three separate field classes – no register_meta() on any of them (first_name/last_name are core WP meta keys, display_name is a wp_users column, not usermeta). v2.12 followed shortly after, removing the three _label() shims entirely once confirmed dead (no caller anywhere, live or dead).
- 2.10: Extracted all five address fields (street address, town, postcode, state/province, country) into five separate field classes, matching the one-class-per-field precedent (postcode kept separate deliberately as a future real-validation candidate). All five registered as validated WP meta.
- 2.08: Extracted birthdate rendering/validation/storage logic into LH_Profile_Birthdate_Field, with back-compat shims. Registered lh_profile-birthdate as validated WP meta (initially UNIX-timestamp typed – later changed to a Y-m-d string in v2.13).
- 2.06: Add writecrow/country_code_converter as a proper Composer dependency, matching the phone/gender library precedent, replacing the manually-bundled includes/country_code_converter-main/ copy used by register_core_scripts() to derive the intlTelInput default country from the site's timezone.
- 2.05: Add tuqqu/gender-detector as a proper Composer dependency, matching the giggsey/libphonenumber-for-php-lite precedent, replacing the manually-bundled includes/gender-detector/ copy (currently dead code via guess_first_name_gender(), unused by any hook). Peter will remove the old bundled directory manually once redundant.
- 2.02: Fix 5 latent bugs surfaced by PHPStan (excluding the new_user_update $user_data->display_name fix, held back separately since it re-enables a currently-bypassed permission check and needs a behavioral heads-up): (1) render_phone_input's elseif checked an undefined $value — changed to else, no behavior change, just removes the undefined-variable notice. (2) _get_excluded_fields' entire body was commented out with no return statement — added an explicit `return '';` matching its documented string return type, preserving the exact current no-op behavior. (3) override_returned_value never initialized $user_id when both bp_displayed_user_id() and get_current_user_id() are falsy — added `$user_id = 0;` default. (4) and (5) gender_avatar_fallback_html and get_custom_avatar both guard with `empty($image)` on a DOMNodeList, which is never true for objects regardless of content — changed both to `0 === $image->length` so the no-image early-return actually fires. Version bump 2.01 -> 2.02.
- 2.01: Migrate write_log() to canonical LH convention (adds $level param, JSON-encodes arrays/objects, colon+2-space line shape) and add return_plugin_debugging_status() filter gate; register-file-class.php delegates return_plugin_namespace() to the main class. Version bump 2.00 -> 2.01. (Third submission: final whitespace-fidelity correction pass in get_custom_avatar/extend_user_search regions and trailing blank line of includes file.)
