Digital Sovereignty for everyone

Architectural Notes & Gotchas

Architectural Notes & Gotchas

Composer-managed dependencies

Three third-party libraries, all under vendor/ via composer.json (not manually bundled copies):

  • giggsey/libphonenumber-for-php-lite โ€“ phone number parsing/validation (E.164).
  • tuqqu/gender-detector (v0.5.2) โ€“ migrated from a manually-bundled includes/gender-detector/ copy this session. Note the API changed between versions: the old bundled copy used ->detect($name) returning a string; the current Composer version uses ->getGender($name) returning a Gender enum. guess_first_name_gender() uses the new API โ€“ but this method has zero callers anywhere in the codebase currently (confirmed via grep), so it’s dead code, not wired to any hook.
  • writecrow/country_code_converter (v1.3.0) โ€“ migrated from a manually-bundled includes/country_code_converter-main/ copy this session. Same convert() API in both versions, no call-site rewrite needed. Used in register_core_scripts() to derive the phone widget’s default country from the site’s timezone.

The old bundled copies (includes/gender-detector/, includes/country_code_converter-main/) are safe to delete โ€“ nothing references them anymore.

Birthdate storage format history

lh_profile-birthdate was originally a UNIX timestamp. This was a poor fit: no meaningful time-of-day, an implicit timezone-dependent midnight, negative-timestamp handling needed for anyone born before 1970, and it fought the native <input type="date"> field’s own Y-m-d value round-trip for no benefit. It was also written inconsistently โ€“ some code paths wrote a timestamp, others wrote a raw date string directly.

v2.13 switched storage to a plain Y-m-d ISO date string, alongside a full one-off data migration:

  1. Backup taken first (lh_profile-birthdate_backup_20260908 / _readable_backup_20260908 meta keys, covering every user).
  2. A cross-field comparison (both lh_profile-birthdate and lh_profile-birthdate_readable normalised to a common format before comparing, to avoid false positives from pure formatting differences) found one genuine discrepancy: one user’s primary field had been overwritten with a bad value while the _readable companion still held the correct original โ€“ restored from there.
  3. Four throwaway test accounts (shawfactor+...@gmail.com addresses) were found and deleted.
  4. 39 real members had a birthdate that converted to an implausible year (post-2016, given the club has no members under 12). Investigation showed this was caused by a live bug โ€“ not root-caused โ€“ that writes the write-time “now” into the field: some rows landed within days of the affected user’s own registration date (consistent with a signup-time default), while others showed the exact same date despite completely unrelated registration years (consistent with a separate batch/cron process touching existing records on one specific day). Both lh_profile-birthdate and lh_profile-birthdate_readable held the identical wrong value for these 39, meaning the true original birthdate was unrecoverable โ€“ they were cleared rather than left holding a wrong value dressed up as clean data.
  5. Every remaining row converted from its stored format (timestamp or already-correct string) to Y-m-d.

The root cause of the “now” bug was never traced โ€“ deliberately deferred, since this plugin had many structural changes around this time and some bad data was considered likely regardless.

Minimum-age filter (v2.14)

render_birthdate_input()‘s max attribute is filterable via lh_profile_birthdate_minimum_age_years (default 0, i.e. today โ€“ unchanged behaviour). Restricting it to “today minus N years” is a client-side-only deterrent against the “now” bug’s laziness angle: the native HTML5 date input’s constraint validation refuses to submit a too-recent value, while the calendar picker UI itself stays fully browsable โ€“ nothing is hidden or disabled beyond blocking selection past the cutoff. Not server-side validation. See task 147165 (queue lh-membership) for wiring this up to a real configured value.

Discovery: lh_profile-emails reveals likely duplicate accounts

While investigating birthdate data, a related discovery: 1,888 users have a non-empty lh_profile-emails value, and the large majority follow a firstname.lastname@theaustraliatimes.com.au <-> real-email cross-reference pattern in both directions โ€“ strongly suggesting a large number of duplicate WordPress accounts (one with a real email as primary, one with a fabricated placeholder email as primary, each referencing the other as “secondary”). A saved query (“Users with a secondary email”, lh_profile-emails, post 147118) and a written exploration brief exist for this โ€“ it’s an lh-user-identity-adjacent problem, out of scope for this plugin, not investigated further here.

File size / MCP editing note

lh-profile-page.php (the god class) is ~115KB. Proposing changes to it via lh-mcp-code-changes MCP tooling risks payload-size issues; the working pattern this session was to build/verify the change in a local sandbox copy, then hand the full file back for manual upload via SFTP/hosting panel, rather than propose-code-change/apply-code-change. The extracted field class files (each a few KB) apply cleanly via the normal MCP flow.

Manual-upload ordering matters: bp_init() unconditionally require_onces all nine xProfile extender files, and plugin_init() unconditionally require_onces every field class file โ€“ if a file containing a require_once for another file is uploaded before that other file exists, any page load in that window fatals. Always upload newly-created dependency files before the file that requires them.

If you like this plugin please consider making a donation to further development

Install LHero.org

lhero.org

Open it straight from your home screen, full screen and quicker to load.

  1. Tap the Share button . If you cannot see it, tap โ€ขโ€ขโ€ข first.
  2. Scroll down and tap Add to Home Screen.
  3. Tap Add.
  1. Tap the browser menu โ‹ฎ.
  2. Tap Install app or Add to Home screen.
  1. In the menu bar, choose File, then Add to Dock.

Use your browser's menu to install this site or add it to your home screen.