Architecture

LH Contacts is a data plugin. It builds members’ contact cards, serves them as downloads, tracks when they change, and hands an address book of them to LH DAV, which handles sign-in, discovery, the CardDAV protocol and the change log. It registers no MCP abilities; tooling plugins use its service methods.

Since 0.3.0 it does everything LH Vcard did, which it replaced. None of LH Vcard’s names or stored data are used.

Classes

ClassFileRole
LH_Contactslh-contacts.phpBootstrap and public service methods. Boots on plugins_loaded at priority 20, after LH DAV, and registers the address book provider when LH DAV is present.
LH_Contacts_Card_Builderincludes/lh-contacts-card-builder-class.phpBuilds each card’s data for a viewer and serialises it as vCard 3.0 with sabre/vobject.
LH_Contacts_Downloadincludes/lh-contacts-download-class.phpSigned, expiring download links and the lh-contacts/v1/download REST route.
LH_Contacts_Change_Trackerincludes/lh-contacts-change-tracker-class.phpKeeps each card’s last-changed time per site and records changes in LH DAV’s change log.
LH_Contacts_Recently_Updatedincludes/lh-contacts-recently-updated-class.phpRecently Updated Contacts dashboard widget and shortcode, and the per-site backfill of last-changed times.
LH_Contacts_Integrationsincludes/lh-contacts-integrations-class.phpDownload links on the Users list, BuddyPress profiles and groups, LH Buddypress Group Reports, LH Buddypress Check Ins and bbPress.
LH_Contacts_Profile_Searchincludes/lh-contacts-profile-search-class.phpAdds vcard_url to LH Profile Page’s search-users results.
LH_Contacts_Members_Providerincludes/lh-contacts-members-provider-class.phpThe read-only member address book, implementing LH_DAV_Collection_Provider.

sabre/vobject comes from the plugin’s own Composer install (composer.json, vendor/).

Who can see member contacts

One capability decides everything: publish_posts by default, filterable with lh_contacts_return_view_capability. Users with it on a site see download links there, the dashboard widget and shortcode, and the member address book. Everyone else sees none of them.

Cards

Every card is built for a viewer, the person it is shown to. A card holds the member’s name, email, photo (256px; none if they never uploaded one), the site name as organisation, their profile link, a note from their biography, a stable UID, and a REV from the last-changed time. Their BuddyPress groups appear as teams: public and private groups always, hidden groups only for a viewer who belongs to them or can moderate BuddyPress. Teams are written both as CATEGORIES (labels in Android and Google Contacts) and as one labelled URL per team (a tappable row on iPhone).

Other plugins add fields through lh_contacts_add_data: LH Profile Page adds phone, address, birthday and gender, and LH User Identity adds verified alias emails.

The UID is a name-based UUID from the network address and the user ID, so a re-imported card updates the existing contact rather than duplicating it.

Downloads

A download link names the users, the viewer the cards are built for, and an expiry (a day by default), signed with the site’s secret. It is not a WordPress nonce, so it works for whoever opens it, without logging in, until it expires; that is why every link is only shown to users with the capability. The route serves only members of the current site, gives the same 404 for any failure, and sends the cards as text/vcard, inline and never cached.

The member address book

Each site serves one, at addressbooks/{user ID}/members/: everyone on the site, whatever their role or membership status, less anyone blocked on it (LH Buddypress Block Members). It is read-only; writes are refused with 403.

Resources are named {user ID}.vcf. A card’s ETag combines the plugin version, the user ID, the card’s last-changed time and the viewer’s visibility hash, so a card is resent when it changes, when what the viewer may see changes, or after a new release. The collection’s state carries the viewer hash and version too, so either change starts that viewer on a full resync.

Change tracking

Each card’s last-changed time is kept per site as user meta, {site prefix}lh_contacts-updated_time. It moves when tracked user meta changes (names, nickname, biography, plus keys added by lh_contacts_return_all_tracked_meta_fields), when BuddyPress profile fields, the avatar, email or display name change, when the member joins or leaves a group, and when one of their groups is renamed or changes status. Every such change is recorded in LH DAV’s change log as the card being modified on each of the member’s sites. A member leaving a site, or being blocked on it, is recorded as the card being deleted there; unblocking records it as modified again.

The first time each site loads after activation (and after any change of schema version), a backfill gives every member without a last-changed time one of now, without change log entries.