Search contact suggestions across several LDAP branches

A directory rarely keeps everything worth suggesting in one branch:
people sit under ou=People, while meeting rooms and other bookable
resources live elsewhere - ou=Resources on our installations. Because
the plugin queries a single base DN, typing part of a room's name while
composing an invitation returns nothing, even though the entry exists.

Widening the base to the domain root is not a fix: it pulls every
service account into the suggestion list.

base_dn now accepts several branches separated by '|', each queried with
the same filter, results concatenated. That separator cannot appear
unescaped in a DN, so existing single-branch configurations keep working
untouched. An unreachable branch is logged with its own DN and no longer
silences the remaining ones - previously a single failed search
discarded the whole lookup.

Plugin version bumped to 2.15.
This commit is contained in:
Fathi BEN NASR 2026-08-19 17:20:21 +00:00
parent c154d23cfe
commit f0bfa29133
2 changed files with 32 additions and 20 deletions

View file

@ -139,29 +139,41 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
$sFilter .= (1 < count($aItems) ? '(|' : '').$sSubFilter.(1 < count($aItems) ? ')' : ''); $sFilter .= (1 < count($aItems) ? '(|' : '').$sSubFilter.(1 < count($aItems) ? ')' : '');
$sFilter .= ')'; $sFilter .= ')';
$this->logWrite('ldap_search: start: '.$sBaseDn.' / '.$sFilter, \LOG_INFO, 'LDAP'); // A directory rarely keeps everything worth suggesting in one branch:
$oS = @\ldap_search($oCon, $sBaseDn, $sFilter, $aItems, 0, 30, 30); // meeting rooms and other bookable resources commonly live outside
if ($oS) { // the people branch. Searching a single subtree either misses them,
$aEntries = @\ldap_get_entries($oCon, $oS); // or - if the base is widened to the domain root - drags every
if (is_array($aEntries)) { // service account into the suggestion list. Base DNs are therefore
if (isset($aEntries['count'])) { // separated by '|', which cannot appear unescaped in a DN, so an
unset($aEntries['count']); // existing single-branch configuration keeps working unchanged.
} $aBaseDns = \array_filter(\array_map('trim', \explode('|', $sBaseDn)), 'strlen');
foreach ($aEntries as $aItem) { foreach ($aBaseDns as $sOneBaseDn) {
if ($aItem) { $this->logWrite('ldap_search: start: '.$sOneBaseDn.' / '.$sFilter, \LOG_INFO, 'LDAP');
$sName = $sEmail = ''; $oS = @\ldap_search($oCon, $sOneBaseDn, $sFilter, $aItems, 0, 30, 30);
list ($sEmail, $sName) = $this->findNameAndEmail($aItem, $aEmails, $aNames, $aUIDs); if ($oS) {
if (!empty($sEmail)) { $aEntries = @\ldap_get_entries($oCon, $oS);
$aResult[] = array($sEmail, $sName); if (is_array($aEntries)) {
if (isset($aEntries['count'])) {
unset($aEntries['count']);
}
foreach ($aEntries as $aItem) {
if ($aItem) {
$sName = $sEmail = '';
list ($sEmail, $sName) = $this->findNameAndEmail($aItem, $aEmails, $aNames, $aUIDs);
if (!empty($sEmail)) {
$aResult[] = array($sEmail, $sName);
}
} }
} }
} else {
$this->logLdapError($oCon, 'ldap_get_entries');
} }
} else { } else {
$this->logLdapError($oCon, 'ldap_get_entries'); // One unreachable branch must not silence the others.
$this->logLdapError($oCon, 'ldap_search ('.$sOneBaseDn.')');
} }
} else {
$this->logLdapError($oCon, 'ldap_search');
} }
} }

View file

@ -4,8 +4,8 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
{ {
const const
NAME = 'Contacts suggestions (LDAP)', NAME = 'Contacts suggestions (LDAP)',
VERSION = '2.14', VERSION = '2.15',
RELEASE = '2024-03-12', RELEASE = '2026-08-19',
REQUIRED = '2.35.3', REQUIRED = '2.35.3',
CATEGORY = 'Contacts', CATEGORY = 'Contacts',
DESCRIPTION = 'Get contacts suggestions from LDAP.'; DESCRIPTION = 'Get contacts suggestions from LDAP.';
@ -79,7 +79,7 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD)
->SetDefaultValue(''), ->SetDefaultValue(''),
\RainLoop\Plugins\Property::NewInstance('base_dn')->SetLabel('Search base DN') \RainLoop\Plugins\Property::NewInstance('base_dn')->SetLabel('Search base DN')
->SetDescription('DN to use as the search base. Supported tokens: {domain}, {domain:dc}, {email}, {email:user}, {email:domain}, {login}, {imap:login}, {imap:host}, {imap:port}') ->SetDescription('DN to use as the search base. Supported tokens: {domain}, {domain:dc}, {email}, {email:user}, {email:domain}, {login}, {imap:login}, {imap:host}, {imap:port} Several branches may be given, separated by | - useful when meeting rooms or other resources live outside the people branch.')
->SetDefaultValue('ou=People,dc=example,dc=com'), ->SetDefaultValue('ou=People,dc=example,dc=com'),
\RainLoop\Plugins\Property::NewInstance('object_classes')->SetLabel('objectClasses') \RainLoop\Plugins\Property::NewInstance('object_classes')->SetLabel('objectClasses')
->SetDescription('LDAP objectClasses to search for, comma separated list') ->SetDescription('LDAP objectClasses to search for, comma separated list')