@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-doc&#038;p=147303>
  a sioc:Post ;
  dc:title "Architectural Notes" ;
  dcterms:identifier 147303 ;
  dc:modified "2026-09-13T14:11:20Z"^^xsd:dateTime ;
  dc:created "2026-09-13T14:09:53Z"^^xsd:dateTime ;
  sioc:link <https://lhero.org/portfolio/lh-user-provisioning/architectural-notes/> ;
  sioc:has_creator <https://lhero.org/author/1/#account> ;
  sioc:has_container <https://lhero.org/#posts> ;
  content:encoded """<ul class="lh_portfolio-meta"><li><strong>Type:</strong> Doc-section</li><li><strong>Part of:</strong> <a href="https://lhero.org/portfolio/lh-user-provisioning/">LH User Provisioning</a></li></ul>
<h2 class="wp-block-heading">Why login/nicename convention writes bypass wp_update_user()</h2>



<p class="wp-block-paragraph"><code>ensure_login_matches_email()</code> and <code>ensure_nicename_matches_id()</code> both write via a direct SQL <code>UPDATE</code> rather than <code>wp_update_user()</code>. <code>wp_insert_user()</code>/<code>wp_update_user()</code> runs its own nicename-uniqueness dedup logic, built for human-chosen slugs that can legitimately collide (<code>john-smith</code> vs <code>john-smith-2</code>). That logic was found to treat a user&#8217;s own already-correct numeric nicename as a foreign collision and &#8220;resolve&#8221; it by appending <code>-2</code>, compounding further on every subsequent call into values like <code>1015925-2-2</code>. A nicename that is always just the numeric ID can never legitimately collide with anyone by construction, so there is nothing for that dedup logic to usefully do — going around it removes the failure mode entirely rather than working around it. It also means these writes don&#8217;t fire <code>profile_update</code>, so they can&#8217;t trigger other plugins&#8217; <code>profile_update</code> handlers or create hook re-entrancy.</p>



<p class="wp-block-paragraph">Both methods are guarded — they compare against the current value first and skip the write entirely when it&#8217;s already correct — so a no-op check costs a single cached read, and a real fix costs one row update.</p>



<h2 class="wp-block-heading">Identity resolution / alias awareness</h2>



<p class="wp-block-paragraph"><code>resolve_existing_user_by_email()</code> follows <code>lh-user-identity</code>&#8216;s documented alias-resolution pattern: check <code>wp_users</code> first, then fall back to the <code>lh_user_identity_get_user_by_email_fallback</code> filter (a documented no-op if <code>lh-user-identity</code> isn&#8217;t active). This matters because <code>ensure_user()</code> is the canonical entry point several other LH plugins delegate to — without alias-aware resolution, provisioning by an email that is a registered alias (rather than an account&#8217;s primary email) would return a false &#8220;no such user&#8221; result and create a duplicate account instead of recognising the existing one.</p>



<h2 class="wp-block-heading">Provenance tracking</h2>



<p class="wp-block-paragraph">Every newly provisioned user gets usermeta recording their origin, keyed as follows:</p>



<figure class="wp-block-table"><table><thead><tr><th>Key</th><th>Description</th></tr></thead><tbody><tr><td><code>lh_provisioning_source</code></td><td>Calling plugin slug, e.g. <code>lh-membership</code></td></tr><tr><td><code>lh_provisioning_action</code></td><td>What triggered it, e.g. <code>registration</code>, <code>checkin</code></td></tr><tr><td><code>lh_provisioning_ref</code></td><td>Optional reference ID, e.g. event ID</td></tr><tr><td><code>lh_provisioning_site_id</code></td><td>Which site triggered creation</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">No custom database tables — all provenance data lives in <code>wp_usermeta</code>. <code>lh_provisioning_source</code> and <code>lh_provisioning_action</code> are surfaced as a &#8220;Provisioned Via&#8221; column on the Network Admin → Users screen; <code>lh_provisioning_ref</code> and <code>lh_provisioning_site_id</code> are not currently shown in any UI. Provenance is only recorded on first creation, not on subsequent <code>ensure_user()</code> calls against an existing account, unless explicitly requested via <code>args['source']</code>.</p>



<h2 class="wp-block-heading">Supersedes &#8220;LH Email Usernames&#8221;</h2>



<p class="wp-block-paragraph">The old standalone &#8220;LH Email Usernames&#8221; plugin partially provided real-time login-matches-email enforcement (login only, via <code>wp_update_user()</code> plus an unconditional site-wide <code>wp_cache_flush()</code> on every profile update). That plugin is fully superseded by this one&#8217;s <code>user_register</code>/<code>profile_update</code> hooks and can be deactivated/removed.</p>
"""^^rdf:XMLLiteral ;
  sioc:content """Type: Doc-sectionPart of: LH User Provisioning
Why login/nicename convention writes bypass wp_update_user()



ensure_login_matches_email() and ensure_nicename_matches_id() both write via a direct SQL UPDATE rather than wp_update_user(). wp_insert_user()/wp_update_user() runs its own nicename-uniqueness dedup logic, built for human-chosen slugs that can legitimately collide (john-smith vs john-smith-2). That logic was found to treat a user&#8217;s own already-correct numeric nicename as a foreign collision and &#8220;resolve&#8221; it by appending -2, compounding further on every subsequent call into values like 1015925-2-2. A nicename that is always just the numeric ID can never legitimately collide with anyone by construction, so there is nothing for that dedup logic to usefully do — going around it removes the failure mode entirely rather than working around it. It also means these writes don&#8217;t fire profile_update, so they can&#8217;t trigger other plugins&#8217; profile_update handlers or create hook re-entrancy.



Both methods are guarded — they compare against the current value first and skip the write entirely when it&#8217;s already correct — so a no-op check costs a single cached read, and a real fix costs one row update.



Identity resolution / alias awareness



resolve_existing_user_by_email() follows lh-user-identity&#8216;s documented alias-resolution pattern: check wp_users first, then fall back to the lh_user_identity_get_user_by_email_fallback filter (a documented no-op if lh-user-identity isn&#8217;t active). This matters because ensure_user() is the canonical entry point several other LH plugins delegate to — without alias-aware resolution, provisioning by an email that is a registered alias (rather than an account&#8217;s primary email) would return a false &#8220;no such user&#8221; result and create a duplicate account instead of recognising the existing one.



Provenance tracking



Every newly provisioned user gets usermeta recording their origin, keyed as follows:



KeyDescriptionlh_provisioning_sourceCalling plugin slug, e.g. lh-membershiplh_provisioning_actionWhat triggered it, e.g. registration, checkinlh_provisioning_refOptional reference ID, e.g. event IDlh_provisioning_site_idWhich site triggered creation



No custom database tables — all provenance data lives in wp_usermeta. lh_provisioning_source and lh_provisioning_action are surfaced as a &#8220;Provisioned Via&#8221; column on the Network Admin → Users screen; lh_provisioning_ref and lh_provisioning_site_id are not currently shown in any UI. Provenance is only recorded on first creation, not on subsequent ensure_user() calls against an existing account, unless explicitly requested via args['source'].



Supersedes &#8220;LH Email Usernames&#8221;



The old standalone &#8220;LH Email Usernames&#8221; plugin partially provided real-time login-matches-email enforcement (login only, via wp_update_user() plus an unconditional site-wide wp_cache_flush() on every profile update). That plugin is fully superseded by this one&#8217;s user_register/profile_update hooks and can be deactivated/removed.
""" ;
  sioc:topic <https://lhero.org/?taxonomy=author&term=cap-1> .

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