diff --git a/lib/Horde/Core/ActiveSync/Connector.php b/lib/Horde/Core/ActiveSync/Connector.php index 922182b4..09ecbfba 100644 --- a/lib/Horde/Core/ActiveSync/Connector.php +++ b/lib/Horde/Core/ActiveSync/Connector.php @@ -456,6 +456,12 @@ public function getRecipientCache($max = 100) */ public function contacts_search($query, array $options = []) { + // IMP can advertise contacts/favouriteRecipients without a full contacts + // provider (Turba). hasInterface('contacts') is false in that case. + if (!$this->_registry->hasInterface('contacts')) { + return []; + } + if ((!$gal = $this->contacts_getGal()) && empty($options['recipient_cache_search'])) { return []; } @@ -522,6 +528,10 @@ public function resolveRecipient($query, array $opts = []) } } + if (!$this->_registry->hasInterface('contacts')) { + return [$query => []]; + } + $gal = $this->contacts_getGal(); $sources = array_keys($this->_registry->contacts->sources(false, true)); if (!in_array($gal, $sources)) { @@ -556,8 +566,14 @@ public function resolveRecipient($query, array $opts = []) */ public function contacts_getGal() { - if (empty($this->_gal)) { - $this->_gal = $this->_registry->contacts->getGalUid(); + if (!isset($this->_gal)) { + // Avoid $registry->contacts->getGalUid() when no contacts provider + // is registered. listAPIs() may still list "contacts" because IMP + // provides contacts/favouriteRecipients, which makes __get('contacts') + // succeed and then call() throw for getGalUid. + $this->_gal = $this->_registry->hasMethod('contacts/getGalUid') + ? $this->_registry->contacts->getGalUid() + : false; } return $this->_gal; } diff --git a/test/Unit/ActiveSync/ConnectorContactsGetGalTest.php b/test/Unit/ActiveSync/ConnectorContactsGetGalTest.php new file mode 100644 index 00000000..fad0d086 --- /dev/null +++ b/test/Unit/ActiveSync/ConnectorContactsGetGalTest.php @@ -0,0 +1,118 @@ + + * @category Horde + * @copyright 2026 The Horde Project + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @package Core + * @subpackage UnitTests + */ + +namespace Horde\Core\Test\Unit\ActiveSync; + +use Horde_Core_ActiveSync_Connector; +use Horde_Registry; +use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\TestCase; + +/** + * Unit tests for Horde_Core_ActiveSync_Connector contacts helpers without Turba. + */ +#[CoversMethod(Horde_Core_ActiveSync_Connector::class, 'contacts_getGal')] +#[CoversMethod(Horde_Core_ActiveSync_Connector::class, 'contacts_search')] +#[CoversMethod(Horde_Core_ActiveSync_Connector::class, 'resolveRecipient')] +class ConnectorContactsGetGalTest extends TestCase +{ + public function testContactsGetGalReturnsFalseWhenMethodMissing(): void + { + $registry = $this->createMock(Horde_Registry::class); + $registry->expects($this->once()) + ->method('hasMethod') + ->with('contacts/getGalUid') + ->willReturn(false); + $registry->expects($this->never()) + ->method('__get'); + + $connector = new Horde_Core_ActiveSync_Connector([ + 'registry' => $registry, + ]); + + $this->assertFalse($connector->contacts_getGal()); + // Cached: second call must not hit the registry again. + $this->assertFalse($connector->contacts_getGal()); + } + + public function testContactsGetGalReturnsUidWhenAvailable(): void + { + $contacts = new class () { + public function getGalUid() + { + return 'gal-source'; + } + }; + + $registry = $this->createMock(Horde_Registry::class); + $registry->expects($this->once()) + ->method('hasMethod') + ->with('contacts/getGalUid') + ->willReturn(true); + $registry->expects($this->once()) + ->method('__get') + ->with('contacts') + ->willReturn($contacts); + + $connector = new Horde_Core_ActiveSync_Connector([ + 'registry' => $registry, + ]); + + $this->assertSame('gal-source', $connector->contacts_getGal()); + $this->assertSame('gal-source', $connector->contacts_getGal()); + } + + public function testContactsSearchReturnsEmptyWithoutContactsInterface(): void + { + $registry = $this->createMock(Horde_Registry::class); + $registry->expects($this->once()) + ->method('hasInterface') + ->with('contacts') + ->willReturn(false); + $registry->expects($this->never()) + ->method('hasMethod'); + $registry->expects($this->never()) + ->method('__get'); + + $connector = new Horde_Core_ActiveSync_Connector([ + 'registry' => $registry, + ]); + + $this->assertSame([], $connector->contacts_search('alice')); + } + + public function testResolveRecipientReturnsEmptyWithoutContactsInterface(): void + { + $registry = $this->createMock(Horde_Registry::class); + $registry->expects($this->once()) + ->method('hasInterface') + ->with('contacts') + ->willReturn(false); + $registry->expects($this->never()) + ->method('__get'); + + $connector = new Horde_Core_ActiveSync_Connector([ + 'registry' => $registry, + ]); + + $this->assertSame( + ['alice@example.com' => []], + $connector->resolveRecipient('alice@example.com') + ); + } +}