Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions lib/Horde/Imap/Client/Base.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?php

/**
Expand Down Expand Up @@ -712,7 +712,8 @@
* additional namespaces to add to the
* namespace list that are not broadcast by
* the server. The namespaces must be UTF-8
* strings.
* strings. Non-scalar entries (e.g. nested
* arrays) are ignored.
* @param array $opts Additional options:
* - ob_return: (boolean) If true, returns a
* Horde_Imap_Client_Namespace_List object instead of an
Expand Down Expand Up @@ -741,7 +742,19 @@
array $additional = [],
array $opts = []
) {
$additional = array_map('strval', $additional);
/* Only scalar/Stringable values are valid namespace names. Nested
* arrays (e.g. from a misconfigured IMP backends.php 'namespace'
* entry) must not be passed to strval() — that triggers
* "Array to string conversion" on PHP 8+. */
$normalized = [];
foreach ($additional as $val) {
if (is_array($val) ||
(is_object($val) && !($val instanceof Stringable))) {
continue;
}
$normalized[] = strval($val);
}
$additional = $normalized;
$sig = hash(
'md5',
json_encode($additional) . intval(empty($opts['ob_return']))
Expand All @@ -756,7 +769,7 @@

/* Skip namespaces if we have already auto-detected them. Also,
* hidden namespaces cannot be empty. */
$to_process = array_diff(array_filter($additional, 'strlen'), array_map('strlen', iterator_to_array($ns)));
$to_process = array_diff(array_filter($additional, 'strlen'), array_map('strval', iterator_to_array($ns)));
if (!empty($to_process)) {
foreach ($this->listMailboxes($to_process, Horde_Imap_Client::MBOX_ALL, ['delimiter' => true]) as $key => $val) {
$ob = new Horde_Imap_Client_Data_Namespace();
Expand Down
165 changes: 165 additions & 0 deletions test/Unit/Base/GetNamespacesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @author Torben Dannhauer <torben@dannhauer.de>
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Imap_Client
*/

namespace Horde\Imap\Client\Test\Unit\Base;

use Horde\Imap\Client\Test\Stub\Base;
use Horde_Imap_Client_Data_Namespace;
use Horde_Imap_Client_Mailbox;
use Horde_Imap_Client_Namespace_List;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;

/**
* Tests for Horde_Imap_Client_Base#getNamespaces().
*
* @author Torben Dannhauer <torben@dannhauer.de>
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
*/
#[CoversNothing]
class GetNamespacesTest extends TestCase
{
/**
* Nested-array additional namespaces must not trigger
* "Array to string conversion" (horde/imp#94).
*/
public function testAdditionalNestedArraysDoNotWarn(): void
{
$ob = new class ([
'username' => 'user',
'password' => 'pass',
]) extends Base {
protected function _getNamespaces()
{
$ns = new Horde_Imap_Client_Data_Namespace();
$ns->delimiter = '/';
$ns->name = '';
$ns->type = Horde_Imap_Client_Data_Namespace::NS_PERSONAL;

return new Horde_Imap_Client_Namespace_List([$ns]);
}
};

$result = $ob->getNamespaces(
[
'#shared/',
'#broken' => ['delimiter' => '/'],
['nested'],
],
['ob_return' => true]
);

$this->assertInstanceOf(Horde_Imap_Client_Namespace_List::class, $result);
$this->assertCount(1, $result);
}

/**
* Already-advertised namespaces in $additional must be skipped
* (array_map must use strval, not strlen, on the namespace list).
*/
public function testSkipsAlreadyDetectedAdditionalNamespaces(): void
{
$listed = [];

$ob = new class ([
'username' => 'user',
'password' => 'pass',
], $listed) extends Base {
/** @var array */
private $listedRef;

public function __construct(array $params, array &$listed)
{
parent::__construct($params);
$this->listedRef = &$listed;
}

protected function _getNamespaces()
{
$ns = new Horde_Imap_Client_Data_Namespace();
$ns->delimiter = '/';
$ns->name = '#shared/';
$ns->type = Horde_Imap_Client_Data_Namespace::NS_SHARED;

return new Horde_Imap_Client_Namespace_List([$ns]);
}

protected function _listMailboxes($pattern, $mode, $options)
{
$this->listedRef[] = $pattern;

return [];
}
};

$ob->getNamespaces(['#shared/', '#public/'], ['ob_return' => true]);

$this->assertCount(1, $listed);
$this->assertSame(
['#public/'],
array_map('strval', $listed[0])
);
}

public function testStringableAdditionalNamespacesAreAccepted(): void
{
$listed = [];

$ob = new class ([
'username' => 'user',
'password' => 'pass',
], $listed) extends Base {
/** @var array */
private $listedRef;

public function __construct(array $params, array &$listed)
{
parent::__construct($params);
$this->listedRef = &$listed;
}

protected function _getNamespaces()
{
$ns = new Horde_Imap_Client_Data_Namespace();
$ns->delimiter = '/';
$ns->name = '';
$ns->type = Horde_Imap_Client_Data_Namespace::NS_PERSONAL;

return new Horde_Imap_Client_Namespace_List([$ns]);
}

protected function _listMailboxes($pattern, $mode, $options)
{
$this->listedRef[] = $pattern;

return [];
}
};

$ob->getNamespaces(
[Horde_Imap_Client_Mailbox::get('#shared/')],
['ob_return' => true]
);

$this->assertCount(1, $listed);
$this->assertSame(
['#shared/'],
array_map('strval', $listed[0])
);
}
}
Loading