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
15 changes: 10 additions & 5 deletions lib/Horde/Core/ActiveSync/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2248,7 +2248,7 @@ public function statMessage($folderid, $id)
* @param string $folderid The folder id
* @param array $ids The message ids to delete
* @param boolean $instanceids If true, $ids is a hash of
* instanceids => uids. @since 2.23.0
* uids => instanceids. @since 2.23.0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly speaking this is a signature BC break.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6_1?

*
* @return array An array of succesfully deleted messages (currently
* only guarenteed for email messages).
Expand Down Expand Up @@ -2277,22 +2277,26 @@ public function deleteMessage($folderid, array $ids, $instanceids = false)

switch ($class) {
case Horde_ActiveSync::CLASS_CALENDAR:
// Keep $ids as an array for the catch-path foreach below.
// Callers pass [uid => instanceid] when $instanceids is true
// (see Horde_ActiveSync_Connector_Importer).
if ($instanceids) {
$instanceid = reset($ids);
$ids = key($ids);
$deleteIds = key($ids);
} else {
$instanceid = false;
$deleteIds = $ids;
}
try {
$this->_logger->meta(
sprintf(
'calendar_delete: %s %s %s',
print_r($ids, true),
print_r($deleteIds, true),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't print_r to the general logger. If you really must dump this data, make it a configurable extra file.

$folder_id,
$instanceid
)
);
$this->_connector->calendar_delete($ids, $folder_id, $instanceid);
$this->_connector->calendar_delete($deleteIds, $folder_id, $instanceid);
} catch (Horde_Exception $e) {
// Since we don't get back successfully deleted ids and we can
// can pass an array of ids to delete, we need to see what ids
Expand All @@ -2301,7 +2305,8 @@ public function deleteMessage($folderid, array $ids, $instanceids = false)
// deleted ids.
$this->_logger->err($e->getMessage());
$success = [];
foreach ($ids as $uid) {
$checkIds = is_array($deleteIds) ? $deleteIds : [$deleteIds];
foreach ($checkIds as $uid) {
if ($mod_time = $this->_connector->calendar_getActionTimestamp($uid, 'delete', $folder_id)) {
$success[] = $uid;
}
Expand Down
112 changes: 112 additions & 0 deletions test/Unit/ActiveSync/DriverDeleteMessageInstanceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?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 Core
* @subpackage UnitTests
*/

namespace Horde\Core\Test\Unit\ActiveSync;

use Horde\Http\ServerRequest;
use Horde_ActiveSync;
use Horde_Core_ActiveSync_Auth;
use Horde_Core_ActiveSync_Connector;
use Horde_Core_ActiveSync_Driver;
use Horde_Exception;
use Horde_Log_Handler_Null;
use Horde_Log_Logger;
use Horde_Registry;
use PHPUnit\Framework\Attributes\CoversMethod;
use PHPUnit\Framework\TestCase;

/**
* Regression: calendar instance deletes must not foreach() a string UID when
* calendar_delete() throws (PHP 8 TypeError / warning on recovery path).
*/
#[CoversMethod(Horde_Core_ActiveSync_Driver::class, 'deleteMessage')]
class DriverDeleteMessageInstanceTest extends TestCase
{
private function createDriver(Horde_Core_ActiveSync_Connector $connector): Horde_Core_ActiveSync_Driver
{
$state = $this->getMockBuilder('Horde_ActiveSync_State_Sql')
->disableOriginalConstructor()
->getMock();
$state->method('setLogger');
$state->method('setBackend');

$auth = $this->getMockBuilder(Horde_Core_ActiveSync_Auth::class)
->disableOriginalConstructor()
->getMock();
$registry = $this->getMockBuilder(Horde_Registry::class)
->disableOriginalConstructor()
->getMock();

$driver = new Horde_Core_ActiveSync_Driver([
'connector' => $connector,
'auth' => $auth,
'serverrequest' => new ServerRequest('POST', '/'),
'registry' => $registry,
'state' => $state,
]);
$driver->setLogger(new Horde_Log_Logger(new Horde_Log_Handler_Null()));

return $driver;
}

public function testCalendarInstanceDeleteErrorPathDoesNotForeachString(): void
{
$uid = 'event-uid-example';
$instanceId = '20250808T153000Z';
$folder = 'Calendar:cal-example';

$connector = $this->getMockBuilder(Horde_Core_ActiveSync_Connector::class)
->disableOriginalConstructor()
->onlyMethods(['calendar_delete', 'calendar_getActionTimestamp'])
->getMock();
$connector->expects($this->once())
->method('calendar_delete')
->with($uid, 'cal-example', $instanceId)
->willThrowException(new Horde_Exception('not found'));
$connector->expects($this->once())
->method('calendar_getActionTimestamp')
->with($uid, 'delete', 'cal-example')
->willReturn(false);

$driver = $this->createDriver($connector);
$results = $driver->deleteMessage($folder, [$uid => $instanceId], true);

$this->assertSame([], $results);
}

public function testCalendarInstanceDeleteErrorPathReportsSuccessfulUid(): void
{
$uid = 'event-uid-example';
$instanceId = '20250808T153000Z';
$folder = 'Calendar:cal-example';

$connector = $this->getMockBuilder(Horde_Core_ActiveSync_Connector::class)
->disableOriginalConstructor()
->onlyMethods(['calendar_delete', 'calendar_getActionTimestamp'])
->getMock();
$connector->method('calendar_delete')
->willThrowException(new Horde_Exception('not found'));
$connector->method('calendar_getActionTimestamp')
->willReturn(1234567890);

$driver = $this->createDriver($connector);
$results = $driver->deleteMessage($folder, [$uid => $instanceId], true);

$this->assertSame([$uid], $results);
}
}
Loading